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