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.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; 22 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; 23 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; 24 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; 25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 26 import static com.android.launcher3.provider.LauncherDbUtils.dropTable; 27 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; 28 29 import android.app.backup.BackupManager; 30 import android.appwidget.AppWidgetHost; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.LauncherActivityInfo; 35 import android.database.Cursor; 36 import android.database.sqlite.SQLiteDatabase; 37 import android.os.UserHandle; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.util.LongSparseArray; 41 import android.util.SparseLongArray; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.launcher3.AppWidgetsRestoredReceiver; 47 import com.android.launcher3.InvariantDeviceProfile; 48 import com.android.launcher3.LauncherPrefs; 49 import com.android.launcher3.LauncherSettings.Favorites; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.logging.FileLog; 52 import com.android.launcher3.model.DeviceGridState; 53 import com.android.launcher3.model.ModelDbController; 54 import com.android.launcher3.model.data.AppInfo; 55 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 56 import com.android.launcher3.model.data.WorkspaceItemInfo; 57 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 58 import com.android.launcher3.uioverrides.ApiWrapper; 59 import com.android.launcher3.util.IntArray; 60 import com.android.launcher3.util.LogConfig; 61 62 import java.io.InvalidObjectException; 63 import java.util.Arrays; 64 import java.util.Collection; 65 import java.util.Map; 66 import java.util.stream.Collectors; 67 68 /** 69 * Utility class to update DB schema after it has been restored. 70 * 71 * This task is executed when Launcher starts for the first time and not immediately after restore. 72 * This helps keep the model consistent if the launcher updates between restore and first startup. 73 */ 74 public class RestoreDbTask { 75 76 private static final String TAG = "RestoreDbTask"; 77 public static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; 78 79 private static final String INFO_COLUMN_NAME = "name"; 80 private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value"; 81 82 public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; 83 public static final String APPWIDGET_IDS = "appwidget_ids"; 84 85 private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen", 86 "container", "cellX", "cellY", "spanX", "spanY", "intent"}; 87 88 /** 89 * Tries to restore the backup DB if needed 90 */ restoreIfNeeded(Context context, ModelDbController dbController)91 public static void restoreIfNeeded(Context context, ModelDbController dbController) { 92 if (!isPending(context)) { 93 Log.d(TAG, "No restore task pending, exiting RestoreDbTask"); 94 return; 95 } 96 if (!performRestore(context, dbController)) { 97 dbController.createEmptyDB(); 98 } 99 100 // Obtain InvariantDeviceProfile first before setting pending to false, so 101 // InvariantDeviceProfile won't switch to new grid when initializing. 102 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); 103 104 // Set is pending to false irrespective of the result, so that it doesn't get 105 // executed again. 106 LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); 107 108 idp.reinitializeAfterRestore(context); 109 } 110 performRestore(Context context, ModelDbController controller)111 private static boolean performRestore(Context context, ModelDbController controller) { 112 SQLiteDatabase db = controller.getDb(); 113 FileLog.d(TAG, "performRestore: starting restore from db"); 114 try (SQLiteTransaction t = new SQLiteTransaction(db)) { 115 RestoreDbTask task = new RestoreDbTask(); 116 task.sanitizeDB(context, controller, db, new BackupManager(context)); 117 task.restoreAppWidgetIdsIfExists(context, controller); 118 t.commit(); 119 return true; 120 } catch (Exception e) { 121 FileLog.e(TAG, "Failed to verify db", e); 122 return false; 123 } 124 } 125 126 /** 127 * Makes the following changes in the provider DB. 128 * 1. Removes all entries belonging to any profiles that were not restored. 129 * 2. Marks all entries as restored. The flags are updated during first load or as 130 * the restored apps get installed. 131 * 3. If the user serial for any restored profile is different than that of the previous 132 * device, update the entries to the new profile id. 133 * 4. If restored from a single display backup, remove gaps between screenIds 134 * 5. Override shortcuts that need to be replaced. 135 * 136 * @return number of items deleted. 137 */ 138 @VisibleForTesting sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, BackupManager backupManager)139 protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, 140 BackupManager backupManager) throws Exception { 141 FileLog.d(TAG, "Old Launcher Database before sanitizing:"); 142 // Primary user ids 143 long myProfileId = controller.getSerialNumberForUser(myUserHandle()); 144 long oldProfileId = getDefaultProfileId(db); 145 FileLog.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId); 146 LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId); 147 LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size() 148 + 1); 149 150 // Build mapping of restored profile ids to their new profile ids. 151 profileMapping.put(oldProfileId, myProfileId); 152 for (int i = oldManagedProfileIds.size() - 1; i >= 0; --i) { 153 long oldManagedProfileId = oldManagedProfileIds.keyAt(i); 154 UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId); 155 if (user != null) { 156 long newManagedProfileId = controller.getSerialNumberForUser(user); 157 profileMapping.put(oldManagedProfileId, newManagedProfileId); 158 FileLog.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId 159 + " should be mapped to new id=" + newManagedProfileId); 160 } else { 161 FileLog.e(TAG, "sanitizeDB: No User found for old profileId, Ancestral Serial " 162 + "Number: " + oldManagedProfileId); 163 } 164 } 165 166 // Delete all entries which do not belong to any restored profile(s). 167 int numProfiles = profileMapping.size(); 168 String[] profileIds = new String[numProfiles]; 169 profileIds[0] = Long.toString(oldProfileId); 170 for (int i = numProfiles - 1; i >= 1; --i) { 171 profileIds[i] = Long.toString(profileMapping.keyAt(i)); 172 } 173 174 final String[] args = new String[profileIds.length]; 175 Arrays.fill(args, "?"); 176 final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")"; 177 logUnrestoredItems(db, where, profileIds); 178 int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds); 179 FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted"); 180 181 // Mark all items as restored. 182 boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS); 183 ContentValues values = new ContentValues(); 184 values.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON 185 | (keepAllIcons ? WorkspaceItemInfo.FLAG_RESTORE_STARTED : 0)); 186 db.update(Favorites.TABLE_NAME, values, null, null); 187 188 // Mark widgets with appropriate restore flag. 189 values.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 190 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY 191 | LauncherAppWidgetInfo.FLAG_UI_NOT_READY 192 | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0)); 193 db.update(Favorites.TABLE_NAME, values, "itemType = ?", 194 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)}); 195 196 // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp 197 // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will 198 // be no overlap. 199 final long tempLocationOffset = Long.MIN_VALUE; 200 SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size()); 201 int numTempMigrations = 0; 202 for (int i = profileMapping.size() - 1; i >= 0; --i) { 203 long oldId = profileMapping.keyAt(i); 204 long newId = profileMapping.valueAt(i); 205 206 if (oldId != newId) { 207 if (profileMapping.indexOfKey(newId) >= 0) { 208 tempMigratedIds.put(numTempMigrations, newId); 209 numTempMigrations++; 210 newId = tempLocationOffset + newId; 211 } 212 migrateProfileId(db, oldId, newId); 213 } 214 } 215 216 // Migrate ids from their temporary id to their actual final id. 217 for (int i = tempMigratedIds.size() - 1; i >= 0; --i) { 218 long newId = tempMigratedIds.valueAt(i); 219 migrateProfileId(db, tempLocationOffset + newId, newId); 220 } 221 222 if (myProfileId != oldProfileId) { 223 changeDefaultColumn(db, myProfileId); 224 } 225 226 // If restored from a single display backup, remove gaps between screenIds 227 if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) { 228 removeScreenIdGaps(db); 229 } 230 231 // Override shortcuts 232 maybeOverrideShortcuts(context, controller, db, myProfileId); 233 return itemsDeletedCount; 234 } 235 236 /** 237 * Queries and logs the items we will delete from unrestored profiles in the launcher db. 238 * This is to understand why items might be missing during the restore process for Launcher. 239 * @param database the Launcher db to query from. 240 * @param where the SELECT statement to query items that will be deleted. 241 * @param profileIds the profile ID's the user will be migrating to. 242 */ logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds)243 private void logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds) { 244 try (Cursor itemsToDelete = database.query( 245 /* table */ Favorites.TABLE_NAME, 246 /* columns */ DB_COLUMNS_TO_LOG, 247 /* selection */ where, 248 /* selection args */ profileIds, 249 /* groupBy */ null, 250 /* having */ null, 251 /* orderBy */ null 252 )) { 253 if (itemsToDelete.moveToFirst()) { 254 String[] columnNames = itemsToDelete.getColumnNames(); 255 StringBuilder stringBuilder = new StringBuilder( 256 "items to be deleted from the Favorites Table during restore:\n" 257 ); 258 do { 259 for (String columnName : columnNames) { 260 stringBuilder.append(columnName) 261 .append("=") 262 .append(itemsToDelete.getString( 263 itemsToDelete.getColumnIndex(columnName))) 264 .append(" "); 265 } 266 stringBuilder.append("\n"); 267 } while (itemsToDelete.moveToNext()); 268 FileLog.d(TAG, stringBuilder.toString()); 269 } else { 270 FileLog.d(TAG, "logDeletedItems: No items found to delete"); 271 } 272 } catch (Exception e) { 273 FileLog.e(TAG, "logDeletedItems: Error reading from database", e); 274 } 275 } 276 277 /** 278 * Remove gaps between screenIds to make sure no empty pages are left in between. 279 * 280 * e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4] 281 */ removeScreenIdGaps(SQLiteDatabase db)282 protected void removeScreenIdGaps(SQLiteDatabase db) { 283 FileLog.d(TAG, "Removing gaps between screenIds"); 284 IntArray distinctScreens = LauncherDbUtils.queryIntArray(true, db, Favorites.TABLE_NAME, 285 Favorites.SCREEN, Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP, null, 286 Favorites.SCREEN); 287 if (distinctScreens.isEmpty()) { 288 return; 289 } 290 291 StringBuilder sql = new StringBuilder("UPDATE ").append(Favorites.TABLE_NAME) 292 .append(" SET ").append(Favorites.SCREEN).append(" =\nCASE\n"); 293 int screenId = distinctScreens.contains(0) ? 0 : 1; 294 for (int i = 0; i < distinctScreens.size(); i++) { 295 sql.append("WHEN ").append(Favorites.SCREEN).append(" == ") 296 .append(distinctScreens.get(i)).append(" THEN ").append(screenId++).append("\n"); 297 } 298 sql.append("ELSE screen\nEND WHERE ").append(Favorites.CONTAINER).append(" = ") 299 .append(Favorites.CONTAINER_DESKTOP).append(";"); 300 db.execSQL(sql.toString()); 301 } 302 303 /** 304 * Updates profile id of all entries from {@param oldProfileId} to {@param newProfileId}. 305 */ migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId)306 protected void migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId) { 307 FileLog.d(TAG, "Changing profile user id from " + oldProfileId + " to " + newProfileId); 308 // Update existing entries. 309 ContentValues values = new ContentValues(); 310 values.put(Favorites.PROFILE_ID, newProfileId); 311 db.update(Favorites.TABLE_NAME, values, "profileId = ?", 312 new String[]{Long.toString(oldProfileId)}); 313 } 314 315 316 /** 317 * Changes the default value for the column. 318 */ changeDefaultColumn(SQLiteDatabase db, long newProfileId)319 protected void changeDefaultColumn(SQLiteDatabase db, long newProfileId) { 320 db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;"); 321 Favorites.addTableToDb(db, newProfileId, false); 322 db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;"); 323 dropTable(db, "favorites_old"); 324 } 325 326 /** 327 * Returns a list of the managed profile id(s) used in the favorites table of the provided db. 328 */ getManagedProfileIds(SQLiteDatabase db, long defaultProfileId)329 private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) { 330 LongSparseArray<Long> ids = new LongSparseArray<>(); 331 try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? " 332 + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) { 333 while (c.moveToNext()) { 334 ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null); 335 } 336 } 337 return ids; 338 } 339 340 /** 341 * Returns a UserHandle of a restored managed profile with the given serial number, or null 342 * if none found. 343 */ getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber)344 private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager, 345 long ancestralSerialNumber) { 346 if (!Utilities.ATLEAST_Q) { 347 return null; 348 } 349 return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber); 350 } 351 352 /** 353 * Returns the profile id used in the favorites table of the provided db. 354 */ getDefaultProfileId(SQLiteDatabase db)355 protected long getDefaultProfileId(SQLiteDatabase db) throws Exception { 356 try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) { 357 int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME); 358 while (c.moveToNext()) { 359 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) { 360 return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE)); 361 } 362 } 363 throw new InvalidObjectException("Table does not have a profile id column"); 364 } 365 } 366 isPending(Context context)367 public static boolean isPending(Context context) { 368 return LauncherPrefs.get(context).has(RESTORE_DEVICE); 369 } 370 371 /** 372 * Marks the DB state as pending restoration 373 */ setPending(Context context)374 public static void setPending(Context context) { 375 FileLog.d(TAG, "Restore data received through full backup"); 376 LauncherPrefs.get(context) 377 .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType())); 378 } 379 restoreAppWidgetIdsIfExists(Context context, ModelDbController controller)380 private void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller) { 381 LauncherPrefs lp = LauncherPrefs.get(context); 382 if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { 383 AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID); 384 AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, controller, 385 IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), 386 IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), 387 host); 388 } else { 389 FileLog.d(TAG, "No app widget ids to restore."); 390 } 391 392 lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS); 393 } 394 setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, @NonNull int[] newIds)395 public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, 396 @NonNull int[] newIds) { 397 LauncherPrefs.get(context).putSync( 398 OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()), 399 APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString())); 400 } 401 maybeOverrideShortcuts(Context context, ModelDbController controller, SQLiteDatabase db, long currentUser)402 protected static void maybeOverrideShortcuts(Context context, ModelDbController controller, 403 SQLiteDatabase db, long currentUser) { 404 Map<String, LauncherActivityInfo> activityOverrides = ApiWrapper.getActivityOverrides( 405 context); 406 407 if (activityOverrides == null || activityOverrides.isEmpty()) { 408 return; 409 } 410 411 try (Cursor c = db.query(Favorites.TABLE_NAME, 412 new String[]{Favorites._ID, Favorites.INTENT}, 413 String.format("%s=? AND %s=? AND ( %s )", Favorites.ITEM_TYPE, Favorites.PROFILE_ID, 414 getTelephonyIntentSQLLiteSelection(activityOverrides.keySet())), 415 new String[]{String.valueOf(ITEM_TYPE_APPLICATION), String.valueOf(currentUser)}, 416 null, null, null); 417 SQLiteTransaction t = new SQLiteTransaction(db)) { 418 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); 419 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); 420 while (c.moveToNext()) { 421 LauncherActivityInfo override = activityOverrides.get(Intent.parseUri( 422 c.getString(intentIndex), 0).getComponent().getPackageName()); 423 if (override != null) { 424 ContentValues values = new ContentValues(); 425 values.put(Favorites.PROFILE_ID, 426 controller.getSerialNumberForUser(override.getUser())); 427 values.put(Favorites.INTENT, AppInfo.makeLaunchIntent(override).toUri(0)); 428 db.update(Favorites.TABLE_NAME, values, String.format("%s=?", Favorites._ID), 429 new String[]{String.valueOf(c.getInt(idIndex))}); 430 } 431 } 432 t.commit(); 433 } catch (Exception ex) { 434 Log.e(TAG, "Error while overriding shortcuts", ex); 435 } 436 } 437 getTelephonyIntentSQLLiteSelection(Collection<String> packages)438 private static String getTelephonyIntentSQLLiteSelection(Collection<String> packages) { 439 return packages.stream().map( 440 packageToChange -> String.format("intent LIKE '%%' || '%s' || '%%' ", 441 packageToChange)).collect( 442 Collectors.joining(" OR ")); 443 } 444 445 } 446