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