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