1 package com.android.launcher3.model; 2 3 import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT; 4 import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE; 5 import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE; 6 import static com.android.launcher3.Utilities.getPointString; 7 import static com.android.launcher3.Utilities.parsePoint; 8 import static com.android.launcher3.provider.LauncherDbUtils.copyTable; 9 10 import android.content.ComponentName; 11 import android.content.ContentValues; 12 import android.content.Context; 13 import android.content.Intent; 14 import android.content.SharedPreferences; 15 import android.content.pm.PackageInfo; 16 import android.content.pm.PackageManager; 17 import android.database.Cursor; 18 import android.database.sqlite.SQLiteDatabase; 19 import android.graphics.Point; 20 import android.os.SystemClock; 21 import android.util.Log; 22 import android.util.SparseArray; 23 24 import androidx.annotation.VisibleForTesting; 25 26 import com.android.launcher3.InvariantDeviceProfile; 27 import com.android.launcher3.LauncherAppState; 28 import com.android.launcher3.LauncherSettings; 29 import com.android.launcher3.LauncherSettings.Favorites; 30 import com.android.launcher3.LauncherSettings.Settings; 31 import com.android.launcher3.Utilities; 32 import com.android.launcher3.Workspace; 33 import com.android.launcher3.config.FeatureFlags; 34 import com.android.launcher3.graphics.LauncherPreviewRenderer; 35 import com.android.launcher3.model.data.ItemInfo; 36 import com.android.launcher3.pm.InstallSessionHelper; 37 import com.android.launcher3.provider.LauncherDbUtils; 38 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 39 import com.android.launcher3.util.GridOccupancy; 40 import com.android.launcher3.util.IntArray; 41 import com.android.launcher3.util.IntSparseArrayMap; 42 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 43 import com.android.launcher3.widget.WidgetManagerHelper; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.HashSet; 48 49 /** 50 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a 51 * result of restoring from a larger device or device density change. 52 */ 53 public class GridSizeMigrationTask { 54 55 private static final String TAG = "GridSizeMigrationTask"; 56 private static final boolean DEBUG = false; 57 58 // These are carefully selected weights for various item types (Math.random?), to allow for 59 // the least absurd migration experience. 60 private static final float WT_SHORTCUT = 1; 61 private static final float WT_APPLICATION = 0.8f; 62 private static final float WT_WIDGET_MIN = 2; 63 private static final float WT_WIDGET_FACTOR = 0.6f; 64 private static final float WT_FOLDER_FACTOR = 0.5f; 65 66 protected final SQLiteDatabase mDb; 67 protected final Context mContext; 68 69 protected final IntArray mEntryToRemove = new IntArray(); 70 protected final ArrayList<DbEntry> mCarryOver = new ArrayList<>(); 71 72 private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>(); 73 private final HashSet<String> mValidPackages; 74 private final String mTableName; 75 76 private final int mSrcX, mSrcY; 77 private final int mTrgX, mTrgY; 78 private final boolean mShouldRemoveX, mShouldRemoveY; 79 80 private final int mSrcHotseatSize; 81 private final int mDestHotseatSize; 82 GridSizeMigrationTask(Context context, SQLiteDatabase db, HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize, Point targetSize)83 protected GridSizeMigrationTask(Context context, SQLiteDatabase db, 84 HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize, 85 Point targetSize) { 86 mContext = context; 87 mDb = db; 88 mValidPackages = validPackages; 89 mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME; 90 91 mSrcX = sourceSize.x; 92 mSrcY = sourceSize.y; 93 94 mTrgX = targetSize.x; 95 mTrgY = targetSize.y; 96 97 mShouldRemoveX = mTrgX < mSrcX; 98 mShouldRemoveY = mTrgY < mSrcY; 99 100 // Non-used variables 101 mSrcHotseatSize = mDestHotseatSize = -1; 102 } 103 104 protected GridSizeMigrationTask(Context context, SQLiteDatabase db, 105 HashSet<String> validPackages, boolean usePreviewTable, int srcHotseatSize, 106 int destHotseatSize) { 107 mContext = context; 108 mDb = db; 109 mValidPackages = validPackages; 110 mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME; 111 112 mSrcHotseatSize = srcHotseatSize; 113 114 mDestHotseatSize = destHotseatSize; 115 116 // Non-used variables 117 mSrcX = mSrcY = mTrgX = mTrgY = -1; 118 mShouldRemoveX = mShouldRemoveY = false; 119 } 120 121 /** 122 * Applied all the pending DB operations 123 * 124 * @return true if any DB operation was commited. 125 */ 126 private boolean applyOperations() throws Exception { 127 // Update items 128 int updateCount = mUpdateOperations.size(); 129 for (int i = 0; i < updateCount; i++) { 130 mDb.update(mTableName, mUpdateOperations.valueAt(i), 131 "_id=" + mUpdateOperations.keyAt(i), null); 132 } 133 134 if (!mEntryToRemove.isEmpty()) { 135 if (DEBUG) { 136 Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString()); 137 } 138 mDb.delete(mTableName, Utilities.createDbSelectionQuery(Favorites._ID, mEntryToRemove), 139 null); 140 } 141 142 return updateCount > 0 || !mEntryToRemove.isEmpty(); 143 } 144 145 /** 146 * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them 147 * in the order in the new hotseat while keeping an empty space for all-apps. If the number of 148 * entries is more than what can fit in the new hotseat, we drop the entries with least weight. 149 * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION} 150 * & {@see #WT_FOLDER_FACTOR}. 151 * 152 * @return true if any DB change was made 153 */ 154 protected boolean migrateHotseat() throws Exception { 155 ArrayList<DbEntry> items = loadHotseatEntries(); 156 while (items.size() > mDestHotseatSize) { 157 // Pick the center item by default. 158 DbEntry toRemove = items.get(items.size() / 2); 159 160 // Find the item with least weight. 161 for (DbEntry entry : items) { 162 if (entry.weight < toRemove.weight) { 163 toRemove = entry; 164 } 165 } 166 167 mEntryToRemove.add(toRemove.id); 168 items.remove(toRemove); 169 } 170 171 // Update screen IDS 172 int newScreenId = 0; 173 for (DbEntry entry : items) { 174 if (entry.screenId != newScreenId) { 175 entry.screenId = newScreenId; 176 177 // These values does not affect the item position, but we should set them 178 // to something other than -1. 179 entry.cellX = newScreenId; 180 entry.cellY = 0; 181 182 update(entry); 183 } 184 185 newScreenId++; 186 } 187 188 return applyOperations(); 189 } 190 191 @VisibleForTesting 192 static IntArray getWorkspaceScreenIds(SQLiteDatabase db, String tableName) { 193 return LauncherDbUtils.queryIntArray(db, tableName, Favorites.SCREEN, 194 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP, 195 Favorites.SCREEN, Favorites.SCREEN); 196 } 197 198 /** 199 * @return true if any DB change was made 200 */ 201 protected boolean migrateWorkspace() throws Exception { 202 IntArray allScreens = getWorkspaceScreenIds(mDb, mTableName); 203 if (allScreens.isEmpty()) { 204 throw new Exception("Unable to get workspace screens"); 205 } 206 207 for (int i = 0; i < allScreens.size(); i++) { 208 int screenId = allScreens.get(i); 209 if (DEBUG) { 210 Log.d(TAG, "Migrating " + screenId); 211 } 212 migrateScreen(screenId); 213 } 214 215 if (!mCarryOver.isEmpty()) { 216 IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>(); 217 for (DbEntry e : mCarryOver) { 218 itemMap.put(e.id, e); 219 } 220 221 do { 222 // Some items are still remaining. Try adding a few new screens. 223 224 // At every iteration, make sure that at least one item is removed from 225 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed, 226 // break the loop and abort migration by throwing an exception. 227 OptimalPlacementSolution placement = new OptimalPlacementSolution( 228 new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true); 229 placement.find(); 230 if (placement.finalPlacedItems.size() > 0) { 231 int newScreenId = LauncherSettings.Settings.call( 232 mContext.getContentResolver(), 233 LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) 234 .getInt(EXTRA_VALUE); 235 for (DbEntry item : placement.finalPlacedItems) { 236 if (!mCarryOver.remove(itemMap.get(item.id))) { 237 throw new Exception("Unable to find matching items"); 238 } 239 item.screenId = newScreenId; 240 update(item); 241 } 242 } else { 243 throw new Exception("None of the items can be placed on an empty screen"); 244 } 245 246 } while (!mCarryOver.isEmpty()); 247 } 248 return applyOperations(); 249 } 250 251 /** 252 * Migrate a particular screen id. 253 * Strategy: 254 * 1) For all possible combinations of row and column, pick the one which causes the least 255 * data loss: {@link #tryRemove(int, int, int, ArrayList, float[])} 256 * 2) Maintain a list of all lost items before this screen, and add any new item lost from 257 * this screen to that list as well. 258 * 3) If all those items from the above list can be placed on this screen, place them 259 * (otherwise they are placed on a new screen). 260 */ 261 protected void migrateScreen(int screenId) { 262 // If we are migrating the first screen, do not touch the first row. 263 int startY = (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID) 264 ? 1 : 0; 265 266 ArrayList<DbEntry> items = loadWorkspaceEntries(screenId); 267 268 int removedCol = Integer.MAX_VALUE; 269 int removedRow = Integer.MAX_VALUE; 270 271 // removeWt represents the cost function for loss of items during migration, and moveWt 272 // represents the cost function for repositioning the items. moveWt is only considered if 273 // removeWt is same for two different configurations. 274 // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least 275 // cost. 276 float removeWt = Float.MAX_VALUE; 277 float moveWt = Float.MAX_VALUE; 278 float[] outLoss = new float[2]; 279 ArrayList<DbEntry> finalItems = null; 280 281 // Try removing all possible combinations 282 for (int x = 0; x < mSrcX; x++) { 283 // Try removing the rows first from bottom. This keeps the workspace 284 // nicely aligned with hotseat. 285 for (int y = mSrcY - 1; y >= startY; y--) { 286 // Use a deep copy when trying out a particular combination as it can change 287 // the underlying object. 288 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), 289 outLoss); 290 291 if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] 292 < moveWt))) { 293 removeWt = outLoss[0]; 294 moveWt = outLoss[1]; 295 removedCol = mShouldRemoveX ? x : removedCol; 296 removedRow = mShouldRemoveY ? y : removedRow; 297 finalItems = itemsOnScreen; 298 } 299 300 // No need to loop over all rows, if a row removal is not needed. 301 if (!mShouldRemoveY) { 302 break; 303 } 304 } 305 306 if (!mShouldRemoveX) { 307 break; 308 } 309 } 310 311 if (DEBUG) { 312 Log.d(TAG, String.format("Removing row %d, column %d on screen %d", 313 removedRow, removedCol, screenId)); 314 } 315 316 IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>(); 317 for (DbEntry e : deepCopy(items)) { 318 itemMap.put(e.id, e); 319 } 320 321 for (DbEntry item : finalItems) { 322 DbEntry org = itemMap.get(item.id); 323 itemMap.remove(item.id); 324 325 // Check if update is required 326 if (!item.columnsSame(org)) { 327 update(item); 328 } 329 } 330 331 // The remaining items in {@link #itemMap} are those which didn't get placed. 332 for (DbEntry item : itemMap) { 333 mCarryOver.add(item); 334 } 335 336 if (!mCarryOver.isEmpty() && removeWt == 0) { 337 // No new items were removed in this step. Try placing all the items on this screen. 338 GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY); 339 occupied.markCells(0, 0, mTrgX, startY, true); 340 for (DbEntry item : finalItems) { 341 occupied.markCells(item, true); 342 } 343 344 OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, 345 deepCopy(mCarryOver), startY, true); 346 placement.find(); 347 if (placement.lowestWeightLoss == 0) { 348 // All items got placed 349 350 for (DbEntry item : placement.finalPlacedItems) { 351 item.screenId = screenId; 352 update(item); 353 } 354 355 mCarryOver.clear(); 356 } 357 } 358 } 359 360 /** 361 * Updates an item in the DB. 362 */ 363 protected void update(DbEntry item) { 364 ContentValues values = new ContentValues(); 365 item.addToContentValues(values); 366 mUpdateOperations.put(item.id, values); 367 } 368 369 /** 370 * Tries the remove the provided row and column. 371 * 372 * @param items all the items on the screen under operation 373 * @param outLoss array of size 2. The first entry is filled with weight loss, and the second 374 * with the overall item movement. 375 */ 376 private ArrayList<DbEntry> tryRemove(int col, int row, int startY, 377 ArrayList<DbEntry> items, float[] outLoss) { 378 GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY); 379 occupied.markCells(0, 0, mTrgX, startY, true); 380 381 col = mShouldRemoveX ? col : Integer.MAX_VALUE; 382 row = mShouldRemoveY ? row : Integer.MAX_VALUE; 383 384 ArrayList<DbEntry> finalItems = new ArrayList<>(); 385 ArrayList<DbEntry> removedItems = new ArrayList<>(); 386 387 for (DbEntry item : items) { 388 if ((item.cellX <= col && (item.spanX + item.cellX) > col) 389 || (item.cellY <= row && (item.spanY + item.cellY) > row)) { 390 removedItems.add(item); 391 if (item.cellX >= col) item.cellX--; 392 if (item.cellY >= row) item.cellY--; 393 } else { 394 if (item.cellX > col) item.cellX--; 395 if (item.cellY > row) item.cellY--; 396 finalItems.add(item); 397 occupied.markCells(item, true); 398 } 399 } 400 401 OptimalPlacementSolution placement = 402 new OptimalPlacementSolution(occupied, removedItems, startY); placement.find()403 placement.find(); 404 finalItems.addAll(placement.finalPlacedItems); 405 outLoss[0] = placement.lowestWeightLoss; 406 outLoss[1] = placement.lowestMoveCost; 407 return finalItems; 408 } 409 410 private class OptimalPlacementSolution { 411 private final ArrayList<DbEntry> itemsToPlace; 412 private final GridOccupancy occupied; 413 414 // If set to true, item movement are not considered in move cost, leading to a more 415 // linear placement. 416 private final boolean ignoreMove; 417 418 // The first row in the grid from where the placement should start. 419 private final int startY; 420 421 float lowestWeightLoss = Float.MAX_VALUE; 422 float lowestMoveCost = Float.MAX_VALUE; 423 ArrayList<DbEntry> finalPlacedItems; 424 425 public OptimalPlacementSolution( 426 GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace, int startY) { 427 this(occupied, itemsToPlace, startY, false); 428 } 429 430 public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace, 431 int startY, boolean ignoreMove) { 432 this.occupied = occupied; 433 this.itemsToPlace = itemsToPlace; 434 this.ignoreMove = ignoreMove; 435 this.startY = startY; 436 437 // Sort the items such that larger widgets appear first followed by 1x1 items 438 Collections.sort(this.itemsToPlace); 439 } 440 441 public void find() { 442 find(0, 0, 0, new ArrayList<DbEntry>()); 443 } 444 445 /** 446 * Recursively finds a placement for the provided items. 447 * 448 * @param index the position in {@link #itemsToPlace} to start looking at. 449 * @param weightLoss total weight loss upto this point 450 * @param moveCost total move cost upto this point 451 * @param itemsPlaced all the items already placed upto this point 452 */ 453 public void find(int index, float weightLoss, float moveCost, 454 ArrayList<DbEntry> itemsPlaced) { 455 if ((weightLoss >= lowestWeightLoss) || 456 ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) { 457 // Abort, as we already have a better solution. 458 return; 459 460 } else if (index >= itemsToPlace.size()) { 461 // End loop. 462 lowestWeightLoss = weightLoss; 463 lowestMoveCost = moveCost; 464 465 // Keep a deep copy of current configuration as it can change during recursion. 466 finalPlacedItems = deepCopy(itemsPlaced); 467 return; 468 } 469 470 DbEntry me = itemsToPlace.get(index); 471 int myX = me.cellX; 472 int myY = me.cellY; 473 474 // List of items to pass over if this item was placed. 475 ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1); 476 itemsIncludingMe.addAll(itemsPlaced); 477 itemsIncludingMe.add(me); 478 479 if (me.spanX > 1 || me.spanY > 1) { 480 // If the current item is a widget (and it greater than 1x1), try to place it at 481 // all possible positions. This is because a widget placed at one position can 482 // affect the placement of a different widget. 483 int myW = me.spanX; 484 int myH = me.spanY; 485 486 for (int y = startY; y < mTrgY; y++) { 487 for (int x = 0; x < mTrgX; x++) { 488 float newMoveCost = moveCost; 489 if (x != myX) { 490 me.cellX = x; 491 newMoveCost++; 492 } 493 if (y != myY) { 494 me.cellY = y; 495 newMoveCost++; 496 } 497 if (ignoreMove) { 498 newMoveCost = moveCost; 499 } 500 501 if (occupied.isRegionVacant(x, y, myW, myH)) { 502 // place at this position and continue search. 503 occupied.markCells(me, true); 504 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); 505 occupied.markCells(me, false); 506 } 507 508 // Try resizing horizontally 509 if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) { 510 me.spanX--; 511 occupied.markCells(me, true); 512 // 1 extra move cost 513 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); 514 occupied.markCells(me, false); 515 me.spanX++; 516 } 517 518 // Try resizing vertically 519 if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) { 520 me.spanY--; 521 occupied.markCells(me, true); 522 // 1 extra move cost 523 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); 524 occupied.markCells(me, false); 525 me.spanY++; 526 } 527 528 // Try resizing horizontally & vertically 529 if (myH > me.minSpanY && myW > me.minSpanX && 530 occupied.isRegionVacant(x, y, myW - 1, myH - 1)) { 531 me.spanX--; 532 me.spanY--; 533 occupied.markCells(me, true); 534 // 2 extra move cost 535 find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe); 536 occupied.markCells(me, false); 537 me.spanX++; 538 me.spanY++; 539 } 540 me.cellX = myX; 541 me.cellY = myY; 542 } 543 } 544 545 // Finally also try a solution when this item is not included. Trying it in the end 546 // causes it to get skipped in most cases due to higher weight loss, and prevents 547 // unnecessary deep copies of various configurations. 548 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); 549 } else { 550 // Since this is a 1x1 item and all the following items are also 1x1, just place 551 // it at 'the most appropriate position' and hope for the best. 552 // The most appropriate position: one with lease straight line distance 553 int newDistance = Integer.MAX_VALUE; 554 int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE; 555 556 for (int y = startY; y < mTrgY; y++) { 557 for (int x = 0; x < mTrgX; x++) { 558 if (!occupied.cells[x][y]) { 559 int dist = ignoreMove ? 0 : 560 ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY 561 - y)); 562 if (dist < newDistance) { 563 newX = x; 564 newY = y; 565 newDistance = dist; 566 } 567 } 568 } 569 } 570 571 if (newX < mTrgX && newY < mTrgY) { 572 float newMoveCost = moveCost; 573 if (newX != myX) { 574 me.cellX = newX; 575 newMoveCost++; 576 } 577 if (newY != myY) { 578 me.cellY = newY; 579 newMoveCost++; 580 } 581 if (ignoreMove) { 582 newMoveCost = moveCost; 583 } 584 occupied.markCells(me, true); 585 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); 586 occupied.markCells(me, false); 587 me.cellX = myX; 588 me.cellY = myY; 589 590 // Try to find a solution without this item, only if 591 // 1) there was at least one space, i.e., we were able to place this item 592 // 2) if the next item has the same weight (all items are already sorted), as 593 // if it has lower weight, that solution will automatically get discarded. 594 // 3) ignoreMove false otherwise, move cost is ignored and the weight will 595 // anyway be same. 596 if (index + 1 < itemsToPlace.size() 597 && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) { 598 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); 599 } 600 } else { 601 // No more space. Jump to the end. 602 for (int i = index + 1; i < itemsToPlace.size(); i++) { 603 weightLoss += itemsToPlace.get(i).weight; 604 } 605 find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced); 606 } 607 } 608 } 609 } 610 611 private ArrayList<DbEntry> loadHotseatEntries() { 612 Cursor c = queryWorkspace( 613 new String[]{ 614 Favorites._ID, // 0 615 Favorites.ITEM_TYPE, // 1 616 Favorites.INTENT, // 2 617 Favorites.SCREEN}, // 3 618 Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT); 619 620 final int indexId = c.getColumnIndexOrThrow(Favorites._ID); 621 final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); 622 final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); 623 final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN); 624 625 ArrayList<DbEntry> entries = new ArrayList<>(); 626 while (c.moveToNext()) { 627 DbEntry entry = new DbEntry(); 628 entry.id = c.getInt(indexId); 629 entry.itemType = c.getInt(indexItemType); 630 entry.screenId = c.getInt(indexScreen); 631 632 if (entry.screenId >= mSrcHotseatSize) { 633 mEntryToRemove.add(entry.id); 634 continue; 635 } 636 637 try { 638 // calculate weight 639 switch (entry.itemType) { 640 case Favorites.ITEM_TYPE_SHORTCUT: 641 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: 642 case Favorites.ITEM_TYPE_APPLICATION: { 643 verifyIntent(c.getString(indexIntent)); 644 entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ? 645 WT_APPLICATION : WT_SHORTCUT; 646 break; 647 } 648 case Favorites.ITEM_TYPE_FOLDER: { 649 int total = getFolderItemsCount(entry.id); 650 if (total == 0) { 651 throw new Exception("Folder is empty"); 652 } 653 entry.weight = WT_FOLDER_FACTOR * total; 654 break; 655 } 656 default: 657 throw new Exception("Invalid item type"); 658 } 659 } catch (Exception e) { 660 if (DEBUG) { 661 Log.d(TAG, "Removing item " + entry.id, e); 662 } 663 mEntryToRemove.add(entry.id); 664 continue; 665 } 666 entries.add(entry); 667 } 668 c.close(); 669 return entries; 670 } 671 672 673 /** 674 * Loads entries for a particular screen id. 675 */ 676 protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) { 677 Cursor c = queryWorkspace( 678 new String[]{ 679 Favorites._ID, // 0 680 Favorites.ITEM_TYPE, // 1 681 Favorites.CELLX, // 2 682 Favorites.CELLY, // 3 683 Favorites.SPANX, // 4 684 Favorites.SPANY, // 5 685 Favorites.INTENT, // 6 686 Favorites.APPWIDGET_PROVIDER, // 7 687 Favorites.APPWIDGET_ID}, // 8 688 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP 689 + " AND " + Favorites.SCREEN + " = " + screen); 690 691 final int indexId = c.getColumnIndexOrThrow(Favorites._ID); 692 final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); 693 final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); 694 final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); 695 final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); 696 final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); 697 final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); 698 final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); 699 final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID); 700 701 ArrayList<DbEntry> entries = new ArrayList<>(); 702 WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext); 703 while (c.moveToNext()) { 704 DbEntry entry = new DbEntry(); 705 entry.id = c.getInt(indexId); 706 entry.itemType = c.getInt(indexItemType); 707 entry.cellX = c.getInt(indexCellX); 708 entry.cellY = c.getInt(indexCellY); 709 entry.spanX = c.getInt(indexSpanX); 710 entry.spanY = c.getInt(indexSpanY); 711 entry.screenId = screen; 712 713 try { 714 // calculate weight 715 switch (entry.itemType) { 716 case Favorites.ITEM_TYPE_SHORTCUT: 717 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: 718 case Favorites.ITEM_TYPE_APPLICATION: { 719 verifyIntent(c.getString(indexIntent)); 720 entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ? 721 WT_APPLICATION : WT_SHORTCUT; 722 break; 723 } 724 case Favorites.ITEM_TYPE_APPWIDGET: { 725 String provider = c.getString(indexAppWidgetProvider); 726 ComponentName cn = ComponentName.unflattenFromString(provider); 727 verifyPackage(cn.getPackageName()); 728 entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR 729 * entry.spanX * entry.spanY); 730 731 int widgetId = c.getInt(indexAppWidgetId); 732 LauncherAppWidgetProviderInfo pInfo = 733 widgetManagerHelper.getLauncherAppWidgetInfo(widgetId); 734 Point spans = null; 735 if (pInfo != null) { 736 spans = pInfo.getMinSpans(); 737 } 738 if (spans != null) { 739 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; 740 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; 741 } else { 742 // Assume that the widget be resized down to 2x2 743 entry.minSpanX = entry.minSpanY = 2; 744 } 745 746 if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { 747 throw new Exception("Widget can't be resized down to fit the grid"); 748 } 749 break; 750 } 751 case Favorites.ITEM_TYPE_FOLDER: { 752 int total = getFolderItemsCount(entry.id); 753 if (total == 0) { 754 throw new Exception("Folder is empty"); 755 } 756 entry.weight = WT_FOLDER_FACTOR * total; 757 break; 758 } 759 default: 760 throw new Exception("Invalid item type"); 761 } 762 } catch (Exception e) { 763 if (DEBUG) { 764 Log.d(TAG, "Removing item " + entry.id, e); 765 } 766 mEntryToRemove.add(entry.id); 767 continue; 768 } 769 entries.add(entry); 770 } 771 c.close(); 772 return entries; 773 } 774 775 /** 776 * @return the number of valid items in the folder. 777 */ 778 private int getFolderItemsCount(int folderId) { 779 Cursor c = queryWorkspace( 780 new String[]{Favorites._ID, Favorites.INTENT}, 781 Favorites.CONTAINER + " = " + folderId); 782 783 int total = 0; 784 while (c.moveToNext()) { 785 try { 786 verifyIntent(c.getString(1)); 787 total++; 788 } catch (Exception e) { 789 mEntryToRemove.add(c.getInt(0)); 790 } 791 } 792 c.close(); 793 return total; 794 } 795 796 protected Cursor queryWorkspace(String[] columns, String where) { 797 return mDb.query(mTableName, columns, where, null, null, null, null); 798 } 799 800 /** 801 * Verifies if the intent should be restored. 802 */ 803 private void verifyIntent(String intentStr) throws Exception { 804 Intent intent = Intent.parseUri(intentStr, 0); 805 if (intent.getComponent() != null) { 806 verifyPackage(intent.getComponent().getPackageName()); 807 } else if (intent.getPackage() != null) { 808 // Only verify package if the component was null. 809 verifyPackage(intent.getPackage()); 810 } 811 } 812 813 /** 814 * Verifies if the package should be restored 815 */ 816 private void verifyPackage(String packageName) throws Exception { 817 if (!mValidPackages.contains(packageName)) { 818 throw new Exception("Package not available"); 819 } 820 } 821 822 protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> { 823 824 public float weight; 825 826 public DbEntry() { 827 } 828 829 public DbEntry copy() { 830 DbEntry entry = new DbEntry(); 831 entry.copyFrom(this); 832 entry.weight = weight; 833 entry.minSpanX = minSpanX; 834 entry.minSpanY = minSpanY; 835 return entry; 836 } 837 838 /** 839 * Comparator such that larger widgets come first, followed by all 1x1 items 840 * based on their weights. 841 */ 842 @Override 843 public int compareTo(DbEntry another) { 844 if (itemType == Favorites.ITEM_TYPE_APPWIDGET) { 845 if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { 846 return another.spanY * another.spanX - spanX * spanY; 847 } else { 848 return -1; 849 } 850 } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { 851 return 1; 852 } else { 853 // Place higher weight before lower weight. 854 return Float.compare(another.weight, weight); 855 } 856 } 857 858 public boolean columnsSame(DbEntry org) { 859 return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX && 860 org.spanY == spanY && org.screenId == screenId; 861 } 862 863 public void addToContentValues(ContentValues values) { 864 values.put(Favorites.SCREEN, screenId); 865 values.put(Favorites.CELLX, cellX); 866 values.put(Favorites.CELLY, cellY); 867 values.put(Favorites.SPANX, spanX); 868 values.put(Favorites.SPANY, spanY); 869 } 870 } 871 872 private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) { 873 ArrayList<DbEntry> dup = new ArrayList<>(src.size()); 874 for (DbEntry e : src) { 875 dup.add(e.copy()); 876 } 877 return dup; 878 } 879 880 public static void markForMigration( 881 Context context, int gridX, int gridY, int hotseatSize) { 882 Utilities.getPrefs(context).edit() 883 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY)) 884 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize) 885 .apply(); 886 } 887 888 /** 889 * Check given a new IDP, if migration is necessary. 890 */ 891 public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) { 892 SharedPreferences prefs = Utilities.getPrefs(context); 893 String gridSizeString = getPointString(idp.numColumns, idp.numRows); 894 895 return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) 896 || idp.numDatabaseHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1); 897 } 898 899 /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */ 900 public static boolean migrateGridIfNeeded(Context context) { 901 if (context instanceof LauncherPreviewRenderer.PreviewContext) { 902 return true; 903 } 904 return migrateGridIfNeeded(context, null); 905 } 906 907 /** 908 * Run the migration algorithm if needed. For preview, we provide the intended idp because it 909 * has not been changed. If idp is null, we read it from the context, for actual grid migration. 910 * 911 * @return false if the migration failed. 912 */ 913 public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) { 914 boolean migrateForPreview = idp != null; 915 if (!migrateForPreview) { 916 idp = LauncherAppState.getIDP(context); 917 } 918 919 if (!needsToMigrate(context, idp)) { 920 return true; 921 } 922 923 SharedPreferences prefs = Utilities.getPrefs(context); 924 String gridSizeString = getPointString(idp.numColumns, idp.numRows); 925 long migrationStartTime = SystemClock.elapsedRealtime(); 926 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call( 927 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION) 928 .getBinder(Settings.EXTRA_VALUE)) { 929 930 int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, 931 idp.numDatabaseHotseatIcons); 932 Point sourceSize = parsePoint(prefs.getString( 933 KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)); 934 935 boolean dbChanged = false; 936 if (migrateForPreview) { 937 copyTable(transaction.getDb(), Favorites.TABLE_NAME, transaction.getDb(), 938 Favorites.PREVIEW_TABLE_NAME, context); 939 } 940 941 GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(), 942 srcHotseatCount, sourceSize.x, sourceSize.y); 943 if (migrateForPreview ? backupTable.restoreToPreviewIfBackupExists() 944 : backupTable.backupOrRestoreAsNeeded()) { 945 dbChanged = true; 946 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize); 947 } 948 949 HashSet<String> validPackages = getValidPackages(context); 950 // Hotseat. 951 if (srcHotseatCount != idp.numDatabaseHotseatIcons 952 && new GridSizeMigrationTask(context, transaction.getDb(), validPackages, 953 migrateForPreview, srcHotseatCount, 954 idp.numDatabaseHotseatIcons).migrateHotseat()) { 955 dbChanged = true; 956 } 957 958 // Grid size 959 Point targetSize = new Point(idp.numColumns, idp.numRows); 960 if (new MultiStepMigrationTask(validPackages, context, transaction.getDb(), 961 migrateForPreview).migrate(sourceSize, targetSize)) { 962 dbChanged = true; 963 } 964 965 if (dbChanged) { 966 // Make sure we haven't removed everything. 967 final Cursor c = context.getContentResolver().query( 968 migrateForPreview ? Favorites.PREVIEW_CONTENT_URI : Favorites.CONTENT_URI, 969 null, null, null, null); 970 boolean hasData = c.moveToNext(); 971 c.close(); 972 if (!hasData) { 973 throw new Exception("Removed every thing during grid resize"); 974 } 975 } 976 977 transaction.commit(); 978 if (!migrateForPreview) { 979 Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE); 980 } 981 return true; 982 } catch (Exception e) { 983 Log.e(TAG, "Error during preview grid migration", e); 984 985 return false; 986 } finally { 987 Log.v(TAG, "Preview workspace migration completed in " 988 + (SystemClock.elapsedRealtime() - migrationStartTime)); 989 990 if (!migrateForPreview) { 991 // Save current configuration, so that the migration does not run again. 992 prefs.edit() 993 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString) 994 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numDatabaseHotseatIcons) 995 .apply(); 996 } 997 } 998 } 999 1000 protected static HashSet<String> getValidPackages(Context context) { 1001 // Initialize list of valid packages. This contain all the packages which are already on 1002 // the device and packages which are being installed. Any item which doesn't belong to 1003 // this set is removed. 1004 // Since the loader removes such items anyway, removing these items here doesn't cause 1005 // any extra data loss and gives us more free space on the grid for better migration. 1006 HashSet<String> validPackages = new HashSet<>(); 1007 for (PackageInfo info : context.getPackageManager() 1008 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { 1009 validPackages.add(info.packageName); 1010 } 1011 InstallSessionHelper.INSTANCE.get(context) 1012 .getActiveSessions().keySet() 1013 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName)); 1014 return validPackages; 1015 } 1016 1017 /** 1018 * Removes any broken item from the hotseat. 1019 * 1020 * @return a map with occupied hotseat position set to non-null value. 1021 */ 1022 public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context) 1023 throws Exception { 1024 try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call( 1025 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION) 1026 .getBinder(Settings.EXTRA_VALUE)) { 1027 GridSizeMigrationTask task = new GridSizeMigrationTask( 1028 context, transaction.getDb(), getValidPackages(context), 1029 false /* usePreviewTable */, Integer.MAX_VALUE, Integer.MAX_VALUE); 1030 1031 // Load all the valid entries 1032 ArrayList<DbEntry> items = task.loadHotseatEntries(); 1033 // Delete any entry marked for deletion by above load. 1034 task.applyOperations(); 1035 IntSparseArrayMap<Object> positions = new IntSparseArrayMap<>(); 1036 for (DbEntry item : items) { 1037 positions.put(item.screenId, item); 1038 } 1039 transaction.commit(); 1040 return positions; 1041 } 1042 } 1043 1044 /** 1045 * Task to run grid migration in multiple steps when the size difference is more than 1. 1046 */ 1047 protected static class MultiStepMigrationTask { 1048 private final HashSet<String> mValidPackages; 1049 private final Context mContext; 1050 private final SQLiteDatabase mDb; 1051 private final boolean mUsePreviewTable; 1052 1053 public MultiStepMigrationTask(HashSet<String> validPackages, Context context, 1054 SQLiteDatabase db, boolean usePreviewTable) { 1055 mValidPackages = validPackages; 1056 mContext = context; 1057 mDb = db; 1058 mUsePreviewTable = usePreviewTable; 1059 } 1060 1061 public boolean migrate(Point sourceSize, Point targetSize) throws Exception { 1062 boolean dbChanged = false; 1063 if (!targetSize.equals(sourceSize)) { 1064 if (sourceSize.x < targetSize.x) { 1065 // Source is smaller that target, just expand the grid without actual migration. 1066 sourceSize.x = targetSize.x; 1067 } 1068 if (sourceSize.y < targetSize.y) { 1069 // Source is smaller that target, just expand the grid without actual migration. 1070 sourceSize.y = targetSize.y; 1071 } 1072 1073 // Migrate the workspace grid, such that the points differ by max 1 in x and y 1074 // each on every step. 1075 while (!targetSize.equals(sourceSize)) { 1076 // Get the next size, such that the points differ by max 1 in x and y each 1077 Point nextSize = new Point(sourceSize); 1078 if (targetSize.x < nextSize.x) { 1079 nextSize.x--; 1080 } 1081 if (targetSize.y < nextSize.y) { 1082 nextSize.y--; 1083 } 1084 if (runStepTask(sourceSize, nextSize)) { 1085 dbChanged = true; 1086 } 1087 sourceSize.set(nextSize.x, nextSize.y); 1088 } 1089 } 1090 return dbChanged; 1091 } 1092 1093 protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception { 1094 return new GridSizeMigrationTask(mContext, mDb, mValidPackages, mUsePreviewTable, 1095 sourceSize, nextSize).migrateWorkspace(); 1096 } 1097 } 1098 } 1099