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