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