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