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