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 com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; 20 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; 21 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; 22 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; 23 import static com.android.launcher3.provider.LauncherDbUtils.dropTable; 24 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID; 25 26 import android.app.backup.BackupManager; 27 import android.appwidget.AppWidgetHost; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.database.Cursor; 31 import android.database.sqlite.SQLiteDatabase; 32 import android.os.UserHandle; 33 import android.text.TextUtils; 34 import android.util.LongSparseArray; 35 import android.util.SparseLongArray; 36 37 import androidx.annotation.NonNull; 38 39 import com.android.launcher3.AppWidgetsRestoredReceiver; 40 import com.android.launcher3.InvariantDeviceProfile; 41 import com.android.launcher3.LauncherAppState; 42 import com.android.launcher3.LauncherPrefs; 43 import com.android.launcher3.LauncherProvider.DatabaseHelper; 44 import com.android.launcher3.LauncherSettings.Favorites; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.logging.FileLog; 47 import com.android.launcher3.model.DeviceGridState; 48 import com.android.launcher3.model.GridBackupTable; 49 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 50 import com.android.launcher3.model.data.WorkspaceItemInfo; 51 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 52 import com.android.launcher3.util.IntArray; 53 import com.android.launcher3.util.LogConfig; 54 55 import java.io.InvalidObjectException; 56 import java.util.Arrays; 57 58 /** 59 * Utility class to update DB schema after it has been restored. 60 * 61 * This task is executed when Launcher starts for the first time and not immediately after restore. 62 * This helps keep the model consistent if the launcher updates between restore and first startup. 63 */ 64 public class RestoreDbTask { 65 66 private static final String TAG = "RestoreDbTask"; 67 public static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; 68 69 private static final String INFO_COLUMN_NAME = "name"; 70 private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value"; 71 72 public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; 73 public static final String APPWIDGET_IDS = "appwidget_ids"; 74 75 /** 76 * Tries to restore the backup DB if needed 77 */ restoreIfNeeded(Context context, DatabaseHelper helper)78 public static void restoreIfNeeded(Context context, DatabaseHelper helper) { 79 if (!isPending(context)) { 80 return; 81 } 82 if (!performRestore(context, helper)) { 83 helper.createEmptyDB(helper.getWritableDatabase()); 84 } 85 86 // Obtain InvariantDeviceProfile first before setting pending to false, so 87 // InvariantDeviceProfile won't switch to new grid when initializing. 88 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); 89 90 // Set is pending to false irrespective of the result, so that it doesn't get 91 // executed again. 92 LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); 93 94 idp.reinitializeAfterRestore(context); 95 } 96 performRestore(Context context, DatabaseHelper helper)97 private static boolean performRestore(Context context, DatabaseHelper helper) { 98 SQLiteDatabase db = helper.getWritableDatabase(); 99 try (SQLiteTransaction t = new SQLiteTransaction(db)) { 100 RestoreDbTask task = new RestoreDbTask(); 101 task.backupWorkspace(context, db); 102 task.sanitizeDB(context, helper, db, new BackupManager(context)); 103 task.restoreAppWidgetIdsIfExists(context); 104 t.commit(); 105 return true; 106 } catch (Exception e) { 107 FileLog.e(TAG, "Failed to verify db", e); 108 return false; 109 } 110 } 111 112 /** 113 * Restore the workspace if backup is available. 114 */ restoreIfPossible(@onNull Context context, @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)115 public static boolean restoreIfPossible(@NonNull Context context, 116 @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) { 117 final SQLiteDatabase db = helper.getWritableDatabase(); 118 try (SQLiteTransaction t = new SQLiteTransaction(db)) { 119 RestoreDbTask task = new RestoreDbTask(); 120 task.restoreWorkspace(context, db, helper, backupManager); 121 t.commit(); 122 return true; 123 } catch (Exception e) { 124 FileLog.e(TAG, "Failed to restore db", e); 125 return false; 126 } 127 } 128 129 /** 130 * Backup the workspace so that if things go south in restore, we can recover these entries. 131 */ backupWorkspace(Context context, SQLiteDatabase db)132 private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception { 133 InvariantDeviceProfile idp = LauncherAppState.getIDP(context); 134 new GridBackupTable(context, db, idp.numDatabaseHotseatIcons, idp.numColumns, idp.numRows) 135 .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION); 136 } 137 restoreWorkspace(@onNull Context context, @NonNull SQLiteDatabase db, @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)138 private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db, 139 @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) 140 throws Exception { 141 final InvariantDeviceProfile idp = LauncherAppState.getIDP(context); 142 GridBackupTable backupTable = new GridBackupTable(context, db, idp.numDatabaseHotseatIcons, 143 idp.numColumns, idp.numRows); 144 if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) { 145 int itemsDeleted = sanitizeDB(context, helper, db, backupManager); 146 LauncherAppState.getInstance(context).getModel().forceReload(); 147 restoreAppWidgetIdsIfExists(context); 148 if (itemsDeleted == 0) { 149 // all the items are restored, we no longer need the backup table 150 dropTable(db, Favorites.BACKUP_TABLE_NAME); 151 } 152 } 153 } 154 155 /** 156 * Makes the following changes in the provider DB. 157 * 1. Removes all entries belonging to any profiles that were not restored. 158 * 2. Marks all entries as restored. The flags are updated during first load or as 159 * the restored apps get installed. 160 * 3. If the user serial for any restored profile is different than that of the previous 161 * device, update the entries to the new profile id. 162 * 4. If restored from a single display backup, remove gaps between screenIds 163 * 164 * @return number of items deleted. 165 */ sanitizeDB(Context context, DatabaseHelper helper, SQLiteDatabase db, BackupManager backupManager)166 private int sanitizeDB(Context context, DatabaseHelper helper, SQLiteDatabase db, 167 BackupManager backupManager) throws Exception { 168 // Primary user ids 169 long myProfileId = helper.getDefaultUserSerial(); 170 long oldProfileId = getDefaultProfileId(db); 171 LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId); 172 LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size() 173 + 1); 174 175 // Build mapping of restored profile ids to their new profile ids. 176 profileMapping.put(oldProfileId, myProfileId); 177 for (int i = oldManagedProfileIds.size() - 1; i >= 0; --i) { 178 long oldManagedProfileId = oldManagedProfileIds.keyAt(i); 179 UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId); 180 if (user != null) { 181 long newManagedProfileId = helper.getSerialNumberForUser(user); 182 profileMapping.put(oldManagedProfileId, newManagedProfileId); 183 } 184 } 185 186 // Delete all entries which do not belong to any restored profile(s). 187 int numProfiles = profileMapping.size(); 188 String[] profileIds = new String[numProfiles]; 189 profileIds[0] = Long.toString(oldProfileId); 190 for (int i = numProfiles - 1; i >= 1; --i) { 191 profileIds[i] = Long.toString(profileMapping.keyAt(i)); 192 } 193 final String[] args = new String[profileIds.length]; 194 Arrays.fill(args, "?"); 195 final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")"; 196 int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds); 197 FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted"); 198 199 // Mark all items as restored. 200 boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS); 201 ContentValues values = new ContentValues(); 202 values.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON 203 | (keepAllIcons ? WorkspaceItemInfo.FLAG_RESTORE_STARTED : 0)); 204 db.update(Favorites.TABLE_NAME, values, null, null); 205 206 // Mark widgets with appropriate restore flag. 207 values.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 208 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY 209 | LauncherAppWidgetInfo.FLAG_UI_NOT_READY 210 | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0)); 211 db.update(Favorites.TABLE_NAME, values, "itemType = ?", 212 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)}); 213 214 // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp 215 // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will 216 // be no overlap. 217 final long tempLocationOffset = Long.MIN_VALUE; 218 SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size()); 219 int numTempMigrations = 0; 220 for (int i = profileMapping.size() - 1; i >= 0; --i) { 221 long oldId = profileMapping.keyAt(i); 222 long newId = profileMapping.valueAt(i); 223 224 if (oldId != newId) { 225 if (profileMapping.indexOfKey(newId) >= 0) { 226 tempMigratedIds.put(numTempMigrations, newId); 227 numTempMigrations++; 228 newId = tempLocationOffset + newId; 229 } 230 migrateProfileId(db, oldId, newId); 231 } 232 } 233 234 // Migrate ids from their temporary id to their actual final id. 235 for (int i = tempMigratedIds.size() - 1; i >= 0; --i) { 236 long newId = tempMigratedIds.valueAt(i); 237 migrateProfileId(db, tempLocationOffset + newId, newId); 238 } 239 240 if (myProfileId != oldProfileId) { 241 changeDefaultColumn(db, myProfileId); 242 } 243 244 // If restored from a single display backup, remove gaps between screenIds 245 if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) { 246 removeScreenIdGaps(db); 247 } 248 249 return itemsDeleted; 250 } 251 252 /** 253 * Remove gaps between screenIds to make sure no empty pages are left in between. 254 * 255 * e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4] 256 */ removeScreenIdGaps(SQLiteDatabase db)257 protected void removeScreenIdGaps(SQLiteDatabase db) { 258 FileLog.d(TAG, "Removing gaps between screenIds"); 259 IntArray distinctScreens = LauncherDbUtils.queryIntArray(true, db, Favorites.TABLE_NAME, 260 Favorites.SCREEN, Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP, null, 261 Favorites.SCREEN); 262 if (distinctScreens.isEmpty()) { 263 return; 264 } 265 266 StringBuilder sql = new StringBuilder("UPDATE ").append(Favorites.TABLE_NAME) 267 .append(" SET ").append(Favorites.SCREEN).append(" =\nCASE\n"); 268 int screenId = distinctScreens.contains(0) ? 0 : 1; 269 for (int i = 0; i < distinctScreens.size(); i++) { 270 sql.append("WHEN ").append(Favorites.SCREEN).append(" == ") 271 .append(distinctScreens.get(i)).append(" THEN ").append(screenId++).append("\n"); 272 } 273 sql.append("ELSE screen\nEND WHERE ").append(Favorites.CONTAINER).append(" = ") 274 .append(Favorites.CONTAINER_DESKTOP).append(";"); 275 db.execSQL(sql.toString()); 276 } 277 278 /** 279 * Updates profile id of all entries from {@param oldProfileId} to {@param newProfileId}. 280 */ migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId)281 protected void migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId) { 282 FileLog.d(TAG, "Changing profile user id from " + oldProfileId + " to " + newProfileId); 283 // Update existing entries. 284 ContentValues values = new ContentValues(); 285 values.put(Favorites.PROFILE_ID, newProfileId); 286 db.update(Favorites.TABLE_NAME, values, "profileId = ?", 287 new String[]{Long.toString(oldProfileId)}); 288 } 289 290 291 /** 292 * Changes the default value for the column. 293 */ changeDefaultColumn(SQLiteDatabase db, long newProfileId)294 protected void changeDefaultColumn(SQLiteDatabase db, long newProfileId) { 295 db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;"); 296 Favorites.addTableToDb(db, newProfileId, false); 297 db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;"); 298 dropTable(db, "favorites_old"); 299 } 300 301 /** 302 * Returns a list of the managed profile id(s) used in the favorites table of the provided db. 303 */ getManagedProfileIds(SQLiteDatabase db, long defaultProfileId)304 private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) { 305 LongSparseArray<Long> ids = new LongSparseArray<>(); 306 try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? " 307 + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) { 308 while (c.moveToNext()) { 309 ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null); 310 } 311 } 312 return ids; 313 } 314 315 /** 316 * Returns a UserHandle of a restored managed profile with the given serial number, or null 317 * if none found. 318 */ getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber)319 private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager, 320 long ancestralSerialNumber) { 321 if (!Utilities.ATLEAST_Q) { 322 return null; 323 } 324 return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber); 325 } 326 327 /** 328 * Returns the profile id used in the favorites table of the provided db. 329 */ getDefaultProfileId(SQLiteDatabase db)330 protected long getDefaultProfileId(SQLiteDatabase db) throws Exception { 331 try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) { 332 int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME); 333 while (c.moveToNext()) { 334 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) { 335 return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE)); 336 } 337 } 338 throw new InvalidObjectException("Table does not have a profile id column"); 339 } 340 } 341 isPending(Context context)342 public static boolean isPending(Context context) { 343 return LauncherPrefs.get(context).has(RESTORE_DEVICE); 344 } 345 346 /** 347 * Marks the DB state as pending restoration 348 */ setPending(Context context)349 public static void setPending(Context context) { 350 FileLog.d(TAG, "Restore data received through full backup "); 351 LauncherPrefs.get(context) 352 .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType())); 353 } 354 restoreAppWidgetIdsIfExists(Context context)355 private void restoreAppWidgetIdsIfExists(Context context) { 356 LauncherPrefs lp = LauncherPrefs.get(context); 357 if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { 358 AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID); 359 AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, 360 IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), 361 IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), 362 host); 363 } else { 364 FileLog.d(TAG, "No app widget ids to restore."); 365 } 366 367 lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS); 368 } 369 setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, @NonNull int[] newIds)370 public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, 371 @NonNull int[] newIds) { 372 LauncherPrefs.get(context).putSync( 373 OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()), 374 APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString())); 375 } 376 377 } 378