1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.model;
17 
18 import static android.provider.BaseColumns._ID;
19 import static android.util.Base64.NO_PADDING;
20 import static android.util.Base64.NO_WRAP;
21 
22 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
24 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
26 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
27 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
28 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
29 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
30 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
31 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
32 
33 import android.app.blob.BlobHandle;
34 import android.app.blob.BlobStoreManager;
35 import android.content.ContentResolver;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.content.pm.PackageManager;
39 import android.content.pm.ProviderInfo;
40 import android.content.res.Resources;
41 import android.database.Cursor;
42 import android.database.SQLException;
43 import android.database.sqlite.SQLiteDatabase;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.os.ParcelFileDescriptor;
47 import android.os.Process;
48 import android.os.UserHandle;
49 import android.os.UserManager;
50 import android.provider.Settings;
51 import android.text.TextUtils;
52 import android.util.Base64;
53 import android.util.Log;
54 import android.util.Xml;
55 
56 import androidx.annotation.Nullable;
57 import androidx.annotation.WorkerThread;
58 
59 import com.android.launcher3.AutoInstallsLayout;
60 import com.android.launcher3.AutoInstallsLayout.SourceResources;
61 import com.android.launcher3.ConstantItem;
62 import com.android.launcher3.DefaultLayoutParser;
63 import com.android.launcher3.EncryptionType;
64 import com.android.launcher3.InvariantDeviceProfile;
65 import com.android.launcher3.LauncherAppState;
66 import com.android.launcher3.LauncherFiles;
67 import com.android.launcher3.LauncherPrefs;
68 import com.android.launcher3.LauncherSettings;
69 import com.android.launcher3.LauncherSettings.Favorites;
70 import com.android.launcher3.Utilities;
71 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
72 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
73 import com.android.launcher3.logging.FileLog;
74 import com.android.launcher3.pm.UserCache;
75 import com.android.launcher3.provider.LauncherDbUtils;
76 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
77 import com.android.launcher3.provider.RestoreDbTask;
78 import com.android.launcher3.util.IOUtils;
79 import com.android.launcher3.util.IntArray;
80 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
81 import com.android.launcher3.util.Partner;
82 import com.android.launcher3.widget.LauncherWidgetHolder;
83 
84 import org.xmlpull.v1.XmlPullParser;
85 
86 import java.io.InputStream;
87 import java.io.StringReader;
88 
89 /**
90  * Utility class which maintains an instance of Launcher database and provides utility methods
91  * around it.
92  */
93 public class ModelDbController {
94     private static final String TAG = "LauncherProvider";
95 
96     private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
97     public static final String EXTRA_DB_NAME = "db_name";
98 
99     protected DatabaseHelper mOpenHelper;
100 
101     private final Context mContext;
102 
ModelDbController(Context context)103     public ModelDbController(Context context) {
104         mContext = context;
105     }
106 
createDbIfNotExists()107     private synchronized void createDbIfNotExists() {
108         if (mOpenHelper == null) {
109             mOpenHelper = createDatabaseHelper(false /* forMigration */);
110             RestoreDbTask.restoreIfNeeded(mContext, this);
111         }
112     }
113 
createDatabaseHelper(boolean forMigration)114     protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
115         boolean isSandbox = mContext instanceof SandboxContext;
116         String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
117 
118         // Set the flag for empty DB
119         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
120                 : () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbName).to(true));
121 
122         DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbName,
123                 this::getSerialNumberForUser, onEmptyDbCreateCallback);
124         // Table creation sometimes fails silently, which leads to a crash loop.
125         // This way, we will try to create a table every time after crash, so the device
126         // would eventually be able to recover.
127         if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
128             Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
129             // This operation is a no-op if the table already exists.
130             addTableToDb(databaseHelper.getWritableDatabase(),
131                     getSerialNumberForUser(Process.myUserHandle()),
132                     true /* optional */);
133         }
134         databaseHelper.mHotseatRestoreTableExists = tableExists(
135                 databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
136 
137         databaseHelper.initIds();
138         return databaseHelper;
139     }
140 
141     /**
142      * Refer {@link SQLiteDatabase#query}
143      */
144     @WorkerThread
query(String table, String[] projection, String selection, String[] selectionArgs, String sortOrder)145     public Cursor query(String table, String[] projection, String selection,
146             String[] selectionArgs, String sortOrder) {
147         createDbIfNotExists();
148         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
149         Cursor result = db.query(
150                 table, projection, selection, selectionArgs, null, null, sortOrder);
151 
152         final Bundle extra = new Bundle();
153         extra.putString(EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
154         result.setExtras(extra);
155         return result;
156     }
157 
158     /**
159      * Refer {@link SQLiteDatabase#insert(String, String, ContentValues)}
160      */
161     @WorkerThread
insert(String table, ContentValues initialValues)162     public int insert(String table, ContentValues initialValues) {
163         createDbIfNotExists();
164 
165         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
166         addModifiedTime(initialValues);
167         int rowId = mOpenHelper.dbInsertAndCheck(db, table, initialValues);
168         if (rowId >= 0) {
169             onAddOrDeleteOp(db);
170         }
171         return rowId;
172     }
173 
174     /**
175      * Refer {@link SQLiteDatabase#delete(String, String, String[])}
176      */
177     @WorkerThread
delete(String table, String selection, String[] selectionArgs)178     public int delete(String table, String selection, String[] selectionArgs) {
179         createDbIfNotExists();
180         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
181 
182         int count = db.delete(table, selection, selectionArgs);
183         if (count > 0) {
184             onAddOrDeleteOp(db);
185         }
186         return count;
187     }
188 
189     /**
190      * Refer {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
191      */
192     @WorkerThread
update(String table, ContentValues values, String selection, String[] selectionArgs)193     public int update(String table, ContentValues values,
194             String selection, String[] selectionArgs) {
195         createDbIfNotExists();
196 
197         addModifiedTime(values);
198         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
199         int count = db.update(table, values, selection, selectionArgs);
200         return count;
201     }
202 
203     /**
204      * Clears a previously set flag corresponding to empty db creation
205      */
206     @WorkerThread
clearEmptyDbFlag()207     public void clearEmptyDbFlag() {
208         createDbIfNotExists();
209         clearFlagEmptyDbCreated();
210     }
211 
212     /**
213      * Generates an id to be used for new item in the favorites table
214      */
215     @WorkerThread
generateNewItemId()216     public int generateNewItemId() {
217         createDbIfNotExists();
218         return mOpenHelper.generateNewItemId();
219     }
220 
221     /**
222      * Generates an id to be used for new workspace screen
223      */
224     @WorkerThread
getNewScreenId()225     public int getNewScreenId() {
226         createDbIfNotExists();
227         return mOpenHelper.getNewScreenId();
228     }
229 
230     /**
231      * Creates an empty DB clearing all existing data
232      */
233     @WorkerThread
createEmptyDB()234     public void createEmptyDB() {
235         createDbIfNotExists();
236         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
237         LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey().to(true));
238     }
239 
240     /**
241      * Removes any widget which are present in the framework, but not in out internal DB
242      */
243     @WorkerThread
removeGhostWidgets()244     public void removeGhostWidgets() {
245         createDbIfNotExists();
246         mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
247     }
248 
249     /**
250      * Returns a new {@link SQLiteTransaction}
251      */
252     @WorkerThread
newTransaction()253     public SQLiteTransaction newTransaction() {
254         createDbIfNotExists();
255         return new SQLiteTransaction(mOpenHelper.getWritableDatabase());
256     }
257 
258     /**
259      * Refreshes the internal state corresponding to presence of hotseat table
260      */
261     @WorkerThread
refreshHotseatRestoreTable()262     public void refreshHotseatRestoreTable() {
263         createDbIfNotExists();
264         mOpenHelper.mHotseatRestoreTableExists = tableExists(
265                 mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
266     }
267 
268 
269     /**
270      * Migrates the DB if needed. If the migration failed, it clears the DB.
271      */
tryMigrateDB(@ullable LauncherRestoreEventLogger restoreEventLogger)272     public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
273 
274         if (!migrateGridIfNeeded()) {
275             if (restoreEventLogger != null) {
276                 sendMetricsForFailedMigration(restoreEventLogger, getDb());
277             }
278             FileLog.d(TAG, "Migration failed: resetting launcher database");
279             createEmptyDB();
280             LauncherPrefs.get(mContext).putSync(
281                     getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
282 
283             // Write the grid state to avoid another migration
284             new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
285         }
286     }
287 
288     /**
289      * Migrates the DB if needed, and returns false if the migration failed
290      * and DB needs to be cleared.
291      * @return true if migration was success or ignored, false if migration failed
292      * and the DB should be reset.
293      */
migrateGridIfNeeded()294     private boolean migrateGridIfNeeded() {
295         createDbIfNotExists();
296         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
297             // If we have already create a new DB, ignore migration
298             Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
299             return false;
300         }
301         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
302         if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
303             Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
304             return true;
305         }
306         String targetDbName = new DeviceGridState(idp).getDbFile();
307         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
308             Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
309             return false;
310         }
311         DatabaseHelper oldHelper = mOpenHelper;
312         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
313                 : createDatabaseHelper(true /* forMigration */);
314         try {
315             // This is the current grid we have, given by the mContext
316             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
317             // This is the state we want to migrate to that is given by the idp
318             DeviceGridState destDeviceState = new DeviceGridState(idp);
319             return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, srcDeviceState,
320                     destDeviceState, mOpenHelper, oldHelper.getWritableDatabase());
321         } catch (Exception e) {
322             FileLog.e(TAG, "Failed to migrate grid", e);
323             return false;
324         } finally {
325             if (mOpenHelper != oldHelper) {
326                 oldHelper.close();
327             }
328         }
329     }
330 
331     /**
332      * In case of migration failure, report metrics for the count of each itemType in the DB.
333      * @param restoreEventLogger logger used to report Launcher restore metrics
334      */
sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger, SQLiteDatabase db)335     private void sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger,
336             SQLiteDatabase db) {
337         try (Cursor cursor = db.rawQuery(
338                 "SELECT itemType, COUNT(*) AS count FROM favorites GROUP BY itemType",
339                 null
340         )) {
341             if (cursor.moveToFirst()) {
342                 do {
343                     restoreEventLogger.logFavoritesItemsRestoreFailed(
344                             cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)),
345                             cursor.getInt(cursor.getColumnIndexOrThrow("count")),
346                             RestoreError.GRID_MIGRATION_FAILURE
347                     );
348                 } while (cursor.moveToNext());
349             }
350         } catch (Exception e) {
351             FileLog.e(TAG, "sendMetricsForFailedDb: Error reading from database", e);
352         }
353     }
354 
355     /**
356      * Returns the underlying model database
357      */
getDb()358     public SQLiteDatabase getDb() {
359         createDbIfNotExists();
360         return mOpenHelper.getWritableDatabase();
361     }
362 
onAddOrDeleteOp(SQLiteDatabase db)363     private void onAddOrDeleteOp(SQLiteDatabase db) {
364         mOpenHelper.onAddOrDeleteOp(db);
365     }
366 
367     /**
368      * Deletes any empty folder from the DB.
369      * @return Ids of deleted folders.
370      */
371     @WorkerThread
deleteEmptyFolders()372     public IntArray deleteEmptyFolders() {
373         createDbIfNotExists();
374 
375         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
376         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
377             // Select folders whose id do not match any container value.
378             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
379                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
380                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT "
381                     + LauncherSettings.Favorites.CONTAINER + " FROM "
382                     + Favorites.TABLE_NAME + ")";
383 
384             IntArray folderIds = LauncherDbUtils.queryIntArray(false, db, Favorites.TABLE_NAME,
385                     Favorites._ID, selection, null, null);
386             if (!folderIds.isEmpty()) {
387                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
388                         LauncherSettings.Favorites._ID, folderIds), null);
389             }
390             t.commit();
391             return folderIds;
392         } catch (SQLException ex) {
393             Log.e(TAG, ex.getMessage(), ex);
394             return new IntArray();
395         }
396     }
397 
398     /**
399      * Deletes any app pair that doesn't contain 2 member apps from the DB.
400      * @return Ids of deleted app pairs.
401      */
402     @WorkerThread
deleteBadAppPairs()403     public IntArray deleteBadAppPairs() {
404         createDbIfNotExists();
405 
406         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
407         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
408             // Select all entries with ITEM_TYPE = ITEM_TYPE_APP_PAIR whose id does not appear
409             // exactly twice in the CONTAINER column.
410             String selection =
411                     ITEM_TYPE + " = " + ITEM_TYPE_APP_PAIR
412                             + " AND " + _ID +  " NOT IN"
413                             + " (SELECT " + CONTAINER + " FROM " + TABLE_NAME
414                             + " GROUP BY " + CONTAINER + " HAVING COUNT(*) = 2)";
415 
416             IntArray appPairIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
417                     _ID, selection, null, null);
418             if (!appPairIds.isEmpty()) {
419                 db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
420                         _ID, appPairIds), null);
421             }
422             t.commit();
423             return appPairIds;
424         } catch (SQLException ex) {
425             Log.e(TAG, ex.getMessage(), ex);
426             return new IntArray();
427         }
428     }
429 
430     /**
431      * Deletes any app with a container id that doesn't exist.
432      * @return Ids of deleted apps.
433      */
434     @WorkerThread
deleteUnparentedApps()435     public IntArray deleteUnparentedApps() {
436         createDbIfNotExists();
437 
438         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
439         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
440             // Select all entries whose container id does not appear in the database.
441             String selection =
442                     CONTAINER + " >= 0"
443                             + " AND " + CONTAINER + " NOT IN"
444                             + " (SELECT " + _ID + " FROM " + TABLE_NAME + ")";
445 
446             IntArray appIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
447                     _ID, selection, null, null);
448             if (!appIds.isEmpty()) {
449                 db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
450                         _ID, appIds), null);
451             }
452             t.commit();
453             return appIds;
454         } catch (SQLException ex) {
455             Log.e(TAG, ex.getMessage(), ex);
456             return new IntArray();
457         }
458     }
459 
addModifiedTime(ContentValues values)460     private static void addModifiedTime(ContentValues values) {
461         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
462     }
463 
clearFlagEmptyDbCreated()464     private void clearFlagEmptyDbCreated() {
465         LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey());
466     }
467 
468     /**
469      * Loads the default workspace based on the following priority scheme:
470      *   1) From the app restrictions
471      *   2) From a package provided by play store
472      *   3) From a partner configuration APK, already in the system image
473      *   4) The default configuration for the particular device
474      */
475     @WorkerThread
loadDefaultFavoritesIfNecessary()476     public synchronized void loadDefaultFavoritesIfNecessary() {
477         createDbIfNotExists();
478 
479         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
480             Log.d(TAG, "loading default workspace");
481 
482             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
483             try {
484                 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
485                 if (loader == null) {
486                     loader = AutoInstallsLayout.get(mContext, widgetHolder, mOpenHelper);
487                 }
488                 if (loader == null) {
489                     final Partner partner = Partner.get(mContext.getPackageManager());
490                     if (partner != null) {
491                         int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT);
492                         if (workspaceResId != 0) {
493                             loader = new DefaultLayoutParser(mContext, widgetHolder,
494                                     mOpenHelper, partner.getResources(), workspaceResId);
495                         }
496                     }
497                 }
498 
499                 final boolean usingExternallyProvidedLayout = loader != null;
500                 if (loader == null) {
501                     loader = getDefaultLayoutParser(widgetHolder);
502                 }
503 
504                 // There might be some partially restored DB items, due to buggy restore logic in
505                 // previous versions of launcher.
506                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
507                 // Populate favorites table with initial favorites
508                 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
509                         && usingExternallyProvidedLayout) {
510                     // Unable to load external layout. Cleanup and load the internal layout.
511                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
512                     mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
513                             getDefaultLayoutParser(widgetHolder));
514                 }
515                 clearFlagEmptyDbCreated();
516             } finally {
517                 widgetHolder.destroy();
518             }
519         }
520     }
521 
522     /**
523      * Creates workspace loader from an XML resource listed in the app restrictions.
524      *
525      * @return the loader if the restrictions are set and the resource exists; null otherwise.
526      */
createWorkspaceLoaderFromAppRestriction( LauncherWidgetHolder widgetHolder)527     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
528             LauncherWidgetHolder widgetHolder) {
529         ContentResolver cr = mContext.getContentResolver();
530         String blobHandlerDigest = Settings.Secure.getString(cr, LAYOUT_DIGEST_KEY);
531         if (!TextUtils.isEmpty(blobHandlerDigest)) {
532             BlobStoreManager blobManager = mContext.getSystemService(BlobStoreManager.class);
533             try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
534                     blobManager.openBlob(BlobHandle.createWithSha256(
535                             Base64.decode(blobHandlerDigest, NO_WRAP | NO_PADDING),
536                             LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)))) {
537                 return getAutoInstallsLayoutFromIS(in, widgetHolder, new SourceResources() { });
538             } catch (Exception e) {
539                 Log.e(TAG, "Error getting layout from blob handle" , e);
540                 return null;
541             }
542         }
543 
544         String authority = Settings.Secure.getString(cr, "launcher3.layout.provider");
545         if (TextUtils.isEmpty(authority)) {
546             return null;
547         }
548 
549         PackageManager pm = mContext.getPackageManager();
550         ProviderInfo pi = pm.resolveContentProvider(authority, 0);
551         if (pi == null) {
552             Log.e(TAG, "No provider found for authority " + authority);
553             return null;
554         }
555         Uri uri = getLayoutUri(authority, mContext);
556         try (InputStream in = cr.openInputStream(uri)) {
557             Log.d(TAG, "Loading layout from " + authority);
558 
559             Resources res = pm.getResourcesForApplication(pi.applicationInfo);
560             return getAutoInstallsLayoutFromIS(in, widgetHolder, SourceResources.wrap(res));
561         } catch (Exception e) {
562             Log.e(TAG, "Error getting layout stream from: " + authority , e);
563             return null;
564         }
565     }
566 
getAutoInstallsLayoutFromIS(InputStream in, LauncherWidgetHolder widgetHolder, SourceResources res)567     private AutoInstallsLayout getAutoInstallsLayoutFromIS(InputStream in,
568             LauncherWidgetHolder widgetHolder, SourceResources res) throws Exception {
569         // Read the full xml so that we fail early in case of any IO error.
570         String layout = new String(IOUtils.toByteArray(in));
571         XmlPullParser parser = Xml.newPullParser();
572         parser.setInput(new StringReader(layout));
573 
574         return new AutoInstallsLayout(mContext, widgetHolder, mOpenHelper, res,
575                 () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
576     }
577 
getLayoutUri(String authority, Context ctx)578     public static Uri getLayoutUri(String authority, Context ctx) {
579         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
580         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
581                 .appendQueryParameter("version", "1")
582                 .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
583                 .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
584                 .appendQueryParameter("hotseatSize", Integer.toString(grid.numDatabaseHotseatIcons))
585                 .build();
586     }
587 
getDefaultLayoutParser(LauncherWidgetHolder widgetHolder)588     private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
589         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
590         int defaultLayout = idp.demoModeLayoutId != 0
591                 && mContext.getSystemService(UserManager.class).isDemoUser()
592                 ? idp.demoModeLayoutId : idp.defaultLayoutId;
593 
594         return new DefaultLayoutParser(mContext, widgetHolder,
595                 mOpenHelper, mContext.getResources(), defaultLayout);
596     }
597 
getEmptyDbCreatedKey()598     private ConstantItem<Boolean> getEmptyDbCreatedKey() {
599         return getEmptyDbCreatedKey(mOpenHelper.getDatabaseName());
600     }
601 
602     /**
603      * Re-composite given key in respect to database. If the current db is
604      * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
605      * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
606      * string will be "EMPTY_DATABASE_CREATED@minimal.db".
607      */
getEmptyDbCreatedKey(String dbName)608     private ConstantItem<Boolean> getEmptyDbCreatedKey(String dbName) {
609         if (mContext instanceof SandboxContext) {
610             return LauncherPrefs.nonRestorableItem(EMPTY_DATABASE_CREATED,
611                     false /* default value */, EncryptionType.ENCRYPTED);
612         }
613         String key = TextUtils.equals(dbName, LauncherFiles.LAUNCHER_DB)
614                 ? EMPTY_DATABASE_CREATED : EMPTY_DATABASE_CREATED + "@" + dbName;
615         return LauncherPrefs.backedUpItem(key, false /* default value */, EncryptionType.ENCRYPTED);
616     }
617 
618     /**
619      * Returns the serial number for the provided user
620      */
getSerialNumberForUser(UserHandle user)621     public long getSerialNumberForUser(UserHandle user) {
622         return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
623     }
624 }
625