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