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