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