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