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