• 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 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