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