• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
20 import static com.android.launcher3.LauncherPrefs.DB_FILE;
21 import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
23 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
24 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
25 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
26 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
27 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
28 
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.database.Cursor;
32 import android.database.SQLException;
33 import android.database.sqlite.SQLiteDatabase;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.Process;
37 import android.os.UserHandle;
38 import android.os.UserManager;
39 import android.text.TextUtils;
40 import android.util.Log;
41 
42 import androidx.annotation.Nullable;
43 import androidx.annotation.WorkerThread;
44 
45 import com.android.launcher3.AutoInstallsLayout;
46 import com.android.launcher3.ConstantItem;
47 import com.android.launcher3.DefaultLayoutParser;
48 import com.android.launcher3.EncryptionType;
49 import com.android.launcher3.InvariantDeviceProfile;
50 import com.android.launcher3.LauncherAppState;
51 import com.android.launcher3.LauncherFiles;
52 import com.android.launcher3.LauncherPrefs;
53 import com.android.launcher3.LauncherSettings;
54 import com.android.launcher3.LauncherSettings.Favorites;
55 import com.android.launcher3.Utilities;
56 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
57 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
58 import com.android.launcher3.dagger.ApplicationContext;
59 import com.android.launcher3.dagger.LauncherAppSingleton;
60 import com.android.launcher3.logging.FileLog;
61 import com.android.launcher3.pm.UserCache;
62 import com.android.launcher3.provider.LauncherDbUtils;
63 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
64 import com.android.launcher3.provider.RestoreDbTask;
65 import com.android.launcher3.util.IntArray;
66 import com.android.launcher3.widget.LauncherWidgetHolder;
67 
68 import java.io.File;
69 import java.util.List;
70 import java.util.stream.Collectors;
71 
72 import javax.inject.Inject;
73 
74 /**
75  * Utility class which maintains an instance of Launcher database and provides utility methods
76  * around it.
77  */
78 @LauncherAppSingleton
79 public class ModelDbController {
80     private static final String TAG = "ModelDbController";
81 
82     private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
83     public static final String EXTRA_DB_NAME = "db_name";
84     public static final String DATA_TYPE_DB_FILE = "database_file";
85 
86     protected DatabaseHelper mOpenHelper;
87 
88     private final Context mContext;
89     private final InvariantDeviceProfile mIdp;
90     private final LauncherPrefs mPrefs;
91     private final UserCache mUserCache;
92     private final LayoutParserFactory mLayoutParserFactory;
93 
94     @Inject
ModelDbController( @pplicationContext Context context, InvariantDeviceProfile idp, LauncherPrefs prefs, UserCache userCache, LayoutParserFactory layoutParserFactory)95     ModelDbController(
96             @ApplicationContext Context context,
97             InvariantDeviceProfile idp,
98             LauncherPrefs prefs,
99             UserCache userCache,
100             LayoutParserFactory layoutParserFactory) {
101         mContext = context;
102         mIdp = idp;
103         mPrefs = prefs;
104         mUserCache = userCache;
105         mLayoutParserFactory = layoutParserFactory;
106     }
107 
printDBs(String prefix)108     private void printDBs(String prefix) {
109         try {
110             File directory = new File(mContext.getDatabasePath(mIdp.dbFile).getParent());
111             if (directory.exists()) {
112                 for (File file : directory.listFiles()) {
113                     Log.d("b/353505773", prefix + "Database file: " + file.getName());
114                 }
115             } else {
116                 Log.d("b/353505773", prefix + "No files found in the database directory");
117             }
118         } catch (Exception e) {
119             Log.e("b/353505773", prefix + e.getMessage());
120         }
121     }
122 
createDbIfNotExists()123     private synchronized void createDbIfNotExists() {
124         if (mOpenHelper == null) {
125             String dbFile = mPrefs.get(DB_FILE);
126             if (dbFile.isEmpty()) {
127                 dbFile = mIdp.dbFile;
128             }
129             mOpenHelper = createDatabaseHelper(false /* forMigration */, dbFile);
130             printDBs("before: ");
131             RestoreDbTask.restoreIfNeeded(mContext, this);
132             printDBs("after: ");
133         }
134     }
135 
createDatabaseHelper(boolean forMigration, String dbFile)136     protected DatabaseHelper createDatabaseHelper(boolean forMigration, String dbFile) {
137         // Set the flag for empty DB
138         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
139                 : () -> mPrefs.putSync(getEmptyDbCreatedKey(dbFile).to(true));
140 
141         DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbFile,
142                 this::getSerialNumberForUser, onEmptyDbCreateCallback);
143         // Table creation sometimes fails silently, which leads to a crash loop.
144         // This way, we will try to create a table every time after crash, so the device
145         // would eventually be able to recover.
146         if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
147             Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
148             // This operation is a no-op if the table already exists.
149             addTableToDb(databaseHelper.getWritableDatabase(),
150                     getSerialNumberForUser(Process.myUserHandle()),
151                     true /* optional */);
152         }
153         databaseHelper.mHotseatRestoreTableExists = tableExists(
154                 databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
155 
156         databaseHelper.initIds();
157         return databaseHelper;
158     }
159 
160     /**
161      * Refer {@link SQLiteDatabase#query}
162      */
163     @WorkerThread
query(String[] projection, String selection, String[] selectionArgs, String sortOrder)164     public Cursor query(String[] projection, String selection,
165             String[] selectionArgs, String sortOrder) {
166         createDbIfNotExists();
167         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
168         Cursor result = db.query(
169                 TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
170 
171         final Bundle extra = new Bundle();
172         extra.putString(EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
173         result.setExtras(extra);
174         return result;
175     }
176 
177     /**
178      * Refer {@link SQLiteDatabase#insert(String, String, ContentValues)}
179      */
180     @WorkerThread
insert(ContentValues initialValues)181     public int insert(ContentValues initialValues) {
182         createDbIfNotExists();
183 
184         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
185         addModifiedTime(initialValues);
186         int rowId = mOpenHelper.dbInsertAndCheck(db, TABLE_NAME, initialValues);
187         if (rowId >= 0) {
188             onAddOrDeleteOp(db);
189         }
190         return rowId;
191     }
192 
193     /**
194      * Refer {@link SQLiteDatabase#delete(String, String, String[])}
195      */
196     @WorkerThread
delete(String selection, String[] selectionArgs)197     public int delete(String selection, String[] selectionArgs) {
198         createDbIfNotExists();
199         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
200 
201         int count = db.delete(TABLE_NAME, selection, selectionArgs);
202         if (count > 0) {
203             onAddOrDeleteOp(db);
204         }
205         return count;
206     }
207 
208     /**
209      * Refer {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
210      */
211     @WorkerThread
update(ContentValues values, String selection, String[] selectionArgs)212     public int update(ContentValues values, String selection, String[] selectionArgs) {
213         createDbIfNotExists();
214 
215         addModifiedTime(values);
216         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
217         return db.update(TABLE_NAME, values, selection, selectionArgs);
218     }
219 
220     /**
221      * Clears a previously set flag corresponding to empty db creation
222      */
223     @WorkerThread
clearEmptyDbFlag()224     public void clearEmptyDbFlag() {
225         createDbIfNotExists();
226         clearFlagEmptyDbCreated();
227     }
228 
229     /**
230      * Generates an id to be used for new item in the favorites table
231      */
232     @WorkerThread
generateNewItemId()233     public int generateNewItemId() {
234         createDbIfNotExists();
235         return mOpenHelper.generateNewItemId();
236     }
237 
238     /**
239      * Generates an id to be used for new workspace screen
240      */
241     @WorkerThread
getNewScreenId()242     public int getNewScreenId() {
243         createDbIfNotExists();
244         return mOpenHelper.getNewScreenId();
245     }
246 
247     /**
248      * Creates an empty DB clearing all existing data
249      */
250     @WorkerThread
createEmptyDB()251     public void createEmptyDB() {
252         createDbIfNotExists();
253         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
254         mPrefs.putSync(getEmptyDbCreatedKey().to(true));
255     }
256 
257     /**
258      * Removes any widget which are present in the framework, but not in out internal DB
259      */
260     @WorkerThread
removeGhostWidgets()261     public void removeGhostWidgets() {
262         createDbIfNotExists();
263         mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
264     }
265 
266     /**
267      * Returns a new {@link SQLiteTransaction}
268      */
269     @WorkerThread
newTransaction()270     public SQLiteTransaction newTransaction() {
271         createDbIfNotExists();
272         return new SQLiteTransaction(mOpenHelper.getWritableDatabase());
273     }
274 
275     /**
276      * Refreshes the internal state corresponding to presence of hotseat table
277      */
278     @WorkerThread
refreshHotseatRestoreTable()279     public void refreshHotseatRestoreTable() {
280         createDbIfNotExists();
281         mOpenHelper.mHotseatRestoreTableExists = tableExists(
282                 mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
283     }
284 
285     /**
286      * Resets the launcher DB if we should reset it.
287      */
resetLauncherDb(@ullable LauncherRestoreEventLogger restoreEventLogger)288     public void resetLauncherDb(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
289         if (restoreEventLogger != null) {
290             sendMetricsForFailedMigration(restoreEventLogger, getDb());
291         }
292         FileLog.d(TAG, "resetLauncherDb: Migration failed: resetting launcher database");
293         createEmptyDB();
294         mPrefs.putSync(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
295 
296         // Write the grid state to avoid another migration
297         new DeviceGridState(mIdp).writeToPrefs(mContext);
298     }
299 
300     /**
301      * Determines if we should reset the DB.
302      */
shouldResetDb()303     private boolean shouldResetDb() {
304         if (isThereExistingDb()) {
305             return true;
306         }
307         if (!isGridMigrationNecessary()) {
308             return false;
309         }
310         if (isCurrentDbSameAsTarget()) {
311             return true;
312         }
313         return false;
314     }
315 
isThereExistingDb()316     private boolean isThereExistingDb() {
317         if (mPrefs.get(getEmptyDbCreatedKey())) {
318             // If we already have a new DB, ignore migration
319             FileLog.d(TAG, "isThereExistingDb: new DB already created, skipping migration");
320             return true;
321         }
322         return false;
323     }
324 
isGridMigrationNecessary()325     private boolean isGridMigrationNecessary() {
326         if (GridSizeMigrationDBController.needsToMigrate(mContext, mIdp)) {
327             return true;
328         }
329         FileLog.d(TAG, "isGridMigrationNecessary: no grid migration needed");
330         return false;
331     }
332 
isCurrentDbSameAsTarget()333     private boolean isCurrentDbSameAsTarget() {
334         String targetDbName = new DeviceGridState(mIdp).getDbFile();
335         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
336             FileLog.e(TAG, "isCurrentDbSameAsTarget: target db is same as current"
337                     + " current db: " + mOpenHelper.getDatabaseName()
338                     + " target db: " + targetDbName);
339             return true;
340         }
341         return false;
342     }
343 
344     /**
345      * Migrates the DB. If the migration failed, it clears the DB.
346      */
attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger, ModelDelegate modelDelegate)347     public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger,
348             ModelDelegate modelDelegate) throws Exception {
349         createDbIfNotExists();
350         if (shouldResetDb()) {
351             resetLauncherDb(restoreEventLogger);
352             return;
353         }
354 
355         DatabaseHelper oldHelper = mOpenHelper;
356 
357         // We save the existing db's before creating the destination db helper so we know what logic
358         // to run in grid migration based on if that grid already existed before migration or not.
359         List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
360                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
361                 .collect(Collectors.toList());
362 
363         mOpenHelper = createDatabaseHelper(true, new DeviceGridState(mIdp).getDbFile());
364         try {
365             // This is the current grid we have, given by the mContext
366             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
367             // This is the state we want to migrate to that is given by the idp
368             DeviceGridState destDeviceState = new DeviceGridState(mIdp);
369 
370             boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
371             GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic();
372             gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState,
373                     mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb, modelDelegate);
374         } catch (Exception e) {
375             resetLauncherDb(restoreEventLogger);
376             throw new Exception("attemptMigrateDb: Failed to migrate grid", e);
377         } finally {
378             if (mOpenHelper != oldHelper) {
379                 oldHelper.close();
380             }
381         }
382     }
383 
384     /**
385      * Migrates the DB if needed. If the migration failed, it clears the DB.
386      */
tryMigrateDB(@ullable LauncherRestoreEventLogger restoreEventLogger, ModelDelegate modelDelegate)387     public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger,
388             ModelDelegate modelDelegate) {
389         if (!migrateGridIfNeeded(modelDelegate)) {
390             if (restoreEventLogger != null) {
391                 if (mPrefs.get(NO_DB_FILES_RESTORED)) {
392                     restoreEventLogger.logLauncherItemsRestoreFailed(DATA_TYPE_DB_FILE, 1,
393                             RestoreError.DATABASE_FILE_NOT_RESTORED);
394                     mPrefs.put(NO_DB_FILES_RESTORED, false);
395                     FileLog.d(TAG, "There is no data to migrate: resetting launcher database");
396                 } else {
397                     restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
398                     sendMetricsForFailedMigration(restoreEventLogger, getDb());
399                 }
400             }
401             FileLog.d(TAG, "tryMigrateDB: Migration failed: resetting launcher database");
402             createEmptyDB();
403             mPrefs.putSync(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
404 
405             // Write the grid state to avoid another migration
406             new DeviceGridState(mIdp).writeToPrefs(mContext);
407         } else if (restoreEventLogger != null) {
408             restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
409         }
410     }
411 
412     /**
413      * Migrates the DB if needed, and returns false if the migration failed
414      * and DB needs to be cleared.
415      * @return true if migration was success or ignored, false if migration failed
416      * and the DB should be reset.
417      */
migrateGridIfNeeded(ModelDelegate modelDelegate)418     private boolean migrateGridIfNeeded(ModelDelegate modelDelegate) {
419         createDbIfNotExists();
420         if (mPrefs.get(getEmptyDbCreatedKey())) {
421             // If we have already create a new DB, ignore migration
422             FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
423             return false;
424         }
425         if (!GridSizeMigrationDBController.needsToMigrate(mContext, mIdp)) {
426             FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
427             return true;
428         }
429         String targetDbName = new DeviceGridState(mIdp).getDbFile();
430         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
431             FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current"
432                     + " current db: " + mOpenHelper.getDatabaseName()
433                     + " target db: " + targetDbName);
434             return false;
435         }
436         DatabaseHelper oldHelper = mOpenHelper;
437         // We save the existing db's before creating the destination db helper so we know what logic
438         // to run in grid migration based on if that grid already existed before migration or not.
439         List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
440                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
441                 .collect(Collectors.toList());
442         mOpenHelper = createDatabaseHelper(true /* forMigration */, targetDbName);
443         try {
444             // This is the current grid we have, given by the mContext
445             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
446             // This is the state we want to migrate to that is given by the idp
447             DeviceGridState destDeviceState = new DeviceGridState(mIdp);
448             boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
449             return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState,
450                     destDeviceState, mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb,
451                     modelDelegate);
452         } catch (Exception e) {
453             FileLog.e(TAG, "migrateGridIfNeeded: Failed to migrate grid", e);
454             return false;
455         } finally {
456             if (mOpenHelper != oldHelper) {
457                 oldHelper.close();
458             }
459         }
460     }
461 
462     /**
463      * In case of migration failure, report metrics for the count of each itemType in the DB.
464      * @param restoreEventLogger logger used to report Launcher restore metrics
465      */
sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger, SQLiteDatabase db)466     private void sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger,
467             SQLiteDatabase db) {
468         try (Cursor cursor = db.rawQuery(
469                 "SELECT itemType, COUNT(*) AS count FROM favorites GROUP BY itemType",
470                 null
471         )) {
472             if (cursor.moveToFirst()) {
473                 do {
474                     restoreEventLogger.logFavoritesItemsRestoreFailed(
475                             cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)),
476                             cursor.getInt(cursor.getColumnIndexOrThrow("count")),
477                             RestoreError.GRID_MIGRATION_FAILURE
478                     );
479                 } while (cursor.moveToNext());
480             }
481         } catch (Exception e) {
482             FileLog.e(TAG, "sendMetricsForFailedDb: Error reading from database", e);
483         }
484     }
485 
486     /**
487      * Returns the underlying model database
488      */
getDb()489     public SQLiteDatabase getDb() {
490         createDbIfNotExists();
491         return mOpenHelper.getWritableDatabase();
492     }
493 
onAddOrDeleteOp(SQLiteDatabase db)494     private void onAddOrDeleteOp(SQLiteDatabase db) {
495         mOpenHelper.onAddOrDeleteOp(db);
496     }
497 
498     /**
499      * Deletes any empty folder from the DB.
500      * @return Ids of deleted folders.
501      */
502     @WorkerThread
deleteEmptyFolders()503     public IntArray deleteEmptyFolders() {
504         createDbIfNotExists();
505 
506         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
507         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
508             // Select folders whose id do not match any container value.
509             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
510                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
511                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT "
512                     + LauncherSettings.Favorites.CONTAINER + " FROM "
513                     + Favorites.TABLE_NAME + ")";
514 
515             IntArray folderIds = LauncherDbUtils.queryIntArray(false, db, Favorites.TABLE_NAME,
516                     Favorites._ID, selection, null, null);
517             if (!folderIds.isEmpty()) {
518                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
519                         LauncherSettings.Favorites._ID, folderIds), null);
520             }
521             t.commit();
522             return folderIds;
523         } catch (SQLException ex) {
524             Log.e(TAG, ex.getMessage(), ex);
525             return new IntArray();
526         }
527     }
528 
529     /**
530      * Deletes any app pair that doesn't contain 2 member apps from the DB.
531      * @return Ids of deleted app pairs.
532      */
533     @WorkerThread
deleteBadAppPairs()534     public IntArray deleteBadAppPairs() {
535         createDbIfNotExists();
536 
537         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
538         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
539             // Select all entries with ITEM_TYPE = ITEM_TYPE_APP_PAIR whose id does not appear
540             // exactly twice in the CONTAINER column.
541             String selection =
542                     ITEM_TYPE + " = " + ITEM_TYPE_APP_PAIR
543                             + " AND " + _ID +  " NOT IN"
544                             + " (SELECT " + CONTAINER + " FROM " + TABLE_NAME
545                             + " GROUP BY " + CONTAINER + " HAVING COUNT(*) = 2)";
546 
547             IntArray appPairIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
548                     _ID, selection, null, null);
549             if (!appPairIds.isEmpty()) {
550                 db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
551                         _ID, appPairIds), null);
552             }
553             t.commit();
554             return appPairIds;
555         } catch (SQLException ex) {
556             Log.e(TAG, ex.getMessage(), ex);
557             return new IntArray();
558         }
559     }
560 
561     /**
562      * Deletes any app with a container id that doesn't exist.
563      * @return Ids of deleted apps.
564      */
565     @WorkerThread
deleteUnparentedApps()566     public IntArray deleteUnparentedApps() {
567         createDbIfNotExists();
568 
569         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
570         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
571             // Select all entries whose container id does not appear in the database.
572             String selection =
573                     CONTAINER + " >= 0"
574                             + " AND " + CONTAINER + " NOT IN"
575                             + " (SELECT " + _ID + " FROM " + TABLE_NAME + ")";
576 
577             IntArray appIds = LauncherDbUtils.queryIntArray(false, db, TABLE_NAME,
578                     _ID, selection, null, null);
579             if (!appIds.isEmpty()) {
580                 db.delete(TABLE_NAME, Utilities.createDbSelectionQuery(
581                         _ID, appIds), null);
582             }
583             t.commit();
584             return appIds;
585         } catch (SQLException ex) {
586             Log.e(TAG, ex.getMessage(), ex);
587             return new IntArray();
588         }
589     }
590 
addModifiedTime(ContentValues values)591     private static void addModifiedTime(ContentValues values) {
592         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
593     }
594 
clearFlagEmptyDbCreated()595     private void clearFlagEmptyDbCreated() {
596         mPrefs.removeSync(getEmptyDbCreatedKey());
597     }
598 
599     /**
600      * Loads the default workspace based on the following priority scheme:
601      *   1) From the app restrictions
602      *   2) From a package provided by play store
603      *   3) From a partner configuration APK, already in the system image
604      *   4) The default configuration for the particular device
605      */
606     @WorkerThread
loadDefaultFavoritesIfNecessary()607     public synchronized void loadDefaultFavoritesIfNecessary() {
608         createDbIfNotExists();
609 
610         if (mPrefs.get(getEmptyDbCreatedKey())) {
611             Log.d(TAG, "loading default workspace");
612 
613             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
614             try {
615                 AutoInstallsLayout loader =
616                         mLayoutParserFactory.createExternalLayoutParser(widgetHolder, mOpenHelper);
617 
618                 final boolean usingExternallyProvidedLayout = loader != null;
619                 if (loader == null) {
620                     loader = getDefaultLayoutParser(widgetHolder);
621                 }
622 
623                 // There might be some partially restored DB items, due to buggy restore logic in
624                 // previous versions of launcher.
625                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
626                 // Populate favorites table with initial favorites
627                 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
628                         && usingExternallyProvidedLayout) {
629                     // Unable to load external layout. Cleanup and load the internal layout.
630                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
631                     mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
632                             getDefaultLayoutParser(widgetHolder));
633                 }
634                 clearFlagEmptyDbCreated();
635             } finally {
636                 widgetHolder.destroy();
637             }
638         }
639     }
640 
getLayoutUri(String authority, Context ctx)641     public static Uri getLayoutUri(String authority, Context ctx) {
642         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
643         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
644                 .appendQueryParameter("version", "1")
645                 .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
646                 .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
647                 .appendQueryParameter("hotseatSize", Integer.toString(grid.numDatabaseHotseatIcons))
648                 .build();
649     }
650 
getDefaultLayoutParser(LauncherWidgetHolder widgetHolder)651     private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
652         int defaultLayout = mIdp.demoModeLayoutId != 0
653                 && mContext.getSystemService(UserManager.class).isDemoUser()
654                 ? mIdp.demoModeLayoutId : mIdp.defaultLayoutId;
655 
656         return new DefaultLayoutParser(mContext, widgetHolder,
657                 mOpenHelper, mContext.getResources(), defaultLayout);
658     }
659 
getEmptyDbCreatedKey()660     private ConstantItem<Boolean> getEmptyDbCreatedKey() {
661         return getEmptyDbCreatedKey(mOpenHelper.getDatabaseName());
662     }
663 
664     /**
665      * Re-composite given key in respect to database. If the current db is
666      * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
667      * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
668      * string will be "EMPTY_DATABASE_CREATED@minimal.db".
669      */
getEmptyDbCreatedKey(String dbName)670     private ConstantItem<Boolean> getEmptyDbCreatedKey(String dbName) {
671         String key = TextUtils.equals(dbName, LauncherFiles.LAUNCHER_DB)
672                 ? EMPTY_DATABASE_CREATED : EMPTY_DATABASE_CREATED + "@" + dbName;
673         return LauncherPrefs.backedUpItem(key, false /* default value */, EncryptionType.ENCRYPTED);
674     }
675 
676     /**
677      * Returns the serial number for the provided user
678      */
getSerialNumberForUser(UserHandle user)679     public long getSerialNumberForUser(UserHandle user) {
680         return mUserCache.getSerialNumberForUser(user);
681     }
682 }
683