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