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