1package com.android.launcher3.model; 2 3import android.content.ComponentName; 4import android.content.ContentProviderOperation; 5import android.content.ContentValues; 6import android.content.Context; 7import android.content.Intent; 8import android.content.SharedPreferences; 9import android.content.pm.PackageInfo; 10import android.database.Cursor; 11import android.graphics.Point; 12import android.text.TextUtils; 13import android.util.Log; 14 15import com.android.launcher3.InvariantDeviceProfile; 16import com.android.launcher3.ItemInfo; 17import com.android.launcher3.LauncherAppState; 18import com.android.launcher3.LauncherAppWidgetProviderInfo; 19import com.android.launcher3.LauncherModel; 20import com.android.launcher3.LauncherProvider; 21import com.android.launcher3.LauncherSettings; 22import com.android.launcher3.LauncherSettings.Favorites; 23import com.android.launcher3.Utilities; 24import com.android.launcher3.compat.PackageInstallerCompat; 25import com.android.launcher3.compat.UserHandleCompat; 26import com.android.launcher3.util.LongArrayMap; 27import com.android.launcher3.util.Thunk; 28 29import java.util.ArrayList; 30import java.util.Collections; 31import java.util.HashMap; 32import java.util.HashSet; 33 34/** 35 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a 36 * result of restoring from a larger device. 37 */ 38public class MigrateFromRestoreTask { 39 40 public static boolean ENABLED = false; 41 42 private static final String TAG = "MigrateFromRestoreTask"; 43 private static final boolean DEBUG = true; 44 45 private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size"; 46 private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size"; 47 48 // These are carefully selected weights for various item types (Math.random?), to allow for 49 // the lease absurd migration experience. 50 private static final float WT_SHORTCUT = 1; 51 private static final float WT_APPLICATION = 0.8f; 52 private static final float WT_WIDGET_MIN = 2; 53 private static final float WT_WIDGET_FACTOR = 0.6f; 54 private static final float WT_FOLDER_FACTOR = 0.5f; 55 56 private final Context mContext; 57 private final ContentValues mTempValues = new ContentValues(); 58 private final HashMap<String, Point> mWidgetMinSize; 59 private final InvariantDeviceProfile mIdp; 60 61 private HashSet<String> mValidPackages; 62 public ArrayList<Long> mEntryToRemove; 63 private ArrayList<ContentProviderOperation> mUpdateOperations; 64 65 private ArrayList<DbEntry> mCarryOver; 66 67 private final int mSrcX, mSrcY; 68 @Thunk final int mTrgX, mTrgY; 69 private final boolean mShouldRemoveX, mShouldRemoveY; 70 71 public MigrateFromRestoreTask(Context context) { 72 mContext = context; 73 74 SharedPreferences prefs = prefs(context); 75 Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, "")); 76 mSrcX = sourceSize.x; 77 mSrcY = sourceSize.y; 78 79 mWidgetMinSize = new HashMap<String, Point>(); 80 for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, 81 Collections.<String>emptySet())) { 82 String[] parts = s.split("#"); 83 mWidgetMinSize.put(parts[0], parsePoint(parts[1])); 84 } 85 86 mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); 87 mTrgX = mIdp.numColumns; 88 mTrgY = mIdp.numRows; 89 mShouldRemoveX = mTrgX < mSrcX; 90 mShouldRemoveY = mTrgY < mSrcY; 91 } 92 93 public void execute() throws Exception { 94 mEntryToRemove = new ArrayList<>(); 95 mCarryOver = new ArrayList<>(); 96 mUpdateOperations = new ArrayList<>(); 97 98 // Initialize list of valid packages. This contain all the packages which are already on 99 // the device and packages which are being installed. Any item which doesn't belong to 100 // this set is removed. 101 // Since the loader removes such items anyway, removing these items here doesn't cause any 102 // extra data loss and gives us more free space on the grid for better migration. 103 mValidPackages = new HashSet<>(); 104 for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) { 105 mValidPackages.add(info.packageName); 106 } 107 mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext) 108 .updateAndGetActiveSessionCache().keySet()); 109 110 ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); 111 if (allScreens.isEmpty()) { 112 throw new Exception("Unable to get workspace screens"); 113 } 114 115 for (long screenId : allScreens) { 116 if (DEBUG) { 117 Log.d(TAG, "Migrating " + screenId); 118 } 119 migrateScreen(screenId); 120 } 121 122 if (!mCarryOver.isEmpty()) { 123 LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); 124 for (DbEntry e : mCarryOver) { 125 itemMap.put(e.id, e); 126 } 127 128 do { 129 // Some items are still remaining. Try adding a few new screens. 130 131 // At every iteration, make sure that at least one item is removed from 132 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed, 133 // break the loop and abort migration by throwing an exception. 134 OptimalPlacementSolution placement = new OptimalPlacementSolution( 135 new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true); 136 placement.find(); 137 if (placement.finalPlacedItems.size() > 0) { 138 long newScreenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); 139 allScreens.add(newScreenId); 140 for (DbEntry item : placement.finalPlacedItems) { 141 if (!mCarryOver.remove(itemMap.get(item.id))) { 142 throw new Exception("Unable to find matching items"); 143 } 144 item.screenId = newScreenId; 145 update(item); 146 } 147 } else { 148 throw new Exception("None of the items can be placed on an empty screen"); 149 } 150 151 } while (!mCarryOver.isEmpty()); 152 153 154 LauncherAppState.getInstance().getModel() 155 .updateWorkspaceScreenOrder(mContext, allScreens); 156 } 157 158 // Update items 159 mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); 160 161 if (!mEntryToRemove.isEmpty()) { 162 if (DEBUG) { 163 Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); 164 } 165 mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, 166 Utilities.createDbSelectionQuery( 167 LauncherSettings.Favorites._ID, mEntryToRemove), null); 168 } 169 170 // Make sure we haven't removed everything. 171 final Cursor c = mContext.getContentResolver().query( 172 LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); 173 boolean hasData = c.moveToNext(); 174 c.close(); 175 if (!hasData) { 176 throw new Exception("Removed every thing during grid resize"); 177 } 178 } 179 180 /** 181 * Migrate a particular screen id. 182 * Strategy: 183 * 1) For all possible combinations of row and column, pick the one which causes the least 184 * data loss: {@link #tryRemove(int, int, ArrayList, float[])} 185 * 2) Maintain a list of all lost items before this screen, and add any new item lost from 186 * this screen to that list as well. 187 * 3) If all those items from the above list can be placed on this screen, place them 188 * (otherwise they are placed on a new screen). 189 */ 190 private void migrateScreen(long screenId) { 191 ArrayList<DbEntry> items = loadEntries(screenId); 192 193 int removedCol = Integer.MAX_VALUE; 194 int removedRow = Integer.MAX_VALUE; 195 196 // removeWt represents the cost function for loss of items during migration, and moveWt 197 // represents the cost function for repositioning the items. moveWt is only considered if 198 // removeWt is same for two different configurations. 199 // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least 200 // cost. 201 float removeWt = Float.MAX_VALUE; 202 float moveWt = Float.MAX_VALUE; 203 float[] outLoss = new float[2]; 204 ArrayList<DbEntry> finalItems = null; 205 206 // Try removing all possible combinations 207 for (int x = 0; x < mSrcX; x++) { 208 for (int y = 0; y < mSrcY; y++) { 209 // Use a deep copy when trying out a particular combination as it can change 210 // the underlying object. 211 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss); 212 213 if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) { 214 removeWt = outLoss[0]; 215 moveWt = outLoss[1]; 216 removedCol = mShouldRemoveX ? x : removedCol; 217 removedRow = mShouldRemoveY ? y : removedRow; 218 finalItems = itemsOnScreen; 219 } 220 221 // No need to loop over all rows, if a row removal is not needed. 222 if (!mShouldRemoveY) { 223 break; 224 } 225 } 226 227 if (!mShouldRemoveX) { 228 break; 229 } 230 } 231 232 if (DEBUG) { 233 Log.d(TAG, String.format("Removing row %d, column %d on screen %d", 234 removedRow, removedCol, screenId)); 235 } 236 237 LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); 238 for (DbEntry e : deepCopy(items)) { 239 itemMap.put(e.id, e); 240 } 241 242 for (DbEntry item : finalItems) { 243 DbEntry org = itemMap.get(item.id); 244 itemMap.remove(item.id); 245 246 // Check if update is required 247 if (!item.columnsSame(org)) { 248 update(item); 249 } 250 } 251 252 // The remaining items in {@link #itemMap} are those which didn't get placed. 253 for (DbEntry item : itemMap) { 254 mCarryOver.add(item); 255 } 256 257 if (!mCarryOver.isEmpty() && removeWt == 0) { 258 // No new items were removed in this step. Try placing all the items on this screen. 259 boolean[][] occupied = new boolean[mTrgX][mTrgY]; 260 for (DbEntry item : finalItems) { 261 markCells(occupied, item, true); 262 } 263 264 OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, 265 deepCopy(mCarryOver), true); 266 placement.find(); 267 if (placement.lowestWeightLoss == 0) { 268 // All items got placed 269 270 for (DbEntry item : placement.finalPlacedItems) { 271 item.screenId = screenId; 272 update(item); 273 } 274 275 mCarryOver.clear(); 276 } 277 } 278 } 279 280 /** 281 * Updates an item in the DB. 282 */ 283 private void update(DbEntry item) { 284 mTempValues.clear(); 285 item.addToContentValues(mTempValues); 286 mUpdateOperations.add(ContentProviderOperation 287 .newUpdate(LauncherSettings.Favorites.getContentUri(item.id)) 288 .withValues(mTempValues).build()); 289 } 290 291 /** 292 * Tries the remove the provided row and column. 293 * @param items all the items on the screen under operation 294 * @param outLoss array of size 2. The first entry is filled with weight loss, and the second 295 * with the overall item movement. 296 */ 297 private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items, 298 float[] outLoss) { 299 boolean[][] occupied = new boolean[mTrgX][mTrgY]; 300 301 col = mShouldRemoveX ? col : Integer.MAX_VALUE; 302 row = mShouldRemoveY ? row : Integer.MAX_VALUE; 303 304 ArrayList<DbEntry> finalItems = new ArrayList<>(); 305 ArrayList<DbEntry> removedItems = new ArrayList<>(); 306 307 for (DbEntry item : items) { 308 if ((item.cellX <= col && (item.spanX + item.cellX) > col) 309 || (item.cellY <= row && (item.spanY + item.cellY) > row)) { 310 removedItems.add(item); 311 if (item.cellX >= col) item.cellX --; 312 if (item.cellY >= row) item.cellY --; 313 } else { 314 if (item.cellX > col) item.cellX --; 315 if (item.cellY > row) item.cellY --; 316 finalItems.add(item); 317 markCells(occupied, item, true); 318 } 319 } 320 321 OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems); 322 placement.find(); 323 finalItems.addAll(placement.finalPlacedItems); 324 outLoss[0] = placement.lowestWeightLoss; 325 outLoss[1] = placement.lowestMoveCost; 326 return finalItems; 327 } 328 329 @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) { 330 for (int i = item.cellX; i < (item.cellX + item.spanX); i++) { 331 for (int j = item.cellY; j < (item.cellY + item.spanY); j++) { 332 occupied[i][j] = val; 333 } 334 } 335 } 336 337 @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { 338 if (x + w > mTrgX) return false; 339 if (y + h > mTrgY) return false; 340 341 for (int i = 0; i < w; i++) { 342 for (int j = 0; j < h; j++) { 343 if (occupied[i + x][j + y]) { 344 return false; 345 } 346 } 347 } 348 return true; 349 } 350 351 private class OptimalPlacementSolution { 352 private final ArrayList<DbEntry> itemsToPlace; 353 private final boolean[][] occupied; 354 355 // If set to true, item movement are not considered in move cost, leading to a more 356 // linear placement. 357 private final boolean ignoreMove; 358 359 float lowestWeightLoss = Float.MAX_VALUE; 360 float lowestMoveCost = Float.MAX_VALUE; 361 ArrayList<DbEntry> finalPlacedItems; 362 363 public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) { 364 this(occupied, itemsToPlace, false); 365 } 366 367 public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace, 368 boolean ignoreMove) { 369 this.occupied = occupied; 370 this.itemsToPlace = itemsToPlace; 371 this.ignoreMove = ignoreMove; 372 373 // Sort the items such that larger widgets appear first followed by 1x1 items 374 Collections.sort(this.itemsToPlace); 375 } 376 377 public void find() { 378 find(0, 0, 0, new ArrayList<DbEntry>()); 379 } 380 381 /** 382 * Recursively finds a placement for the provided items. 383 * @param index the position in {@link #itemsToPlace} to start looking at. 384 * @param weightLoss total weight loss upto this point 385 * @param moveCost total move cost upto this point 386 * @param itemsPlaced all the items already placed upto this point 387 */ 388 public void find(int index, float weightLoss, float moveCost, 389 ArrayList<DbEntry> itemsPlaced) { 390 if ((weightLoss >= lowestWeightLoss) || 391 ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) { 392 // Abort, as we already have a better solution. 393 return; 394 395 } else if (index >= itemsToPlace.size()) { 396 // End loop. 397 lowestWeightLoss = weightLoss; 398 lowestMoveCost = moveCost; 399 400 // Keep a deep copy of current configuration as it can change during recursion. 401 finalPlacedItems = deepCopy(itemsPlaced); 402 return; 403 } 404 405 DbEntry me = itemsToPlace.get(index); 406 int myX = me.cellX; 407 int myY = me.cellY; 408 409 // List of items to pass over if this item was placed. 410 ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1); 411 itemsIncludingMe.addAll(itemsPlaced); 412 itemsIncludingMe.add(me); 413 414 if (me.spanX > 1 || me.spanY > 1) { 415 // If the current item is a widget (and it greater than 1x1), try to place it at 416 // all possible positions. This is because a widget placed at one position can 417 // affect the placement of a different widget. 418 int myW = me.spanX; 419 int myH = me.spanY; 420 421 for (int y = 0; y < mTrgY; y++) { 422 for (int x = 0; x < mTrgX; x++) { 423 float newMoveCost = moveCost; 424 if (x != myX) { 425 me.cellX = x; 426 newMoveCost ++; 427 } 428 if (y != myY) { 429 me.cellY = y; 430 newMoveCost ++; 431 } 432 if (ignoreMove) { 433 newMoveCost = moveCost; 434 } 435 436 if (isVacant(occupied, x, y, myW, myH)) { 437 // place at this position and continue search. 438 markCells(occupied, me, true); 439 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); 440 markCells(occupied, me, false); 441 } 442 443 // Try resizing horizontally 444 if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) { 445 me.spanX --; 446 markCells(occupied, me, true); 447 // 1 extra move cost 448 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); 449 markCells(occupied, me, false); 450 me.spanX ++; 451 } 452 453 // Try resizing vertically 454 if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) { 455 me.spanY --; 456 markCells(occupied, me, true); 457 // 1 extra move cost 458 find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); 459 markCells(occupied, me, false); 460 me.spanY ++; 461 } 462 463 // Try resizing horizontally & vertically 464 if (myH > me.minSpanY && myW > me.minSpanX && 465 isVacant(occupied, x, y, myW - 1, myH - 1)) { 466 me.spanX --; 467 me.spanY --; 468 markCells(occupied, me, true); 469 // 2 extra move cost 470 find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe); 471 markCells(occupied, me, false); 472 me.spanX ++; 473 me.spanY ++; 474 } 475 me.cellX = myX; 476 me.cellY = myY; 477 } 478 } 479 480 // Finally also try a solution when this item is not included. Trying it in the end 481 // causes it to get skipped in most cases due to higher weight loss, and prevents 482 // unnecessary deep copies of various configurations. 483 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); 484 } else { 485 // Since this is a 1x1 item and all the following items are also 1x1, just place 486 // it at 'the most appropriate position' and hope for the best. 487 // The most appropriate position: one with lease straight line distance 488 int newDistance = Integer.MAX_VALUE; 489 int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE; 490 491 for (int y = 0; y < mTrgY; y++) { 492 for (int x = 0; x < mTrgX; x++) { 493 if (!occupied[x][y]) { 494 int dist = ignoreMove ? 0 : 495 ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y)); 496 if (dist < newDistance) { 497 newX = x; 498 newY = y; 499 newDistance = dist; 500 } 501 } 502 } 503 } 504 505 if (newX < mTrgX && newY < mTrgY) { 506 float newMoveCost = moveCost; 507 if (newX != myX) { 508 me.cellX = newX; 509 newMoveCost ++; 510 } 511 if (newY != myY) { 512 me.cellY = newY; 513 newMoveCost ++; 514 } 515 if (ignoreMove) { 516 newMoveCost = moveCost; 517 } 518 markCells(occupied, me, true); 519 find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); 520 markCells(occupied, me, false); 521 me.cellX = myX; 522 me.cellY = myY; 523 524 // Try to find a solution without this item, only if 525 // 1) there was at least one space, i.e., we were able to place this item 526 // 2) if the next item has the same weight (all items are already sorted), as 527 // if it has lower weight, that solution will automatically get discarded. 528 // 3) ignoreMove false otherwise, move cost is ignored and the weight will 529 // anyway be same. 530 if (index + 1 < itemsToPlace.size() 531 && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) { 532 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); 533 } 534 } else { 535 // No more space. Jump to the end. 536 for (int i = index + 1; i < itemsToPlace.size(); i++) { 537 weightLoss += itemsToPlace.get(i).weight; 538 } 539 find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced); 540 } 541 } 542 } 543 } 544 545 /** 546 * Loads entries for a particular screen id. 547 */ 548 public ArrayList<DbEntry> loadEntries(long screen) { 549 Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, 550 new String[] { 551 Favorites._ID, // 0 552 Favorites.ITEM_TYPE, // 1 553 Favorites.CELLX, // 2 554 Favorites.CELLY, // 3 555 Favorites.SPANX, // 4 556 Favorites.SPANY, // 5 557 Favorites.INTENT, // 6 558 Favorites.APPWIDGET_PROVIDER}, // 7 559 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP 560 + " AND " + Favorites.SCREEN + " = " + screen, null, null, null); 561 562 final int indexId = c.getColumnIndexOrThrow(Favorites._ID); 563 final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); 564 final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); 565 final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); 566 final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); 567 final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); 568 final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); 569 final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); 570 571 ArrayList<DbEntry> entries = new ArrayList<>(); 572 while (c.moveToNext()) { 573 DbEntry entry = new DbEntry(); 574 entry.id = c.getLong(indexId); 575 entry.itemType = c.getInt(indexItemType); 576 entry.cellX = c.getInt(indexCellX); 577 entry.cellY = c.getInt(indexCellY); 578 entry.spanX = c.getInt(indexSpanX); 579 entry.spanY = c.getInt(indexSpanY); 580 entry.screenId = screen; 581 582 try { 583 // calculate weight 584 switch (entry.itemType) { 585 case Favorites.ITEM_TYPE_SHORTCUT: 586 case Favorites.ITEM_TYPE_APPLICATION: { 587 verifyIntent(c.getString(indexIntent)); 588 entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT 589 ? WT_SHORTCUT : WT_APPLICATION; 590 break; 591 } 592 case Favorites.ITEM_TYPE_APPWIDGET: { 593 String provider = c.getString(indexAppWidgetProvider); 594 ComponentName cn = ComponentName.unflattenFromString(provider); 595 verifyPackage(cn.getPackageName()); 596 entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR 597 * entry.spanX * entry.spanY); 598 599 // Migration happens for current user only. 600 LauncherAppWidgetProviderInfo pInfo = LauncherModel.getProviderInfo( 601 mContext, cn, UserHandleCompat.myUserHandle()); 602 Point spans = pInfo == null ? 603 mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext); 604 if (spans != null) { 605 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; 606 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; 607 } else { 608 // Assume that the widget be resized down to 2x2 609 entry.minSpanX = entry.minSpanY = 2; 610 } 611 612 if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { 613 throw new Exception("Widget can't be resized down to fit the grid"); 614 } 615 break; 616 } 617 case Favorites.ITEM_TYPE_FOLDER: { 618 int total = getFolderItemsCount(entry.id); 619 if (total == 0) { 620 throw new Exception("Folder is empty"); 621 } 622 entry.weight = WT_FOLDER_FACTOR * total; 623 break; 624 } 625 default: 626 throw new Exception("Invalid item type"); 627 } 628 } catch (Exception e) { 629 if (DEBUG) { 630 Log.d(TAG, "Removing item " + entry.id, e); 631 } 632 mEntryToRemove.add(entry.id); 633 continue; 634 } 635 636 entries.add(entry); 637 } 638 return entries; 639 } 640 641 /** 642 * @return the number of valid items in the folder. 643 */ 644 private int getFolderItemsCount(long folderId) { 645 Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, 646 new String[] {Favorites._ID, Favorites.INTENT}, 647 Favorites.CONTAINER + " = " + folderId, null, null, null); 648 649 int total = 0; 650 while (c.moveToNext()) { 651 try { 652 verifyIntent(c.getString(1)); 653 total++; 654 } catch (Exception e) { 655 mEntryToRemove.add(c.getLong(0)); 656 } 657 } 658 659 return total; 660 } 661 662 /** 663 * Verifies if the intent should be restored. 664 */ 665 private void verifyIntent(String intentStr) throws Exception { 666 Intent intent = Intent.parseUri(intentStr, 0); 667 if (intent.getComponent() != null) { 668 verifyPackage(intent.getComponent().getPackageName()); 669 } else if (intent.getPackage() != null) { 670 // Only verify package if the component was null. 671 verifyPackage(intent.getPackage()); 672 } 673 } 674 675 /** 676 * Verifies if the package should be restored 677 */ 678 private void verifyPackage(String packageName) throws Exception { 679 if (!mValidPackages.contains(packageName)) { 680 throw new Exception("Package not available"); 681 } 682 } 683 684 private static class DbEntry extends ItemInfo implements Comparable<DbEntry> { 685 686 public float weight; 687 688 public DbEntry() { } 689 690 public DbEntry copy() { 691 DbEntry entry = new DbEntry(); 692 entry.copyFrom(this); 693 entry.weight = weight; 694 entry.minSpanX = minSpanX; 695 entry.minSpanY = minSpanY; 696 return entry; 697 } 698 699 /** 700 * Comparator such that larger widgets come first, followed by all 1x1 items 701 * based on their weights. 702 */ 703 @Override 704 public int compareTo(DbEntry another) { 705 if (itemType == Favorites.ITEM_TYPE_APPWIDGET) { 706 if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { 707 return another.spanY * another.spanX - spanX * spanY; 708 } else { 709 return -1; 710 } 711 } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { 712 return 1; 713 } else { 714 // Place higher weight before lower weight. 715 return Float.compare(another.weight, weight); 716 } 717 } 718 719 public boolean columnsSame(DbEntry org) { 720 return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX && 721 org.spanY == spanY && org.screenId == screenId; 722 } 723 724 public void addToContentValues(ContentValues values) { 725 values.put(LauncherSettings.Favorites.SCREEN, screenId); 726 values.put(LauncherSettings.Favorites.CELLX, cellX); 727 values.put(LauncherSettings.Favorites.CELLY, cellY); 728 values.put(LauncherSettings.Favorites.SPANX, spanX); 729 values.put(LauncherSettings.Favorites.SPANY, spanY); 730 } 731 } 732 733 @Thunk static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) { 734 ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size()); 735 for (DbEntry e : src) { 736 dup.add(e.copy()); 737 } 738 return dup; 739 } 740 741 private static Point parsePoint(String point) { 742 String[] split = point.split(","); 743 return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1])); 744 } 745 746 public static void markForMigration(Context context, int srcX, int srcY, 747 HashSet<String> widgets) { 748 prefs(context).edit() 749 .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY) 750 .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets) 751 .apply(); 752 } 753 754 public static boolean shouldRunTask(Context context) { 755 return !TextUtils.isEmpty(prefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, "")); 756 } 757 758 public static void clearFlags(Context context) { 759 prefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE) 760 .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); 761 } 762 763 private static SharedPreferences prefs(Context context) { 764 return context.getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), 765 Context.MODE_PRIVATE); 766 } 767} 768