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