• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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