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