1 /*
2  * Copyright (C) 2020 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.model;
18 
19 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
20 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
21 import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
22 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
23 import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
24 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
25 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
26 
27 import android.content.ComponentName;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.database.Cursor;
34 import android.database.DatabaseUtils;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.graphics.Point;
37 import android.util.ArrayMap;
38 import android.util.Log;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.launcher3.Flags;
44 import com.android.launcher3.InvariantDeviceProfile;
45 import com.android.launcher3.LauncherPrefs;
46 import com.android.launcher3.LauncherSettings;
47 import com.android.launcher3.Utilities;
48 import com.android.launcher3.config.FeatureFlags;
49 import com.android.launcher3.model.data.ItemInfo;
50 import com.android.launcher3.pm.InstallSessionHelper;
51 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
52 import com.android.launcher3.util.ContentWriter;
53 import com.android.launcher3.util.GridOccupancy;
54 import com.android.launcher3.util.IntArray;
55 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
56 import com.android.launcher3.widget.WidgetManagerHelper;
57 
58 import java.net.URISyntaxException;
59 import java.util.ArrayList;
60 import java.util.Collections;
61 import java.util.HashMap;
62 import java.util.HashSet;
63 import java.util.Iterator;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Objects;
67 import java.util.Set;
68 import java.util.stream.Collectors;
69 
70 /**
71  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
72  * result of restoring from a larger device or device density change.
73  */
74 public class GridSizeMigrationUtil {
75 
76     private static final String TAG = "GridSizeMigrationUtil";
77     private static final boolean DEBUG = true;
78 
GridSizeMigrationUtil()79     private GridSizeMigrationUtil() {
80         // Util class should not be instantiated
81     }
82 
83     /**
84      * Check given a new IDP, if migration is necessary.
85      */
needsToMigrate(Context context, InvariantDeviceProfile idp)86     public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
87         return needsToMigrate(new DeviceGridState(context), new DeviceGridState(idp));
88     }
89 
needsToMigrate( DeviceGridState srcDeviceState, DeviceGridState destDeviceState)90     private static boolean needsToMigrate(
91             DeviceGridState srcDeviceState, DeviceGridState destDeviceState) {
92         boolean needsToMigrate = !destDeviceState.isCompatible(srcDeviceState);
93         if (needsToMigrate) {
94             Log.i(TAG, "Migration is needed. destDeviceState: " + destDeviceState
95                     + ", srcDeviceState: " + srcDeviceState);
96         }
97         return needsToMigrate;
98     }
99 
100     @VisibleForTesting
readAllEntries(SQLiteDatabase db, String tableName, Context context)101     public static List<DbEntry> readAllEntries(SQLiteDatabase db, String tableName,
102             Context context) {
103         DbReader dbReader = new DbReader(db, tableName, context, getValidPackages(context));
104         List<DbEntry> result = dbReader.loadAllWorkspaceEntries();
105         result.addAll(dbReader.loadHotseatEntries());
106         return result;
107     }
108 
109     /**
110      * When migrating the grid, we copy the table
111      * {@link LauncherSettings.Favorites#TABLE_NAME} from {@code source} into
112      * {@link LauncherSettings.Favorites#TMP_TABLE}, run the grid size migration algorithm
113      * to migrate the later to the former, and load the workspace from the default
114      * {@link LauncherSettings.Favorites#TABLE_NAME}.
115      *
116      * @return false if the migration failed.
117      */
migrateGridIfNeeded( @onNull Context context, @NonNull DeviceGridState srcDeviceState, @NonNull DeviceGridState destDeviceState, @NonNull DatabaseHelper target, @NonNull SQLiteDatabase source)118     public static boolean migrateGridIfNeeded(
119             @NonNull Context context,
120             @NonNull DeviceGridState srcDeviceState,
121             @NonNull DeviceGridState destDeviceState,
122             @NonNull DatabaseHelper target,
123             @NonNull SQLiteDatabase source) {
124         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
125             return true;
126         }
127 
128         if (Flags.enableGridMigrationFix()
129                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
130                 && srcDeviceState.getRows() < destDeviceState.getRows()) {
131             // Only use this strategy when comparing the previous grid to the new grid and the
132             // columns are the same and the destination has more rows
133             copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
134             destDeviceState.writeToPrefs(context);
135             return true;
136         }
137         copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
138 
139         HashSet<String> validPackages = getValidPackages(context);
140         long migrationStartTime = System.currentTimeMillis();
141         try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) {
142             DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context, validPackages);
143             DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context, validPackages);
144 
145             Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
146             migrate(target, srcReader, destReader, destDeviceState.getNumHotseat(),
147                     targetSize, srcDeviceState, destDeviceState);
148             dropTable(t.getDb(), TMP_TABLE);
149             t.commit();
150             return true;
151         } catch (Exception e) {
152             Log.e(TAG, "Error during grid migration", e);
153 
154             return false;
155         } finally {
156             Log.v(TAG, "Workspace migration completed in "
157                     + (System.currentTimeMillis() - migrationStartTime));
158 
159             // Save current configuration, so that the migration does not run again.
160             destDeviceState.writeToPrefs(context);
161         }
162     }
163 
migrate( @onNull DatabaseHelper helper, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, final int destHotseatSize, @NonNull final Point targetSize, @NonNull final DeviceGridState srcDeviceState, @NonNull final DeviceGridState destDeviceState)164     public static boolean migrate(
165             @NonNull DatabaseHelper helper,
166             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
167             final int destHotseatSize, @NonNull final Point targetSize,
168             @NonNull final DeviceGridState srcDeviceState,
169             @NonNull final DeviceGridState destDeviceState) {
170 
171         final List<DbEntry> srcHotseatItems = srcReader.loadHotseatEntries();
172         final List<DbEntry> srcWorkspaceItems = srcReader.loadAllWorkspaceEntries();
173         final List<DbEntry> dstHotseatItems = destReader.loadHotseatEntries();
174         final List<DbEntry> dstWorkspaceItems = destReader.loadAllWorkspaceEntries();
175         final List<DbEntry> hotseatToBeAdded = new ArrayList<>(1);
176         final List<DbEntry> workspaceToBeAdded = new ArrayList<>(1);
177         final IntArray toBeRemoved = new IntArray();
178 
179         calcDiff(srcHotseatItems, dstHotseatItems, hotseatToBeAdded, toBeRemoved);
180         calcDiff(srcWorkspaceItems, dstWorkspaceItems, workspaceToBeAdded, toBeRemoved);
181 
182         final int trgX = targetSize.x;
183         final int trgY = targetSize.y;
184 
185         if (DEBUG) {
186             Log.d(TAG, "Start migration:"
187                     + "\n Source Device:"
188                     + srcWorkspaceItems.stream().map(DbEntry::toString).collect(
189                     Collectors.joining(",\n", "[", "]"))
190                     + "\n Target Device:"
191                     + dstWorkspaceItems.stream().map(DbEntry::toString).collect(
192                     Collectors.joining(",\n", "[", "]"))
193                     + "\n Removing Items:"
194                     + dstWorkspaceItems.stream().filter(entry ->
195                             toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
196                     Collectors.joining(",\n", "[", "]"))
197                     + "\n Adding Workspace Items:"
198                     + workspaceToBeAdded.stream().map(DbEntry::toString).collect(
199                     Collectors.joining(",\n", "[", "]"))
200                     + "\n Adding Hotseat Items:"
201                     + hotseatToBeAdded.stream().map(DbEntry::toString).collect(
202                     Collectors.joining(",\n", "[", "]"))
203             );
204         }
205         if (!toBeRemoved.isEmpty()) {
206             removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
207         }
208         if (hotseatToBeAdded.isEmpty() && workspaceToBeAdded.isEmpty()) {
209             return false;
210         }
211 
212         // Sort the items by the reading order.
213         Collections.sort(hotseatToBeAdded);
214         Collections.sort(workspaceToBeAdded);
215 
216         // Migrate hotseat
217         solveHotseatPlacement(helper, destHotseatSize,
218                 srcReader, destReader, dstHotseatItems, hotseatToBeAdded);
219 
220         // Migrate workspace.
221         // First we create a collection of the screens
222         List<Integer> screens = new ArrayList<>();
223         for (int screenId = 0; screenId <= destReader.mLastScreenId; screenId++) {
224             screens.add(screenId);
225         }
226 
227         // Then we place the items on the screens
228         for (int screenId : screens) {
229             if (DEBUG) {
230                 Log.d(TAG, "Migrating " + screenId);
231             }
232             solveGridPlacement(helper, srcReader,
233                     destReader, screenId, trgX, trgY, workspaceToBeAdded);
234             if (workspaceToBeAdded.isEmpty()) {
235                 break;
236             }
237         }
238 
239         // In case the new grid is smaller, there might be some leftover items that don't fit on
240         // any of the screens, in this case we add them to new screens until all of them are placed.
241         int screenId = destReader.mLastScreenId + 1;
242         while (!workspaceToBeAdded.isEmpty()) {
243             solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
244                     workspaceToBeAdded);
245             screenId++;
246         }
247 
248         return true;
249     }
250 
251     /**
252      * Calculate the differences between {@code src} (denoted by A) and {@code dest}
253      * (denoted by B).
254      * All DbEntry in A - B will be added to {@code toBeAdded}
255      * All DbEntry.id in B - A will be added to {@code toBeRemoved}
256      */
calcDiff(@onNull final List<DbEntry> src, @NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded, @NonNull final IntArray toBeRemoved)257     private static void calcDiff(@NonNull final List<DbEntry> src,
258             @NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded,
259             @NonNull final IntArray toBeRemoved) {
260         src.forEach(entry -> {
261             if (!dest.contains(entry)) {
262                 toBeAdded.add(entry);
263             }
264         });
265         dest.forEach(entry -> {
266             if (!src.contains(entry)) {
267                 toBeRemoved.add(entry.id);
268                 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
269                     entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
270                 }
271             }
272         });
273     }
274 
insertEntryInDb(DatabaseHelper helper, DbEntry entry, String srcTableName, String destTableName)275     private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
276             String srcTableName, String destTableName) {
277         int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
278 
279         if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER
280                 || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
281             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
282                 for (int itemId : itemIds) {
283                     copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
284                 }
285             }
286         }
287     }
288 
copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, String srcTableName, String destTableName)289     private static int copyEntryAndUpdate(DatabaseHelper helper,
290             DbEntry entry, String srcTableName, String destTableName) {
291         return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName);
292     }
293 
copyEntryAndUpdate(DatabaseHelper helper, int id, int folderId, String srcTableName, String destTableName)294     private static int copyEntryAndUpdate(DatabaseHelper helper,
295             int id, int folderId, String srcTableName, String destTableName) {
296         return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName);
297     }
298 
copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry, int id, int folderId, String srcTableName, String destTableName)299     private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry,
300             int id, int folderId, String srcTableName, String destTableName) {
301         int newId = -1;
302         Cursor c = helper.getWritableDatabase().query(srcTableName, null,
303                 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
304                 null, null, null, null);
305         while (c.moveToNext()) {
306             ContentValues values = new ContentValues();
307             DatabaseUtils.cursorRowToContentValues(c, values);
308             if (entry != null) {
309                 entry.updateContentValues(values);
310             } else {
311                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
312             }
313             newId = helper.generateNewItemId();
314             values.put(LauncherSettings.Favorites._ID, newId);
315             helper.getWritableDatabase().insert(destTableName, null, values);
316         }
317         c.close();
318         return newId;
319     }
320 
removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds)321     private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
322         db.delete(tableName,
323                 Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null);
324     }
325 
getValidPackages(Context context)326     private static HashSet<String> getValidPackages(Context context) {
327         // Initialize list of valid packages. This contain all the packages which are already on
328         // the device and packages which are being installed. Any item which doesn't belong to
329         // this set is removed.
330         // Since the loader removes such items anyway, removing these items here doesn't cause
331         // any extra data loss and gives us more free space on the grid for better migration.
332         HashSet<String> validPackages = new HashSet<>();
333         for (PackageInfo info : context.getPackageManager()
334                 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
335             validPackages.add(info.packageName);
336         }
337         InstallSessionHelper.INSTANCE.get(context)
338                 .getActiveSessions().keySet()
339                 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
340         return validPackages;
341     }
342 
solveGridPlacement(@onNull final DatabaseHelper helper, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, final int screenId, final int trgX, final int trgY, @NonNull final List<DbEntry> sortedItemsToPlace)343     private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
344             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
345             final int screenId, final int trgX, final int trgY,
346             @NonNull final List<DbEntry> sortedItemsToPlace) {
347         final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
348         final Point trg = new Point(trgX, trgY);
349         final Point next = new Point(0, screenId == 0
350                 && (FeatureFlags.QSB_ON_FIRST_SCREEN
351                 && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(destReader.mContext)
352                 .getBoolean(SMARTSPACE_ON_HOME_SCREEN, true))
353                 && !SHOULD_SHOW_FIRST_PAGE_WIDGET)
354                 ? 1 /* smartspace */ : 0);
355         List<DbEntry> existedEntries = destReader.mWorkspaceEntriesByScreenId.get(screenId);
356         if (existedEntries != null) {
357             for (DbEntry entry : existedEntries) {
358                 occupied.markCells(entry, true);
359             }
360         }
361         Iterator<DbEntry> iterator = sortedItemsToPlace.iterator();
362         while (iterator.hasNext()) {
363             final DbEntry entry = iterator.next();
364             if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
365                 iterator.remove();
366                 continue;
367             }
368             if (findPlacementForEntry(entry, next, trg, occupied, screenId)) {
369                 insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
370                 iterator.remove();
371             }
372         }
373     }
374 
375     /**
376      * Search for the next possible placement of an icon. (mNextStartX, mNextStartY) serves as
377      * a memoization of last placement, we can start our search for next placement from there
378      * to speed up the search.
379      */
findPlacementForEntry(@onNull final DbEntry entry, @NonNull final Point next, @NonNull final Point trg, @NonNull final GridOccupancy occupied, final int screenId)380     private static boolean findPlacementForEntry(@NonNull final DbEntry entry,
381             @NonNull final Point next, @NonNull final Point trg,
382             @NonNull final GridOccupancy occupied, final int screenId) {
383         for (int y = next.y; y <  trg.y; y++) {
384             for (int x = next.x; x < trg.x; x++) {
385                 boolean fits = occupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
386                 boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX,
387                         entry.minSpanY);
388                 if (minFits) {
389                     entry.spanX = entry.minSpanX;
390                     entry.spanY = entry.minSpanY;
391                 }
392                 if (fits || minFits) {
393                     entry.screenId = screenId;
394                     entry.cellX = x;
395                     entry.cellY = y;
396                     occupied.markCells(entry, true);
397                     next.set(x + entry.spanX, y);
398                     return true;
399                 }
400             }
401             next.set(0, next.y);
402         }
403         return false;
404     }
405 
solveHotseatPlacement( @onNull final DatabaseHelper helper, final int hotseatSize, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, @NonNull final List<DbEntry> placedHotseatItems, @NonNull final List<DbEntry> itemsToPlace)406     private static void solveHotseatPlacement(
407             @NonNull final DatabaseHelper helper, final int hotseatSize,
408             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
409             @NonNull final  List<DbEntry> placedHotseatItems,
410             @NonNull final List<DbEntry> itemsToPlace) {
411 
412         final boolean[] occupied = new boolean[hotseatSize];
413         for (DbEntry entry : placedHotseatItems) {
414             occupied[entry.screenId] = true;
415         }
416 
417         for (int i = 0; i < occupied.length; i++) {
418             if (!occupied[i] && !itemsToPlace.isEmpty()) {
419                 DbEntry entry = itemsToPlace.remove(0);
420                 entry.screenId = i;
421                 // These values does not affect the item position, but we should set them
422                 // to something other than -1.
423                 entry.cellX = i;
424                 entry.cellY = 0;
425                 insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
426                 occupied[entry.screenId] = true;
427             }
428         }
429     }
430 
431     @VisibleForTesting
432     public static class DbReader {
433 
434         private final SQLiteDatabase mDb;
435         private final String mTableName;
436         private final Context mContext;
437         private final Set<String> mValidPackages;
438         private int mLastScreenId = -1;
439 
440         private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
441                 new ArrayMap<>();
442 
DbReader(SQLiteDatabase db, String tableName, Context context, Set<String> validPackages)443         public DbReader(SQLiteDatabase db, String tableName, Context context,
444                 Set<String> validPackages) {
445             mDb = db;
446             mTableName = tableName;
447             mContext = context;
448             mValidPackages = validPackages;
449         }
450 
loadHotseatEntries()451         protected List<DbEntry> loadHotseatEntries() {
452             final List<DbEntry> hotseatEntries = new ArrayList<>();
453             Cursor c = queryWorkspace(
454                     new String[]{
455                             LauncherSettings.Favorites._ID,                  // 0
456                             LauncherSettings.Favorites.ITEM_TYPE,            // 1
457                             LauncherSettings.Favorites.INTENT,               // 2
458                             LauncherSettings.Favorites.SCREEN},              // 3
459                     LauncherSettings.Favorites.CONTAINER + " = "
460                             + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
461 
462             final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
463             final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
464             final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
465             final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
466 
467             IntArray entriesToRemove = new IntArray();
468             while (c.moveToNext()) {
469                 DbEntry entry = new DbEntry();
470                 entry.id = c.getInt(indexId);
471                 entry.itemType = c.getInt(indexItemType);
472                 entry.screenId = c.getInt(indexScreen);
473 
474                 try {
475                     // calculate weight
476                     switch (entry.itemType) {
477                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
478                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
479                             entry.mIntent = c.getString(indexIntent);
480                             verifyIntent(c.getString(indexIntent));
481                             break;
482                         }
483                         case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
484                             int total = getFolderItemsCount(entry);
485                             if (total == 0) {
486                                 throw new Exception("Folder is empty");
487                             }
488                             break;
489                         }
490                         case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
491                             int total = getFolderItemsCount(entry);
492                             if (total != 2) {
493                                 throw new Exception("App pair contains fewer or more than 2 items");
494                             }
495                             break;
496                         }
497                         default:
498                             throw new Exception("Invalid item type");
499                     }
500                 } catch (Exception e) {
501                     if (DEBUG) {
502                         Log.d(TAG, "Removing item " + entry.id, e);
503                     }
504                     entriesToRemove.add(entry.id);
505                     continue;
506                 }
507                 hotseatEntries.add(entry);
508             }
509             removeEntryFromDb(mDb, mTableName, entriesToRemove);
510             c.close();
511             return hotseatEntries;
512         }
513 
loadAllWorkspaceEntries()514         protected List<DbEntry> loadAllWorkspaceEntries() {
515             final List<DbEntry> workspaceEntries = new ArrayList<>();
516             Cursor c = queryWorkspace(
517                     new String[]{
518                             LauncherSettings.Favorites._ID,                  // 0
519                             LauncherSettings.Favorites.ITEM_TYPE,            // 1
520                             LauncherSettings.Favorites.SCREEN,               // 2
521                             LauncherSettings.Favorites.CELLX,                // 3
522                             LauncherSettings.Favorites.CELLY,                // 4
523                             LauncherSettings.Favorites.SPANX,                // 5
524                             LauncherSettings.Favorites.SPANY,                // 6
525                             LauncherSettings.Favorites.INTENT,               // 7
526                             LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
527                             LauncherSettings.Favorites.APPWIDGET_ID},        // 9
528                         LauncherSettings.Favorites.CONTAINER + " = "
529                             + LauncherSettings.Favorites.CONTAINER_DESKTOP);
530             final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
531             final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
532             final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
533             final int indexCellX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
534             final int indexCellY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
535             final int indexSpanX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
536             final int indexSpanY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
537             final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
538             final int indexAppWidgetProvider = c.getColumnIndexOrThrow(
539                     LauncherSettings.Favorites.APPWIDGET_PROVIDER);
540             final int indexAppWidgetId = c.getColumnIndexOrThrow(
541                     LauncherSettings.Favorites.APPWIDGET_ID);
542 
543             IntArray entriesToRemove = new IntArray();
544             WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
545             while (c.moveToNext()) {
546                 DbEntry entry = new DbEntry();
547                 entry.id = c.getInt(indexId);
548                 entry.itemType = c.getInt(indexItemType);
549                 entry.screenId = c.getInt(indexScreen);
550                 mLastScreenId = Math.max(mLastScreenId, entry.screenId);
551                 entry.cellX = c.getInt(indexCellX);
552                 entry.cellY = c.getInt(indexCellY);
553                 entry.spanX = c.getInt(indexSpanX);
554                 entry.spanY = c.getInt(indexSpanY);
555 
556                 try {
557                     // calculate weight
558                     switch (entry.itemType) {
559                         case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
560                         case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
561                             entry.mIntent = c.getString(indexIntent);
562                             verifyIntent(entry.mIntent);
563                             break;
564                         }
565                         case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
566                             entry.mProvider = c.getString(indexAppWidgetProvider);
567                             ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
568                             verifyPackage(cn.getPackageName());
569 
570                             int widgetId = c.getInt(indexAppWidgetId);
571                             LauncherAppWidgetProviderInfo pInfo = widgetManagerHelper
572                                     .getLauncherAppWidgetInfo(widgetId, cn);
573                             Point spans = null;
574                             if (pInfo != null) {
575                                 spans = pInfo.getMinSpans();
576                             }
577                             if (spans != null) {
578                                 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
579                                 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
580                             } else {
581                                 // Assume that the widget be resized down to 2x2
582                                 entry.minSpanX = entry.minSpanY = 2;
583                             }
584 
585                             break;
586                         }
587                         case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
588                             int total = getFolderItemsCount(entry);
589                             if (total == 0) {
590                                 throw new Exception("Folder is empty");
591                             }
592                             break;
593                         }
594                         case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
595                             int total = getFolderItemsCount(entry);
596                             if (total != 2) {
597                                 throw new Exception("App pair contains fewer or more than 2 items");
598                             }
599                             break;
600                         }
601                         default:
602                             throw new Exception("Invalid item type");
603                     }
604                 } catch (Exception e) {
605                     if (DEBUG) {
606                         Log.d(TAG, "Removing item " + entry.id, e);
607                     }
608                     entriesToRemove.add(entry.id);
609                     continue;
610                 }
611                 workspaceEntries.add(entry);
612                 if (!mWorkspaceEntriesByScreenId.containsKey(entry.screenId)) {
613                     mWorkspaceEntriesByScreenId.put(entry.screenId, new ArrayList<>());
614                 }
615                 mWorkspaceEntriesByScreenId.get(entry.screenId).add(entry);
616             }
617             removeEntryFromDb(mDb, mTableName, entriesToRemove);
618             c.close();
619             return workspaceEntries;
620         }
621 
getFolderItemsCount(DbEntry entry)622         private int getFolderItemsCount(DbEntry entry) {
623             Cursor c = queryWorkspace(
624                     new String[]{LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT},
625                     LauncherSettings.Favorites.CONTAINER + " = " + entry.id);
626 
627             int total = 0;
628             while (c.moveToNext()) {
629                 try {
630                     int id = c.getInt(0);
631                     String intent = c.getString(1);
632                     verifyIntent(intent);
633                     total++;
634                     if (!entry.mFolderItems.containsKey(intent)) {
635                         entry.mFolderItems.put(intent, new HashSet<>());
636                     }
637                     entry.mFolderItems.get(intent).add(id);
638                 } catch (Exception e) {
639                     removeEntryFromDb(mDb, mTableName, IntArray.wrap(c.getInt(0)));
640                 }
641             }
642             c.close();
643             return total;
644         }
645 
queryWorkspace(String[] columns, String where)646         private Cursor queryWorkspace(String[] columns, String where) {
647             return mDb.query(mTableName, columns, where, null, null, null, null);
648         }
649 
650         /** Verifies if the mIntent should be restored. */
verifyIntent(String intentStr)651         private void verifyIntent(String intentStr)
652                 throws Exception {
653             Intent intent = Intent.parseUri(intentStr, 0);
654             if (intent.getComponent() != null) {
655                 verifyPackage(intent.getComponent().getPackageName());
656             } else if (intent.getPackage() != null) {
657                 // Only verify package if the component was null.
658                 verifyPackage(intent.getPackage());
659             }
660         }
661 
662         /** Verifies if the package should be restored */
verifyPackage(String packageName)663         private void verifyPackage(String packageName)
664                 throws Exception {
665             if (!mValidPackages.contains(packageName)) {
666                 // TODO(b/151468819): Handle promise app icon restoration during grid migration.
667                 throw new Exception("Package not available");
668             }
669         }
670     }
671 
672     public static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
673 
674         private String mIntent;
675         private String mProvider;
676         private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
677 
678         /**
679          * Id of the specific widget.
680          */
681         public int appWidgetId = NO_ID;
682 
683         /** Comparator according to the reading order */
684         @Override
compareTo(DbEntry another)685         public int compareTo(DbEntry another) {
686             if (screenId != another.screenId) {
687                 return Integer.compare(screenId, another.screenId);
688             }
689             if (cellY != another.cellY) {
690                 return Integer.compare(cellY, another.cellY);
691             }
692             return Integer.compare(cellX, another.cellX);
693         }
694 
695         @Override
equals(Object o)696         public boolean equals(Object o) {
697             if (this == o) return true;
698             if (o == null || getClass() != o.getClass()) return false;
699             DbEntry entry = (DbEntry) o;
700             return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
701         }
702 
703         @Override
hashCode()704         public int hashCode() {
705             return Objects.hash(getEntryMigrationId());
706         }
707 
updateContentValues(ContentValues values)708         public void updateContentValues(ContentValues values) {
709             values.put(LauncherSettings.Favorites.SCREEN, screenId);
710             values.put(LauncherSettings.Favorites.CELLX, cellX);
711             values.put(LauncherSettings.Favorites.CELLY, cellY);
712             values.put(LauncherSettings.Favorites.SPANX, spanX);
713             values.put(LauncherSettings.Favorites.SPANY, spanY);
714         }
715 
716         @Override
writeToValues(@onNull ContentWriter writer)717         public void writeToValues(@NonNull ContentWriter writer) {
718             super.writeToValues(writer);
719             writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
720         }
721 
722         @Override
readFromValues(@onNull ContentValues values)723         public void readFromValues(@NonNull ContentValues values) {
724             super.readFromValues(values);
725             appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
726         }
727 
728         /** This id is not used in the DB is only used while doing the migration and it identifies
729          * an entry on each workspace. For example two calculator icons would have the same
730          * migration id even thought they have different database ids.
731          */
getEntryMigrationId()732         public String getEntryMigrationId() {
733             switch (itemType) {
734                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
735                 case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
736                     return getFolderMigrationId();
737                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
738                     // mProvider is the app the widget belongs to and appWidgetId it's the unique
739                     // is of the widget, we need both because if you remove a widget and then add it
740                     // again, then it can change and the WidgetProvider would not know the widget.
741                     return mProvider + appWidgetId;
742                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
743                     final String intentStr = cleanIntentString(mIntent);
744                     try {
745                         Intent i = Intent.parseUri(intentStr, 0);
746                         return Objects.requireNonNull(i.getComponent()).toString();
747                     } catch (Exception e) {
748                         return intentStr;
749                     }
750                 default:
751                     return cleanIntentString(mIntent);
752             }
753         }
754 
755         /**
756          * This method should return an id that should be the same for two folders containing the
757          * same elements.
758          */
759         @NonNull
getFolderMigrationId()760         private String getFolderMigrationId() {
761             return mFolderItems.keySet().stream()
762                     .map(intentString -> mFolderItems.get(intentString).size()
763                             + cleanIntentString(intentString))
764                     .sorted()
765                     .collect(Collectors.joining(","));
766         }
767 
768         /**
769          * This is needed because sourceBounds can change and make the id of two equal items
770          * different.
771          */
772         @NonNull
cleanIntentString(@onNull String intentStr)773         private String cleanIntentString(@NonNull String intentStr) {
774             try {
775                 Intent i = Intent.parseUri(intentStr, 0);
776                 i.setSourceBounds(null);
777                 return i.toURI();
778             } catch (URISyntaxException e) {
779                 Log.e(TAG, "Unable to parse Intent string", e);
780                 return intentStr;
781             }
782 
783         }
784     }
785 }
786