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