• 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.BuildConfig.WIDGETS_ENABLED;
22 import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
23 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
24 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
25 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
26 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS;
27 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE;
28 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
29 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
30 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
31 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
32 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
33 
34 import android.app.backup.BackupManager;
35 import android.appwidget.AppWidgetHost;
36 import android.appwidget.AppWidgetManager;
37 import android.appwidget.AppWidgetProviderInfo;
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.pm.LauncherActivityInfo;
42 import android.database.Cursor;
43 import android.database.sqlite.SQLiteDatabase;
44 import android.os.UserHandle;
45 import android.text.TextUtils;
46 import android.util.Log;
47 import android.util.LongSparseArray;
48 import android.util.SparseLongArray;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.VisibleForTesting;
52 import androidx.annotation.WorkerThread;
53 
54 import com.android.launcher3.Flags;
55 import com.android.launcher3.InvariantDeviceProfile;
56 import com.android.launcher3.LauncherAppState;
57 import com.android.launcher3.LauncherFiles;
58 import com.android.launcher3.LauncherPrefs;
59 import com.android.launcher3.LauncherSettings;
60 import com.android.launcher3.LauncherSettings.Favorites;
61 import com.android.launcher3.Utilities;
62 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
63 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
64 import com.android.launcher3.logging.FileLog;
65 import com.android.launcher3.model.DeviceGridState;
66 import com.android.launcher3.model.LoaderTask;
67 import com.android.launcher3.model.ModelDbController;
68 import com.android.launcher3.model.data.AppInfo;
69 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
70 import com.android.launcher3.model.data.WorkspaceItemInfo;
71 import com.android.launcher3.pm.UserCache;
72 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
73 import com.android.launcher3.util.ApiWrapper;
74 import com.android.launcher3.util.ContentWriter;
75 import com.android.launcher3.util.IntArray;
76 import com.android.launcher3.util.LogConfig;
77 
78 import java.io.InvalidObjectException;
79 import java.util.Arrays;
80 import java.util.Collection;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.function.Supplier;
84 import java.util.stream.Collectors;
85 
86 /**
87  * Utility class to update DB schema after it has been restored.
88  *
89  * This task is executed when Launcher starts for the first time and not immediately after restore.
90  * This helps keep the model consistent if the launcher updates between restore and first startup.
91  */
92 public class RestoreDbTask {
93 
94     private static final String TAG = "RestoreDbTask";
95     public static final String RESTORED_DEVICE_TYPE = "restored_task_pending";
96     public static final String FIRST_LOAD_AFTER_RESTORE_KEY = "first_load_after_restore";
97 
98     private static final String INFO_COLUMN_NAME = "name";
99     private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
100 
101     public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
102     public static final String APPWIDGET_IDS = "appwidget_ids";
103     @VisibleForTesting
104     public static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
105             "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider",
106             "appWidgetId", "restored"};
107 
108     /**
109      * Tries to restore the backup DB if needed
110      */
restoreIfNeeded(Context context, ModelDbController dbController)111     public static void restoreIfNeeded(Context context, ModelDbController dbController) {
112         if (!isPending(context)) {
113             Log.d(TAG, "No restore task pending, exiting RestoreDbTask");
114             return;
115         }
116         if (!performRestore(context, dbController)) {
117             dbController.createEmptyDB();
118         }
119 
120         // Obtain InvariantDeviceProfile first before setting pending to false, so
121         // InvariantDeviceProfile won't switch to new grid when initializing.
122         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
123 
124         // Set is pending to false irrespective of the result, so that it doesn't get
125         // executed again.
126         LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
127 
128         DeviceGridState deviceGridState = new DeviceGridState(context);
129         FileLog.d(TAG, "restoreIfNeeded: deviceGridState from context: " + deviceGridState);
130         String oldPhoneFileName = deviceGridState.getDbFile();
131         List<String> previousDbs = existingDbs(context);
132         removeOldDBs(context, oldPhoneFileName);
133         // The idp before this contains data about the old phone, after this it becomes the idp
134         // of the current phone.
135         if (!Flags.oneGridSpecs()) {
136             FileLog.d(TAG, "Resetting IDP to default for restore dest device");
137             idp.reset(context);
138             trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
139         }
140     }
141 
142 
143     /**
144      * Try setting the gird used in the previous phone to the new one. If the current device doesn't
145      * support the previous grid option it will not be set.
146      */
trySettingPreviousGridAsCurrent(Context context, InvariantDeviceProfile idp, String oldPhoneDbFileName, List<String> previousDbs)147     private static void trySettingPreviousGridAsCurrent(Context context, InvariantDeviceProfile idp,
148             String oldPhoneDbFileName, List<String> previousDbs) {
149         InvariantDeviceProfile.GridOption oldPhoneGridOption = idp.getGridOptionFromFileName(
150                 context, oldPhoneDbFileName);
151         // The grid option could be null if current phone doesn't support the previous db.
152         if (oldPhoneGridOption != null) {
153             FileLog.d(TAG, "trySettingPreviousGridAsCurrent:"
154                     + ", oldPhoneDbFileName: " + oldPhoneDbFileName
155                     + ", oldPhoneGridOption: " + oldPhoneGridOption
156                     + ", previousDbs: " + previousDbs);
157 
158             /* If the user only used the default db on the previous phone and the new default db is
159              * bigger than or equal to the previous one, then keep the new default db */
160             if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns
161                     && oldPhoneGridOption.numRows <= idp.numRows) {
162                 /* Keep the user in default grid */
163                 FileLog.d(TAG, "Keeping default db from restore as current grid");
164                 return;
165             }
166             /*
167              * Here we are setting the previous db as the current one.
168              */
169             FileLog.d(TAG, "Setting grid from old device as current grid: "
170                 + "oldPhoneGridOption:" + oldPhoneGridOption.name);
171             idp.setCurrentGrid(context, oldPhoneGridOption.name);
172         }
173     }
174 
175     /**
176      * Returns a list of paths of the existing launcher dbs.
177      */
178     @VisibleForTesting
existingDbs(Context context)179     public static List<String> existingDbs(Context context) {
180         // At this point idp.dbFile contains the name of the dbFile from the previous phone
181         return LauncherFiles.GRID_DB_FILES.stream()
182                 .filter(dbName -> context.getDatabasePath(dbName).exists())
183                 .collect(Collectors.toList());
184     }
185 
186     /**
187      * Only keep the last database used on the previous device.
188      */
189     @VisibleForTesting
removeOldDBs(Context context, String oldPhoneDbFileName)190     public static void removeOldDBs(Context context, String oldPhoneDbFileName) {
191         // At this point idp.dbFile contains the name of the dbFile from the previous phone
192         LauncherFiles.GRID_DB_FILES.stream()
193                 .filter(dbName -> !dbName.equals(oldPhoneDbFileName))
194                 .forEach(dbName -> {
195                     if (context.getDatabasePath(dbName).delete()) {
196                         FileLog.d(TAG, "Removed old grid db file: " + dbName);
197                     }
198                 });
199     }
200 
performRestore(Context context, ModelDbController controller)201     private static boolean performRestore(Context context, ModelDbController controller) {
202         SQLiteDatabase db = controller.getDb();
203         FileLog.d(TAG, "performRestore: starting restore from db");
204         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
205             RestoreDbTask task = new RestoreDbTask();
206             BackupManager backupManager = new BackupManager(context);
207             LauncherRestoreEventLogger restoreEventLogger =
208                     LauncherRestoreEventLogger.Companion.newInstance(context);
209             task.sanitizeDB(context, controller, db, backupManager, restoreEventLogger);
210             task.restoreAppWidgetIdsIfExists(context, controller, restoreEventLogger,
211                     () -> new AppWidgetHost(context, APPWIDGET_HOST_ID));
212             t.commit();
213             return true;
214         } catch (Exception e) {
215             FileLog.e(TAG, "Failed to verify db", e);
216             return false;
217         }
218     }
219 
220     /**
221      * Makes the following changes in the provider DB.
222      *   1. Removes all entries belonging to any profiles that were not restored.
223      *   2. Marks all entries as restored. The flags are updated during first load or as
224      *      the restored apps get installed.
225      *   3. If the user serial for any restored profile is different than that of the previous
226      *      device, update the entries to the new profile id.
227      *   4. If restored from a single display backup, remove gaps between screenIds
228      *   5. Override shortcuts that need to be replaced.
229      *
230      * @return number of items deleted
231      */
232     @VisibleForTesting
sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db, BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger)233     protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
234             BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger)
235             throws Exception {
236         logFavoritesTable(db, "Old Launcher Database before sanitizing:", null, null);
237         // Primary user ids
238         long myProfileId = controller.getSerialNumberForUser(myUserHandle());
239         long oldProfileId = getDefaultProfileId(db);
240         FileLog.d(TAG, "sanitizeDB: myProfileId= " + myProfileId
241                 + ", oldProfileId= " + oldProfileId);
242         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
243         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
244                 + 1);
245 
246         // Build mapping of restored profile ids to their new profile ids.
247         profileMapping.put(oldProfileId, myProfileId);
248         for (int i = oldManagedProfileIds.size() - 1; i >= 0; --i) {
249             long oldManagedProfileId = oldManagedProfileIds.keyAt(i);
250             UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId);
251             if (user != null) {
252                 long newManagedProfileId = controller.getSerialNumberForUser(user);
253                 profileMapping.put(oldManagedProfileId, newManagedProfileId);
254                 FileLog.d(TAG, "sanitizeDB: managed profile id=" + oldManagedProfileId
255                         + " should be mapped to new id=" + newManagedProfileId);
256             } else {
257                 FileLog.e(TAG, "sanitizeDB: No User found for old profileId, Ancestral Serial "
258                         + "Number: " + oldManagedProfileId);
259             }
260         }
261 
262         // Delete all entries which do not belong to any restored profile(s).
263         int numProfiles = profileMapping.size();
264         String[] profileIds = new String[numProfiles];
265         profileIds[0] = Long.toString(oldProfileId);
266         for (int i = numProfiles - 1; i >= 1; --i) {
267             profileIds[i] = Long.toString(profileMapping.keyAt(i));
268         }
269 
270         final String[] args = new String[profileIds.length];
271         Arrays.fill(args, "?");
272         final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
273         logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds);
274         if (enableLauncherBrMetricsFixed()) {
275             reportUnrestoredProfiles(db, where, profileIds, restoreEventLogger);
276         }
277         int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
278         FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
279 
280         // Mark all items as restored.
281         boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
282         ContentValues values = new ContentValues();
283         values.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON
284                 | (keepAllIcons ? WorkspaceItemInfo.FLAG_RESTORE_STARTED : 0));
285         db.update(Favorites.TABLE_NAME, values, null, null);
286 
287         // Mark widgets with appropriate restore flag.
288         values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
289                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
290                 | LauncherAppWidgetInfo.FLAG_UI_NOT_READY
291                 | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
292         db.update(Favorites.TABLE_NAME, values, "itemType = ?",
293                 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
294 
295         // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp
296         // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will
297         // be no overlap.
298         final long tempLocationOffset = Long.MIN_VALUE;
299         SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size());
300         int numTempMigrations = 0;
301         for (int i = profileMapping.size() - 1; i >= 0; --i) {
302             long oldId = profileMapping.keyAt(i);
303             long newId = profileMapping.valueAt(i);
304 
305             if (oldId != newId) {
306                 if (profileMapping.indexOfKey(newId) >= 0) {
307                     tempMigratedIds.put(numTempMigrations, newId);
308                     numTempMigrations++;
309                     newId = tempLocationOffset + newId;
310                 }
311                 migrateProfileId(db, oldId, newId);
312             }
313         }
314 
315         // Migrate ids from their temporary id to their actual final id.
316         for (int i = tempMigratedIds.size() - 1; i >= 0; --i) {
317             long newId = tempMigratedIds.valueAt(i);
318             migrateProfileId(db, tempLocationOffset + newId, newId);
319         }
320 
321         if (myProfileId != oldProfileId) {
322             changeDefaultColumn(db, myProfileId);
323         }
324 
325         // If restored from a single display backup, remove gaps between screenIds
326         if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) {
327             removeScreenIdGaps(db);
328         }
329 
330         // Override shortcuts
331         maybeOverrideShortcuts(context, controller, db, myProfileId);
332         return itemsDeletedCount;
333     }
334 
335     /**
336      * Remove gaps between screenIds to make sure no empty pages are left in between.
337      *
338      * e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4]
339      */
removeScreenIdGaps(SQLiteDatabase db)340     protected void removeScreenIdGaps(SQLiteDatabase db) {
341         FileLog.d(TAG, "Removing gaps between screenIds");
342         IntArray distinctScreens = LauncherDbUtils.queryIntArray(true, db, Favorites.TABLE_NAME,
343                 Favorites.SCREEN, Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP, null,
344                 Favorites.SCREEN);
345         if (distinctScreens.isEmpty()) {
346             return;
347         }
348 
349         StringBuilder sql = new StringBuilder("UPDATE ").append(Favorites.TABLE_NAME)
350                 .append(" SET ").append(Favorites.SCREEN).append(" =\nCASE\n");
351         int screenId = distinctScreens.contains(0) ? 0 : 1;
352         for (int i = 0; i < distinctScreens.size(); i++) {
353             sql.append("WHEN ").append(Favorites.SCREEN).append(" == ")
354                     .append(distinctScreens.get(i)).append(" THEN ").append(screenId++).append("\n");
355         }
356         sql.append("ELSE screen\nEND WHERE ").append(Favorites.CONTAINER).append(" = ")
357                 .append(Favorites.CONTAINER_DESKTOP).append(";");
358         db.execSQL(sql.toString());
359     }
360 
361     /**
362      * Updates profile id of all entries from {@param oldProfileId} to {@param newProfileId}.
363      */
migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId)364     protected void migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId) {
365         FileLog.d(TAG, "Changing profile user id from " + oldProfileId + " to " + newProfileId);
366         // Update existing entries.
367         ContentValues values = new ContentValues();
368         values.put(Favorites.PROFILE_ID, newProfileId);
369         db.update(Favorites.TABLE_NAME, values, "profileId = ?",
370                 new String[]{Long.toString(oldProfileId)});
371     }
372 
373 
374     /**
375      * Changes the default value for the column.
376      */
changeDefaultColumn(SQLiteDatabase db, long newProfileId)377     protected void changeDefaultColumn(SQLiteDatabase db, long newProfileId) {
378         db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;");
379         Favorites.addTableToDb(db, newProfileId, false);
380         db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;");
381         dropTable(db, "favorites_old");
382     }
383 
384     /**
385      * Returns a list of the managed profile id(s) used in the favorites table of the provided db.
386      */
getManagedProfileIds(SQLiteDatabase db, long defaultProfileId)387     private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) {
388         LongSparseArray<Long> ids = new LongSparseArray<>();
389         try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? "
390                 + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) {
391             while (c.moveToNext()) {
392                 ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
393             }
394         }
395         return ids;
396     }
397 
398     /**
399      * Returns a UserHandle of a restored managed profile with the given serial number, or null
400      * if none found.
401      */
getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber)402     private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager,
403             long ancestralSerialNumber) {
404         return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber);
405     }
406 
407     /**
408      * Returns the profile id used in the favorites table of the provided db.
409      */
getDefaultProfileId(SQLiteDatabase db)410     protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
411         try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) {
412             int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
413             while (c.moveToNext()) {
414                 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
415                     return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE));
416                 }
417             }
418             throw new InvalidObjectException("Table does not have a profile id column");
419         }
420     }
421 
isPending(Context context)422     public static boolean isPending(Context context) {
423         return isPending(LauncherPrefs.get(context));
424     }
425 
isPending(LauncherPrefs prefs)426     public static boolean isPending(LauncherPrefs prefs) {
427         return prefs.has(RESTORE_DEVICE);
428     }
429 
430     /**
431      * Marks the DB state as pending restoration
432      */
setPending(Context context)433     public static void setPending(Context context) {
434         DeviceGridState deviceGridState = new DeviceGridState(context);
435         FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState);
436         LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType()));
437         LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
438     }
439 
440     @WorkerThread
441     @VisibleForTesting
restoreAppWidgetIdsIfExists(Context context, ModelDbController controller, LauncherRestoreEventLogger restoreEventLogger, Supplier<AppWidgetHost> hostSupplier)442     void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller,
443             LauncherRestoreEventLogger restoreEventLogger, Supplier<AppWidgetHost> hostSupplier) {
444         LauncherPrefs lp = LauncherPrefs.get(context);
445         if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) {
446             restoreAppWidgetIds(context, controller, restoreEventLogger,
447                     IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(),
448                     IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
449                     hostSupplier.get());
450         } else {
451             FileLog.d(TAG, "Did not receive new app widget id map during Launcher restore");
452         }
453 
454         lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS);
455     }
456 
457     /**
458      * Updates the app widgets whose id has changed during the restore process.
459      */
460     @WorkerThread
restoreAppWidgetIds(Context context, ModelDbController controller, LauncherRestoreEventLogger launcherRestoreEventLogger, int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host)461     private void restoreAppWidgetIds(Context context, ModelDbController controller,
462             LauncherRestoreEventLogger launcherRestoreEventLogger, int[] oldWidgetIds,
463             int[] newWidgetIds, @NonNull AppWidgetHost host) {
464         if (!WIDGETS_ENABLED) {
465             FileLog.e(TAG, "Skipping widget ID remap as widgets not supported");
466             host.deleteHost();
467             launcherRestoreEventLogger.logFavoritesItemsRestoreFailed(Favorites.ITEM_TYPE_APPWIDGET,
468                     oldWidgetIds.length, RestoreError.WIDGETS_DISABLED);
469             return;
470         }
471         if (!RestoreDbTask.isPending(context)) {
472             // Someone has already gone through our DB once, probably LoaderTask. Skip any further
473             // modifications of the DB.
474             FileLog.e(TAG, "Skipping widget ID remap as DB already in use");
475             for (int widgetId : newWidgetIds) {
476                 FileLog.d(TAG, "Deleting widgetId: " + widgetId);
477                 host.deleteAppWidgetId(widgetId);
478             }
479             return;
480         }
481 
482         final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
483 
484         FileLog.d(TAG, "restoreAppWidgetIds: "
485                 + "oldWidgetIds=" + IntArray.wrap(oldWidgetIds).toConcatString()
486                 + ", newWidgetIds=" + IntArray.wrap(newWidgetIds).toConcatString());
487 
488         // TODO(b/234700507): Remove the logs after the bug is fixed
489         logDatabaseWidgetInfo(controller);
490 
491         for (int i = 0; i < oldWidgetIds.length; i++) {
492             FileLog.i(TAG, "migrating appWidgetId: " + oldWidgetIds[i] + " => " + newWidgetIds[i]);
493 
494             final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]);
495             final int state;
496             if (LoaderTask.isValidProvider(provider)) {
497                 // This will ensure that we show 'Click to setup' UI if required.
498                 state = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
499             } else {
500                 state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
501             }
502 
503             // b/135926478: Work profile widget restore is broken in platform. This forces us to
504             // recreate the widget during loading with the correct host provider.
505             long mainProfileId = UserCache.INSTANCE.get(context)
506                     .getSerialNumberForUser(myUserHandle());
507             long controllerProfileId = controller.getSerialNumberForUser(myUserHandle());
508             String oldWidgetId = Integer.toString(oldWidgetIds[i]);
509             final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?";
510             String profileId = Long.toString(mainProfileId);
511             final String[] args = new String[] { oldWidgetId, profileId };
512             FileLog.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId
513                     + " with controller profile ID=" + controllerProfileId);
514             int result = new ContentWriter(context,
515                     new ContentWriter.CommitParams(controller, where, args))
516                     .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
517                     .put(LauncherSettings.Favorites.RESTORED, state)
518                     .commit();
519             if (result == 0) {
520                 // TODO(b/234700507): Remove the logs after the bug is fixed
521                 FileLog.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in"
522                         + " the database anymore");
523                 try (Cursor cursor = controller.getDb().query(
524                         Favorites.TABLE_NAME,
525                         new String[]{Favorites.APPWIDGET_ID},
526                         "appWidgetId=?", new String[]{oldWidgetId}, null, null, null)) {
527                     if (!cursor.moveToFirst()) {
528                         // The widget no long exists.
529                         FileLog.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: "
530                                 + oldWidgetId);
531                         host.deleteAppWidgetId(newWidgetIds[i]);
532                         launcherRestoreEventLogger.logSingleFavoritesItemRestoreFailed(
533                                 ITEM_TYPE_APPWIDGET,
534                                 RestoreError.WIDGET_REMOVED
535                         );
536                     }
537                 }
538             }
539         }
540 
541         logFavoritesTable(controller.getDb(), "launcher db after remap widget ids", null, null);
542         LauncherAppState.INSTANCE.get(context).getModel().reloadIfActive();
543     }
544 
logDatabaseWidgetInfo(ModelDbController controller)545     private static void logDatabaseWidgetInfo(ModelDbController controller) {
546         try (Cursor cursor = controller.getDb().query(Favorites.TABLE_NAME,
547                 new String[]{Favorites.APPWIDGET_ID, Favorites.RESTORED, Favorites.PROFILE_ID},
548                 Favorites.APPWIDGET_ID + "!=" + LauncherAppWidgetInfo.NO_ID, null,
549                 null, null, null)) {
550             IntArray widgetIdList = new IntArray();
551             IntArray widgetRestoreList = new IntArray();
552             IntArray widgetProfileIdList = new IntArray();
553 
554             if (cursor.moveToFirst()) {
555                 final int widgetIdColumnIndex = cursor.getColumnIndex(Favorites.APPWIDGET_ID);
556                 final int widgetRestoredColumnIndex = cursor.getColumnIndex(Favorites.RESTORED);
557                 final int widgetProfileIdIndex = cursor.getColumnIndex(Favorites.PROFILE_ID);
558                 while (!cursor.isAfterLast()) {
559                     int widgetId = cursor.getInt(widgetIdColumnIndex);
560                     int widgetRestoredFlag = cursor.getInt(widgetRestoredColumnIndex);
561                     int widgetProfileId = cursor.getInt(widgetProfileIdIndex);
562 
563                     widgetIdList.add(widgetId);
564                     widgetRestoreList.add(widgetRestoredFlag);
565                     widgetProfileIdList.add(widgetProfileId);
566                     cursor.moveToNext();
567                 }
568             }
569 
570             StringBuilder builder = new StringBuilder();
571             builder.append("[");
572             for (int i = 0; i < widgetIdList.size(); i++) {
573                 builder.append("[appWidgetId=")
574                         .append(widgetIdList.get(i))
575                         .append(", restoreFlag=")
576                         .append(widgetRestoreList.get(i))
577                         .append(", profileId=")
578                         .append(widgetProfileIdList.get(i))
579                         .append("]");
580             }
581             builder.append("]");
582             Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: " + builder);
583         } catch (Exception ex) {
584             Log.e(TAG, "Getting widget ids from the database failed", ex);
585         }
586     }
587 
maybeOverrideShortcuts(Context context, ModelDbController controller, SQLiteDatabase db, long currentUser)588     protected static void maybeOverrideShortcuts(Context context, ModelDbController controller,
589             SQLiteDatabase db, long currentUser) {
590         Map<String, LauncherActivityInfo> activityOverrides =
591                 ApiWrapper.INSTANCE.get(context).getActivityOverrides();
592         if (activityOverrides == null || activityOverrides.isEmpty()) {
593             return;
594         }
595 
596         try (Cursor c = db.query(Favorites.TABLE_NAME,
597                 new String[]{Favorites._ID, Favorites.INTENT},
598                 String.format("%s=? AND %s=? AND ( %s )", Favorites.ITEM_TYPE, Favorites.PROFILE_ID,
599                         getTelephonyIntentSQLLiteSelection(activityOverrides.keySet())),
600                 new String[]{String.valueOf(ITEM_TYPE_APPLICATION), String.valueOf(currentUser)},
601                 null, null, null);
602              SQLiteTransaction t = new SQLiteTransaction(db)) {
603             final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
604             final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
605             while (c.moveToNext()) {
606                 LauncherActivityInfo override = activityOverrides.get(Intent.parseUri(
607                         c.getString(intentIndex), 0).getComponent().getPackageName());
608                 if (override != null) {
609                     ContentValues values = new ContentValues();
610                     values.put(Favorites.PROFILE_ID,
611                             controller.getSerialNumberForUser(override.getUser()));
612                     values.put(Favorites.INTENT, AppInfo.makeLaunchIntent(override).toUri(0));
613                     db.update(Favorites.TABLE_NAME, values, String.format("%s=?", Favorites._ID),
614                             new String[]{String.valueOf(c.getInt(idIndex))});
615                 }
616             }
617             t.commit();
618         } catch (Exception ex) {
619             Log.e(TAG, "Error while overriding shortcuts", ex);
620         }
621     }
622 
getTelephonyIntentSQLLiteSelection(Collection<String> packages)623     private static String getTelephonyIntentSQLLiteSelection(Collection<String> packages) {
624         return packages.stream().map(
625                 packageToChange -> String.format("intent LIKE '%%' || '%s' || '%%' ",
626                         packageToChange)).collect(
627                 Collectors.joining(" OR "));
628     }
629 
630     /**
631      * Queries and logs the items from the Favorites table in the launcher db.
632      * This is to understand why items might be missing during the restore process for Launcher.
633      * @param database The Launcher db to query from.
634      * @param logHeader First line in log statement, used to explain what is being logged.
635      * @param where The SELECT statement to query items.
636      * @param profileIds The profile ID's for each user profile.
637      */
logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader, String where, String[] profileIds)638     public static void logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader,
639             String where, String[] profileIds) {
640         try (Cursor cursor = database.query(
641                 /* table */ Favorites.TABLE_NAME,
642                 /* columns */ DB_COLUMNS_TO_LOG,
643                 /* selection */ where,
644                 /* selection args */ profileIds,
645                 /* groupBy */ null,
646                 /* having */ null,
647                 /* orderBy */ null
648         )) {
649             if (cursor.moveToFirst()) {
650                 String[] columnNames = cursor.getColumnNames();
651                 StringBuilder stringBuilder = new StringBuilder(logHeader + "\n");
652                 do {
653                     for (String columnName : columnNames) {
654                         stringBuilder.append(columnName)
655                                 .append("=")
656                                 .append(cursor.getString(
657                                         cursor.getColumnIndex(columnName)))
658                                 .append(" ");
659                     }
660                     stringBuilder.append("\n");
661                 } while (cursor.moveToNext());
662                 FileLog.d(TAG, stringBuilder.toString());
663             } else {
664                 FileLog.d(TAG, "logFavoritesTable: No items found from query for "
665                         + "\"" + logHeader + "\"");
666             }
667         } catch (Exception e) {
668             FileLog.e(TAG, "logFavoritesTable: Error reading from database", e);
669         }
670     }
671 
672 
673     /**
674      * Queries and reports the count of each itemType to be removed due to unrestored profiles.
675      * @param database The Launcher db to query from.
676      * @param where Query being used for to find unrestored profiles
677      * @param profileIds profile ids that were not restored
678      * @param restoreEventLogger Backup/Restore Logger to report metrics
679      */
reportUnrestoredProfiles(SQLiteDatabase database, String where, String[] profileIds, LauncherRestoreEventLogger restoreEventLogger)680     private void reportUnrestoredProfiles(SQLiteDatabase database, String where,
681             String[] profileIds, LauncherRestoreEventLogger restoreEventLogger) {
682         final String query = "SELECT itemType, COUNT(*) AS count FROM favorites WHERE "
683                 + where + " GROUP BY itemType";
684         try (Cursor cursor = database.rawQuery(query, profileIds)) {
685             if (cursor.moveToFirst()) {
686                 do {
687                     restoreEventLogger.logFavoritesItemsRestoreFailed(
688                             cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)),
689                             cursor.getInt(cursor.getColumnIndexOrThrow("count")),
690                             RestoreError.PROFILE_NOT_RESTORED
691                     );
692                 } while (cursor.moveToNext());
693             }
694         } catch (Exception e) {
695             FileLog.e(TAG, "reportUnrestoredProfiles: Error reading from database", e);
696         }
697     }
698 }
699