1 /* 2 * Copyright (C) 2016 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 17 package com.android.launcher3.provider; 18 19 import static android.os.Process.myUserHandle; 20 21 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; 22 import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed; 23 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; 24 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; 25 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; 26 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; 27 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; 28 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE; 29 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 30 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; 31 import static com.android.launcher3.provider.LauncherDbUtils.dropTable; 32 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; 33 34 import android.app.backup.BackupManager; 35 import android.appwidget.AppWidgetHost; 36 import android.appwidget.AppWidgetManager; 37 import android.appwidget.AppWidgetProviderInfo; 38 import android.content.ContentValues; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.pm.LauncherActivityInfo; 42 import android.database.Cursor; 43 import android.database.sqlite.SQLiteDatabase; 44 import android.os.UserHandle; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.util.LongSparseArray; 48 import android.util.SparseLongArray; 49 50 import androidx.annotation.NonNull; 51 import androidx.annotation.VisibleForTesting; 52 import androidx.annotation.WorkerThread; 53 54 import com.android.launcher3.Flags; 55 import com.android.launcher3.InvariantDeviceProfile; 56 import com.android.launcher3.LauncherAppState; 57 import com.android.launcher3.LauncherFiles; 58 import com.android.launcher3.LauncherPrefs; 59 import com.android.launcher3.LauncherSettings; 60 import com.android.launcher3.LauncherSettings.Favorites; 61 import com.android.launcher3.Utilities; 62 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; 63 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError; 64 import com.android.launcher3.logging.FileLog; 65 import com.android.launcher3.model.DeviceGridState; 66 import com.android.launcher3.model.LoaderTask; 67 import com.android.launcher3.model.ModelDbController; 68 import com.android.launcher3.model.data.AppInfo; 69 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 70 import com.android.launcher3.model.data.WorkspaceItemInfo; 71 import com.android.launcher3.pm.UserCache; 72 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 73 import com.android.launcher3.util.ApiWrapper; 74 import com.android.launcher3.util.ContentWriter; 75 import com.android.launcher3.util.IntArray; 76 import com.android.launcher3.util.LogConfig; 77 78 import java.io.File; 79 import java.io.InvalidObjectException; 80 import java.util.Arrays; 81 import java.util.Collection; 82 import java.util.List; 83 import java.util.Map; 84 import java.util.stream.Collectors; 85 86 /** 87 * Utility class to update DB schema after it has been restored. 88 * 89 * This task is executed when Launcher starts for the first time and not immediately after restore. 90 * This helps keep the model consistent if the launcher updates between restore and first startup. 91 */ 92 public class RestoreDbTask { 93 94 private static final String TAG = "RestoreDbTask"; 95 public static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; 96 public static final String FIRST_LOAD_AFTER_RESTORE_KEY = "first_load_after_restore"; 97 98 private static final String INFO_COLUMN_NAME = "name"; 99 private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value"; 100 101 public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; 102 public static final String APPWIDGET_IDS = "appwidget_ids"; 103 @VisibleForTesting 104 public static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen", 105 "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider", 106 "appWidgetId", "restored"}; 107 108 /** 109 * Tries to restore the backup DB if needed 110 */ restoreIfNeeded(Context context, ModelDbController dbController)111 public static void restoreIfNeeded(Context context, ModelDbController dbController) { 112 if (!isPending(context)) { 113 Log.d(TAG, "No restore task pending, exiting RestoreDbTask"); 114 return; 115 } 116 if (!performRestore(context, dbController)) { 117 dbController.createEmptyDB(); 118 } 119 120 // Obtain InvariantDeviceProfile first before setting pending to false, so 121 // InvariantDeviceProfile won't switch to new grid when initializing. 122 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); 123 124 // Set is pending to false irrespective of the result, so that it doesn't get 125 // executed again. 126 LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); 127 128 if (Flags.enableNarrowGridRestore()) { 129 String oldPhoneFileName = idp.dbFile; 130 List<String> previousDbs = existingDbs(); 131 removeOldDBs(context, oldPhoneFileName); 132 // The idp before this contains data about the old phone, after this it becomes the idp 133 // of the current phone. 134 idp.reset(context); 135 trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName, previousDbs); 136 } else { 137 idp.reinitializeAfterRestore(context); 138 } 139 } 140 141 142 /** 143 * Try setting the gird used in the previous phone to the new one. If the current device doesn't 144 * support the previous grid option it will not be set. 145 */ trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp, String oldPhoneDbFileName, List<String> previousDbs)146 private static void trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp, 147 String oldPhoneDbFileName, List<String> previousDbs) { 148 InvariantDeviceProfile.GridOption oldPhoneGridOption = idp.getGridOptionFromFileName( 149 context, oldPhoneDbFileName); 150 // The grid option could be null if current phone doesn't support the previous db. 151 if (oldPhoneGridOption != null) { 152 /* If the user only used the default db on the previous phone and the new default db is 153 * bigger than or equal to the previous one, then keep the new default db */ 154 if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns 155 && oldPhoneGridOption.numRows <= idp.numRows) { 156 /* Keep the user in default grid */ 157 return; 158 } 159 /* 160 * Here we are setting the previous db as the current one. 161 */ 162 idp.setCurrentGrid(context, oldPhoneGridOption.name); 163 } 164 } 165 166 /** 167 * Returns a list of paths of the existing launcher dbs. 168 */ existingDbs()169 private static List<String> existingDbs() { 170 // At this point idp.dbFile contains the name of the dbFile from the previous phone 171 return LauncherFiles.GRID_DB_FILES.stream() 172 .filter(dbName -> new File(dbName).exists()) 173 .toList(); 174 } 175 176 /** 177 * Only keep the last database used on the previous device. 178 */ removeOldDBs(Context context, String oldPhoneDbFileName)179 private static void removeOldDBs(Context context, String oldPhoneDbFileName) { 180 // At this point idp.dbFile contains the name of the dbFile from the previous phone 181 LauncherFiles.GRID_DB_FILES.stream() 182 .filter(dbName -> !dbName.equals(oldPhoneDbFileName)) 183 .forEach(dbName -> { 184 if (context.getDatabasePath(dbName).delete()) { 185 FileLog.d(TAG, "Removed old grid db file: " + dbName); 186 } 187 }); 188 } 189 performRestore(Context context, ModelDbController controller)190 private static boolean performRestore(Context context, ModelDbController controller) { 191 SQLiteDatabase db = controller.getDb(); 192 FileLog.d(TAG, "performRestore: starting restore from db"); 193 try (SQLiteTransaction t = new SQLiteTransaction(db)) { 194 RestoreDbTask task = new RestoreDbTask(); 195 BackupManager backupManager = new BackupManager(context); 196 LauncherRestoreEventLogger restoreEventLogger = 197 LauncherRestoreEventLogger.Companion.newInstance(context); 198 task.sanitizeDB(context, controller, db, backupManager, restoreEventLogger); 199 task.restoreAppWidgetIdsIfExists(context, controller, restoreEventLogger); 200 t.commit(); 201 return true; 202 } catch (Exception e) { 203 FileLog.e(TAG, "Failed to verify db", e); 204 return false; 205 } 206 } 207 208 /** 209 * Makes the following changes in the provider DB. 210 * 1. Removes all entries belonging to any profiles that were not restored. 211 * 2. Marks all entries as restored. The flags are updated during first load or as 212 * the restored apps get installed. 213 * 3. If the user serial for any restored profile is different than that of the previous 214 * device, update the entries to the new profile id. 215 * 4. If restored from a single display backup, remove gaps between screenIds 216 * 5. Override shortcuts that need to be replaced. 217 * 218 * @return number of items deleted 219 */ 220 @VisibleForTesting sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger)221 protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, 222 BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger) 223 throws Exception { 224 logFavoritesTable(db, "Old Launcher Database before sanitizing:", null, null); 225 // Primary user ids 226 long myProfileId = controller.getSerialNumberForUser(myUserHandle()); 227 long oldProfileId = getDefaultProfileId(db); 228 FileLog.d(TAG, "sanitizeDB: myProfileId= " + myProfileId 229 + ", oldProfileId= " + oldProfileId); 230 LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId); 231 LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size() 232 + 1); 233 234 // Build mapping of restored profile ids to their new profile ids. 235 profileMapping.put(oldProfileId, myProfileId); 236 for (int i = oldManagedProfileIds.size() - 1; i >= 0; --i) { 237 long oldManagedProfileId = oldManagedProfileIds.keyAt(i); 238 UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId); 239 if (user != null) { 240 long newManagedProfileId = controller.getSerialNumberForUser(user); 241 profileMapping.put(oldManagedProfileId, newManagedProfileId); 242 FileLog.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId 243 + " should be mapped to new id=" + newManagedProfileId); 244 } else { 245 FileLog.e(TAG, "sanitizeDB: No User found for old profileId, Ancestral Serial " 246 + "Number: " + oldManagedProfileId); 247 } 248 } 249 250 // Delete all entries which do not belong to any restored profile(s). 251 int numProfiles = profileMapping.size(); 252 String[] profileIds = new String[numProfiles]; 253 profileIds[0] = Long.toString(oldProfileId); 254 for (int i = numProfiles - 1; i >= 1; --i) { 255 profileIds[i] = Long.toString(profileMapping.keyAt(i)); 256 } 257 258 final String[] args = new String[profileIds.length]; 259 Arrays.fill(args, "?"); 260 final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")"; 261 logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds); 262 if (enableLauncherBrMetricsFixed()) { 263 reportUnrestoredProfiles(db, where, profileIds, restoreEventLogger); 264 } 265 int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds); 266 FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted"); 267 268 // Mark all items as restored. 269 boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS); 270 ContentValues values = new ContentValues(); 271 values.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON 272 | (keepAllIcons ? WorkspaceItemInfo.FLAG_RESTORE_STARTED : 0)); 273 db.update(Favorites.TABLE_NAME, values, null, null); 274 275 // Mark widgets with appropriate restore flag. 276 values.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 277 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY 278 | LauncherAppWidgetInfo.FLAG_UI_NOT_READY 279 | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0)); 280 db.update(Favorites.TABLE_NAME, values, "itemType = ?", 281 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)}); 282 283 // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp 284 // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will 285 // be no overlap. 286 final long tempLocationOffset = Long.MIN_VALUE; 287 SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size()); 288 int numTempMigrations = 0; 289 for (int i = profileMapping.size() - 1; i >= 0; --i) { 290 long oldId = profileMapping.keyAt(i); 291 long newId = profileMapping.valueAt(i); 292 293 if (oldId != newId) { 294 if (profileMapping.indexOfKey(newId) >= 0) { 295 tempMigratedIds.put(numTempMigrations, newId); 296 numTempMigrations++; 297 newId = tempLocationOffset + newId; 298 } 299 migrateProfileId(db, oldId, newId); 300 } 301 } 302 303 // Migrate ids from their temporary id to their actual final id. 304 for (int i = tempMigratedIds.size() - 1; i >= 0; --i) { 305 long newId = tempMigratedIds.valueAt(i); 306 migrateProfileId(db, tempLocationOffset + newId, newId); 307 } 308 309 if (myProfileId != oldProfileId) { 310 changeDefaultColumn(db, myProfileId); 311 } 312 313 // If restored from a single display backup, remove gaps between screenIds 314 if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) { 315 removeScreenIdGaps(db); 316 } 317 318 // Override shortcuts 319 maybeOverrideShortcuts(context, controller, db, myProfileId); 320 return itemsDeletedCount; 321 } 322 323 /** 324 * Remove gaps between screenIds to make sure no empty pages are left in between. 325 * 326 * e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4] 327 */ removeScreenIdGaps(SQLiteDatabase db)328 protected void removeScreenIdGaps(SQLiteDatabase db) { 329 FileLog.d(TAG, "Removing gaps between screenIds"); 330 IntArray distinctScreens = LauncherDbUtils.queryIntArray(true, db, Favorites.TABLE_NAME, 331 Favorites.SCREEN, Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP, null, 332 Favorites.SCREEN); 333 if (distinctScreens.isEmpty()) { 334 return; 335 } 336 337 StringBuilder sql = new StringBuilder("UPDATE ").append(Favorites.TABLE_NAME) 338 .append(" SET ").append(Favorites.SCREEN).append(" =\nCASE\n"); 339 int screenId = distinctScreens.contains(0) ? 0 : 1; 340 for (int i = 0; i < distinctScreens.size(); i++) { 341 sql.append("WHEN ").append(Favorites.SCREEN).append(" == ") 342 .append(distinctScreens.get(i)).append(" THEN ").append(screenId++).append("\n"); 343 } 344 sql.append("ELSE screen\nEND WHERE ").append(Favorites.CONTAINER).append(" = ") 345 .append(Favorites.CONTAINER_DESKTOP).append(";"); 346 db.execSQL(sql.toString()); 347 } 348 349 /** 350 * Updates profile id of all entries from {@param oldProfileId} to {@param newProfileId}. 351 */ migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId)352 protected void migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId) { 353 FileLog.d(TAG, "Changing profile user id from " + oldProfileId + " to " + newProfileId); 354 // Update existing entries. 355 ContentValues values = new ContentValues(); 356 values.put(Favorites.PROFILE_ID, newProfileId); 357 db.update(Favorites.TABLE_NAME, values, "profileId = ?", 358 new String[]{Long.toString(oldProfileId)}); 359 } 360 361 362 /** 363 * Changes the default value for the column. 364 */ changeDefaultColumn(SQLiteDatabase db, long newProfileId)365 protected void changeDefaultColumn(SQLiteDatabase db, long newProfileId) { 366 db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;"); 367 Favorites.addTableToDb(db, newProfileId, false); 368 db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;"); 369 dropTable(db, "favorites_old"); 370 } 371 372 /** 373 * Returns a list of the managed profile id(s) used in the favorites table of the provided db. 374 */ getManagedProfileIds(SQLiteDatabase db, long defaultProfileId)375 private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) { 376 LongSparseArray<Long> ids = new LongSparseArray<>(); 377 try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? " 378 + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) { 379 while (c.moveToNext()) { 380 ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null); 381 } 382 } 383 return ids; 384 } 385 386 /** 387 * Returns a UserHandle of a restored managed profile with the given serial number, or null 388 * if none found. 389 */ getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber)390 private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager, 391 long ancestralSerialNumber) { 392 return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber); 393 } 394 395 /** 396 * Returns the profile id used in the favorites table of the provided db. 397 */ getDefaultProfileId(SQLiteDatabase db)398 protected long getDefaultProfileId(SQLiteDatabase db) throws Exception { 399 try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) { 400 int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME); 401 while (c.moveToNext()) { 402 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) { 403 return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE)); 404 } 405 } 406 throw new InvalidObjectException("Table does not have a profile id column"); 407 } 408 } 409 isPending(Context context)410 public static boolean isPending(Context context) { 411 return LauncherPrefs.get(context).has(RESTORE_DEVICE); 412 } 413 414 /** 415 * Marks the DB state as pending restoration 416 */ setPending(Context context)417 public static void setPending(Context context) { 418 DeviceGridState deviceGridState = new DeviceGridState(context); 419 FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState); 420 LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType())); 421 LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true)); 422 } 423 424 @WorkerThread 425 @VisibleForTesting restoreAppWidgetIdsIfExists(Context context, ModelDbController controller, LauncherRestoreEventLogger restoreEventLogger)426 void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller, 427 LauncherRestoreEventLogger restoreEventLogger) { 428 LauncherPrefs lp = LauncherPrefs.get(context); 429 if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { 430 AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID); 431 restoreAppWidgetIds(context, controller, restoreEventLogger, 432 IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), 433 IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), 434 host); 435 } else { 436 FileLog.d(TAG, "Did not receive new app widget id map during Launcher restore"); 437 } 438 439 lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS); 440 } 441 442 /** 443 * Updates the app widgets whose id has changed during the restore process. 444 */ 445 @WorkerThread restoreAppWidgetIds(Context context, ModelDbController controller, LauncherRestoreEventLogger launcherRestoreEventLogger, int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host)446 private void restoreAppWidgetIds(Context context, ModelDbController controller, 447 LauncherRestoreEventLogger launcherRestoreEventLogger, int[] oldWidgetIds, 448 int[] newWidgetIds, @NonNull AppWidgetHost host) { 449 if (!WIDGETS_ENABLED) { 450 FileLog.e(TAG, "Skipping widget ID remap as widgets not supported"); 451 host.deleteHost(); 452 launcherRestoreEventLogger.logFavoritesItemsRestoreFailed(Favorites.ITEM_TYPE_APPWIDGET, 453 oldWidgetIds.length, RestoreError.WIDGETS_DISABLED); 454 return; 455 } 456 if (!RestoreDbTask.isPending(context)) { 457 // Someone has already gone through our DB once, probably LoaderTask. Skip any further 458 // modifications of the DB. 459 FileLog.e(TAG, "Skipping widget ID remap as DB already in use"); 460 for (int widgetId : newWidgetIds) { 461 FileLog.d(TAG, "Deleting widgetId: " + widgetId); 462 host.deleteAppWidgetId(widgetId); 463 } 464 return; 465 } 466 467 final AppWidgetManager widgets = AppWidgetManager.getInstance(context); 468 469 FileLog.d(TAG, "restoreAppWidgetIds: " 470 + "oldWidgetIds=" + IntArray.wrap(oldWidgetIds).toConcatString() 471 + ", newWidgetIds=" + IntArray.wrap(newWidgetIds).toConcatString()); 472 473 // TODO(b/234700507): Remove the logs after the bug is fixed 474 logDatabaseWidgetInfo(controller); 475 476 for (int i = 0; i < oldWidgetIds.length; i++) { 477 FileLog.i(TAG, "migrating appWidgetId: " + oldWidgetIds[i] + " => " + newWidgetIds[i]); 478 479 final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]); 480 final int state; 481 if (LoaderTask.isValidProvider(provider)) { 482 // This will ensure that we show 'Click to setup' UI if required. 483 state = LauncherAppWidgetInfo.FLAG_UI_NOT_READY; 484 } else { 485 state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 486 } 487 488 // b/135926478: Work profile widget restore is broken in platform. This forces us to 489 // recreate the widget during loading with the correct host provider. 490 long mainProfileId = UserCache.INSTANCE.get(context) 491 .getSerialNumberForUser(myUserHandle()); 492 long controllerProfileId = controller.getSerialNumberForUser(myUserHandle()); 493 String oldWidgetId = Integer.toString(oldWidgetIds[i]); 494 final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?"; 495 String profileId = Long.toString(mainProfileId); 496 final String[] args = new String[] { oldWidgetId, profileId }; 497 FileLog.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId 498 + " with controller profile ID=" + controllerProfileId); 499 int result = new ContentWriter(context, 500 new ContentWriter.CommitParams(controller, where, args)) 501 .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i]) 502 .put(LauncherSettings.Favorites.RESTORED, state) 503 .commit(); 504 if (result == 0) { 505 // TODO(b/234700507): Remove the logs after the bug is fixed 506 FileLog.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in" 507 + " the database anymore"); 508 try (Cursor cursor = controller.getDb().query( 509 Favorites.TABLE_NAME, 510 new String[]{Favorites.APPWIDGET_ID}, 511 "appWidgetId=?", new String[]{oldWidgetId}, null, null, null)) { 512 if (!cursor.moveToFirst()) { 513 // The widget no long exists. 514 FileLog.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: " 515 + oldWidgetId); 516 host.deleteAppWidgetId(newWidgetIds[i]); 517 launcherRestoreEventLogger.logSingleFavoritesItemRestoreFailed( 518 ITEM_TYPE_APPWIDGET, 519 RestoreError.WIDGET_REMOVED 520 ); 521 } 522 } 523 } 524 } 525 526 logFavoritesTable(controller.getDb(), "launcher db after remap widget ids", null, null); 527 LauncherAppState.INSTANCE.executeIfCreated(app -> app.getModel().forceReload()); 528 } 529 logDatabaseWidgetInfo(ModelDbController controller)530 private static void logDatabaseWidgetInfo(ModelDbController controller) { 531 try (Cursor cursor = controller.getDb().query(Favorites.TABLE_NAME, 532 new String[]{Favorites.APPWIDGET_ID, Favorites.RESTORED, Favorites.PROFILE_ID}, 533 Favorites.APPWIDGET_ID + "!=" + LauncherAppWidgetInfo.NO_ID, null, 534 null, null, null)) { 535 IntArray widgetIdList = new IntArray(); 536 IntArray widgetRestoreList = new IntArray(); 537 IntArray widgetProfileIdList = new IntArray(); 538 539 if (cursor.moveToFirst()) { 540 final int widgetIdColumnIndex = cursor.getColumnIndex(Favorites.APPWIDGET_ID); 541 final int widgetRestoredColumnIndex = cursor.getColumnIndex(Favorites.RESTORED); 542 final int widgetProfileIdIndex = cursor.getColumnIndex(Favorites.PROFILE_ID); 543 while (!cursor.isAfterLast()) { 544 int widgetId = cursor.getInt(widgetIdColumnIndex); 545 int widgetRestoredFlag = cursor.getInt(widgetRestoredColumnIndex); 546 int widgetProfileId = cursor.getInt(widgetProfileIdIndex); 547 548 widgetIdList.add(widgetId); 549 widgetRestoreList.add(widgetRestoredFlag); 550 widgetProfileIdList.add(widgetProfileId); 551 cursor.moveToNext(); 552 } 553 } 554 555 StringBuilder builder = new StringBuilder(); 556 builder.append("["); 557 for (int i = 0; i < widgetIdList.size(); i++) { 558 builder.append("[appWidgetId=") 559 .append(widgetIdList.get(i)) 560 .append(", restoreFlag=") 561 .append(widgetRestoreList.get(i)) 562 .append(", profileId=") 563 .append(widgetProfileIdList.get(i)) 564 .append("]"); 565 } 566 builder.append("]"); 567 Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: " + builder); 568 } catch (Exception ex) { 569 Log.e(TAG, "Getting widget ids from the database failed", ex); 570 } 571 } 572 maybeOverrideShortcuts(Context context, ModelDbController controller, SQLiteDatabase db, long currentUser)573 protected static void maybeOverrideShortcuts(Context context, ModelDbController controller, 574 SQLiteDatabase db, long currentUser) { 575 Map<String, LauncherActivityInfo> activityOverrides = 576 ApiWrapper.INSTANCE.get(context).getActivityOverrides(); 577 if (activityOverrides == null || activityOverrides.isEmpty()) { 578 return; 579 } 580 581 try (Cursor c = db.query(Favorites.TABLE_NAME, 582 new String[]{Favorites._ID, Favorites.INTENT}, 583 String.format("%s=? AND %s=? AND ( %s )", Favorites.ITEM_TYPE, Favorites.PROFILE_ID, 584 getTelephonyIntentSQLLiteSelection(activityOverrides.keySet())), 585 new String[]{String.valueOf(ITEM_TYPE_APPLICATION), String.valueOf(currentUser)}, 586 null, null, null); 587 SQLiteTransaction t = new SQLiteTransaction(db)) { 588 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); 589 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); 590 while (c.moveToNext()) { 591 LauncherActivityInfo override = activityOverrides.get(Intent.parseUri( 592 c.getString(intentIndex), 0).getComponent().getPackageName()); 593 if (override != null) { 594 ContentValues values = new ContentValues(); 595 values.put(Favorites.PROFILE_ID, 596 controller.getSerialNumberForUser(override.getUser())); 597 values.put(Favorites.INTENT, AppInfo.makeLaunchIntent(override).toUri(0)); 598 db.update(Favorites.TABLE_NAME, values, String.format("%s=?", Favorites._ID), 599 new String[]{String.valueOf(c.getInt(idIndex))}); 600 } 601 } 602 t.commit(); 603 } catch (Exception ex) { 604 Log.e(TAG, "Error while overriding shortcuts", ex); 605 } 606 } 607 getTelephonyIntentSQLLiteSelection(Collection<String> packages)608 private static String getTelephonyIntentSQLLiteSelection(Collection<String> packages) { 609 return packages.stream().map( 610 packageToChange -> String.format("intent LIKE '%%' || '%s' || '%%' ", 611 packageToChange)).collect( 612 Collectors.joining(" OR ")); 613 } 614 615 /** 616 * Queries and logs the items from the Favorites table in the launcher db. 617 * This is to understand why items might be missing during the restore process for Launcher. 618 * @param database The Launcher db to query from. 619 * @param logHeader First line in log statement, used to explain what is being logged. 620 * @param where The SELECT statement to query items. 621 * @param profileIds The profile ID's for each user profile. 622 */ logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader, String where, String[] profileIds)623 public static void logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader, 624 String where, String[] profileIds) { 625 try (Cursor cursor = database.query( 626 /* table */ Favorites.TABLE_NAME, 627 /* columns */ DB_COLUMNS_TO_LOG, 628 /* selection */ where, 629 /* selection args */ profileIds, 630 /* groupBy */ null, 631 /* having */ null, 632 /* orderBy */ null 633 )) { 634 if (cursor.moveToFirst()) { 635 String[] columnNames = cursor.getColumnNames(); 636 StringBuilder stringBuilder = new StringBuilder(logHeader + "\n"); 637 do { 638 for (String columnName : columnNames) { 639 stringBuilder.append(columnName) 640 .append("=") 641 .append(cursor.getString( 642 cursor.getColumnIndex(columnName))) 643 .append(" "); 644 } 645 stringBuilder.append("\n"); 646 } while (cursor.moveToNext()); 647 FileLog.d(TAG, stringBuilder.toString()); 648 } else { 649 FileLog.d(TAG, "logFavoritesTable: No items found from query for " 650 + "\"" + logHeader + "\""); 651 } 652 } catch (Exception e) { 653 FileLog.e(TAG, "logFavoritesTable: Error reading from database", e); 654 } 655 } 656 657 658 /** 659 * Queries and reports the count of each itemType to be removed due to unrestored profiles. 660 * @param database The Launcher db to query from. 661 * @param where Query being used for to find unrestored profiles 662 * @param profileIds profile ids that were not restored 663 * @param restoreEventLogger Backup/Restore Logger to report metrics 664 */ reportUnrestoredProfiles(SQLiteDatabase database, String where, String[] profileIds, LauncherRestoreEventLogger restoreEventLogger)665 private void reportUnrestoredProfiles(SQLiteDatabase database, String where, 666 String[] profileIds, LauncherRestoreEventLogger restoreEventLogger) { 667 final String query = "SELECT itemType, COUNT(*) AS count FROM favorites WHERE " 668 + where + " GROUP BY itemType"; 669 try (Cursor cursor = database.rawQuery(query, profileIds)) { 670 if (cursor.moveToFirst()) { 671 do { 672 restoreEventLogger.logFavoritesItemsRestoreFailed( 673 cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)), 674 cursor.getInt(cursor.getColumnIndexOrThrow("count")), 675 RestoreError.PROFILE_NOT_RESTORED 676 ); 677 } while (cursor.moveToNext()); 678 } 679 } catch (Exception e) { 680 FileLog.e(TAG, "reportUnrestoredProfiles: Error reading from database", e); 681 } 682 } 683 } 684