1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.model; 18 19 import static com.android.launcher3.provider.LauncherDbUtils.dropTable; 20 21 import android.content.ComponentName; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.SharedPreferences; 26 import android.content.pm.PackageInfo; 27 import android.content.pm.PackageManager; 28 import android.database.Cursor; 29 import android.database.DatabaseUtils; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.graphics.Point; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 35 import androidx.annotation.VisibleForTesting; 36 37 import com.android.launcher3.InvariantDeviceProfile; 38 import com.android.launcher3.LauncherAppState; 39 import com.android.launcher3.LauncherSettings; 40 import com.android.launcher3.Utilities; 41 import com.android.launcher3.graphics.LauncherPreviewRenderer; 42 import com.android.launcher3.model.data.ItemInfo; 43 import com.android.launcher3.pm.InstallSessionHelper; 44 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; 45 import com.android.launcher3.util.GridOccupancy; 46 import com.android.launcher3.util.IntArray; 47 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 48 import com.android.launcher3.widget.WidgetManagerHelper; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Iterator; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Objects; 58 import java.util.Set; 59 60 /** 61 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a 62 * result of restoring from a larger device or device density change. 63 */ 64 public class GridSizeMigrationTaskV2 { 65 66 private static final String TAG = "GridSizeMigrationTaskV2"; 67 private static final boolean DEBUG = false; 68 69 private final Context mContext; 70 private final SQLiteDatabase mDb; 71 private final DbReader mSrcReader; 72 private final DbReader mDestReader; 73 74 private final List<DbEntry> mHotseatItems; 75 private final List<DbEntry> mWorkspaceItems; 76 77 private final List<DbEntry> mHotseatDiff; 78 private final List<DbEntry> mWorkspaceDiff; 79 80 private final int mDestHotseatSize; 81 private final int mTrgX, mTrgY; 82 83 @VisibleForTesting GridSizeMigrationTaskV2(Context context, SQLiteDatabase db, DbReader srcReader, DbReader destReader, int destHotseatSize, Point targetSize)84 protected GridSizeMigrationTaskV2(Context context, SQLiteDatabase db, DbReader srcReader, 85 DbReader destReader, int destHotseatSize, Point targetSize) { 86 mContext = context; 87 mDb = db; 88 mSrcReader = srcReader; 89 mDestReader = destReader; 90 91 mHotseatItems = destReader.loadHotseatEntries(); 92 mWorkspaceItems = destReader.loadAllWorkspaceEntries(); 93 94 mHotseatDiff = calcDiff(mSrcReader.loadHotseatEntries(), mHotseatItems); 95 mWorkspaceDiff = calcDiff(mSrcReader.loadAllWorkspaceEntries(), mWorkspaceItems); 96 mDestHotseatSize = destHotseatSize; 97 98 mTrgX = targetSize.x; 99 mTrgY = targetSize.y; 100 } 101 102 /** 103 * Check given a new IDP, if migration is necessary. 104 */ needsToMigrate(Context context, InvariantDeviceProfile idp)105 public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) { 106 DeviceGridState idpGridState = new DeviceGridState(idp); 107 DeviceGridState contextGridState = new DeviceGridState(context); 108 boolean needsToMigrate = !idpGridState.isCompatible(contextGridState); 109 // TODO(b/198965093): Revert this change after bug is fixed 110 if (needsToMigrate) { 111 Log.d("b/198965093", "Migration is needed. idpGridState: " + idpGridState 112 + ", contextGridState: " + contextGridState); 113 } 114 return needsToMigrate; 115 } 116 117 /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */ migrateGridIfNeeded(Context context)118 public static boolean migrateGridIfNeeded(Context context) { 119 if (context instanceof LauncherPreviewRenderer.PreviewContext) { 120 return true; 121 } 122 return migrateGridIfNeeded(context, null); 123 } 124 125 /** 126 * When migrating the grid for preview, we copy the table 127 * {@link LauncherSettings.Favorites#TABLE_NAME} into 128 * {@link LauncherSettings.Favorites#PREVIEW_TABLE_NAME}, run grid size migration from the 129 * former to the later, then use the later table for preview. 130 * 131 * Similarly when doing the actual grid migration, the former grid option's table 132 * {@link LauncherSettings.Favorites#TABLE_NAME} is copied into the new grid option's 133 * {@link LauncherSettings.Favorites#TMP_TABLE}, we then run the grid size migration algorithm 134 * to migrate the later to the former, and load the workspace from the default 135 * {@link LauncherSettings.Favorites#TABLE_NAME}. 136 * 137 * @return false if the migration failed. 138 */ migrateGridIfNeeded(Context context, InvariantDeviceProfile idp)139 public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) { 140 boolean migrateForPreview = idp != null; 141 if (!migrateForPreview) { 142 idp = LauncherAppState.getIDP(context); 143 } 144 145 if (!needsToMigrate(context, idp)) { 146 return true; 147 } 148 149 SharedPreferences prefs = Utilities.getPrefs(context); 150 HashSet<String> validPackages = getValidPackages(context); 151 152 if (migrateForPreview) { 153 if (!LauncherSettings.Settings.call( 154 context.getContentResolver(), 155 LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW, idp.dbFile).getBoolean( 156 LauncherSettings.Settings.EXTRA_VALUE)) { 157 return false; 158 } 159 } else if (!LauncherSettings.Settings.call( 160 context.getContentResolver(), 161 LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER).getBoolean( 162 LauncherSettings.Settings.EXTRA_VALUE)) { 163 return false; 164 } 165 166 long migrationStartTime = System.currentTimeMillis(); 167 try (SQLiteTransaction t = (SQLiteTransaction) LauncherSettings.Settings.call( 168 context.getContentResolver(), 169 LauncherSettings.Settings.METHOD_NEW_TRANSACTION).getBinder( 170 LauncherSettings.Settings.EXTRA_VALUE)) { 171 172 DbReader srcReader = new DbReader(t.getDb(), 173 migrateForPreview ? LauncherSettings.Favorites.TABLE_NAME 174 : LauncherSettings.Favorites.TMP_TABLE, 175 context, validPackages); 176 DbReader destReader = new DbReader(t.getDb(), 177 migrateForPreview ? LauncherSettings.Favorites.PREVIEW_TABLE_NAME 178 : LauncherSettings.Favorites.TABLE_NAME, 179 context, validPackages); 180 181 Point targetSize = new Point(idp.numColumns, idp.numRows); 182 GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(), 183 srcReader, destReader, idp.numDatabaseHotseatIcons, targetSize); 184 task.migrate(idp); 185 186 if (!migrateForPreview) { 187 dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE); 188 } 189 190 t.commit(); 191 return true; 192 } catch (Exception e) { 193 Log.e(TAG, "Error during grid migration", e); 194 195 return false; 196 } finally { 197 Log.v(TAG, "Workspace migration completed in " 198 + (System.currentTimeMillis() - migrationStartTime)); 199 200 if (!migrateForPreview) { 201 // Save current configuration, so that the migration does not run again. 202 new DeviceGridState(idp).writeToPrefs(context); 203 } 204 } 205 } 206 207 @VisibleForTesting migrate(InvariantDeviceProfile idp)208 protected boolean migrate(InvariantDeviceProfile idp) { 209 if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) { 210 return false; 211 } 212 213 // Migrate hotseat 214 HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader, 215 mDestReader, mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff); 216 hotseatSolution.find(); 217 218 // Sort the items by the reading order. 219 Collections.sort(mWorkspaceDiff); 220 221 // Migrate workspace. 222 // First we create a collection of the screens 223 List<Integer> screens = new ArrayList<>(); 224 for (int screenId = 0; screenId <= mDestReader.mLastScreenId; screenId++) { 225 screens.add(screenId); 226 } 227 228 // Then we place the items on the screens 229 for (int screenId : screens) { 230 if (DEBUG) { 231 Log.d(TAG, "Migrating " + screenId); 232 } 233 GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader, 234 mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff); 235 workspaceSolution.find(); 236 if (mWorkspaceDiff.isEmpty()) { 237 break; 238 } 239 } 240 241 // In case the new grid is smaller, there might be some leftover items that don't fit on 242 // any of the screens, in this case we add them to new screens until all of them are placed. 243 int screenId = mDestReader.mLastScreenId + 1; 244 while (!mWorkspaceDiff.isEmpty()) { 245 GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader, 246 mDestReader, mContext, screenId, mTrgX, mTrgY, mWorkspaceDiff); 247 workspaceSolution.find(); 248 screenId++; 249 } 250 return true; 251 } 252 253 /** Return what's in the src but not in the dest */ calcDiff(List<DbEntry> src, List<DbEntry> dest)254 private static List<DbEntry> calcDiff(List<DbEntry> src, List<DbEntry> dest) { 255 Set<String> destIntentSet = new HashSet<>(); 256 Set<Map<String, Integer>> destFolderIntentSet = new HashSet<>(); 257 for (DbEntry entry : dest) { 258 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 259 destFolderIntentSet.add(getFolderIntents(entry)); 260 } else { 261 destIntentSet.add(entry.mIntent); 262 } 263 } 264 List<DbEntry> diff = new ArrayList<>(); 265 for (DbEntry entry : src) { 266 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 267 if (!destFolderIntentSet.contains(getFolderIntents(entry))) { 268 diff.add(entry); 269 } 270 } else { 271 if (!destIntentSet.contains(entry.mIntent)) { 272 diff.add(entry); 273 } 274 } 275 } 276 return diff; 277 } 278 getFolderIntents(DbEntry entry)279 private static Map<String, Integer> getFolderIntents(DbEntry entry) { 280 Map<String, Integer> folder = new HashMap<>(); 281 for (String intent : entry.mFolderItems.keySet()) { 282 folder.put(intent, entry.mFolderItems.get(intent).size()); 283 } 284 return folder; 285 } 286 insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry, String srcTableName, String destTableName)287 private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry, 288 String srcTableName, String destTableName) { 289 int id = copyEntryAndUpdate(db, context, entry, srcTableName, destTableName); 290 291 if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 292 for (Set<Integer> itemIds : entry.mFolderItems.values()) { 293 for (int itemId : itemIds) { 294 copyEntryAndUpdate(db, context, itemId, id, srcTableName, destTableName); 295 } 296 } 297 } 298 } 299 copyEntryAndUpdate(SQLiteDatabase db, Context context, DbEntry entry, String srcTableName, String destTableName)300 private static int copyEntryAndUpdate(SQLiteDatabase db, Context context, 301 DbEntry entry, String srcTableName, String destTableName) { 302 return copyEntryAndUpdate(db, context, entry, -1, -1, srcTableName, destTableName); 303 } 304 copyEntryAndUpdate(SQLiteDatabase db, Context context, int id, int folderId, String srcTableName, String destTableName)305 private static int copyEntryAndUpdate(SQLiteDatabase db, Context context, 306 int id, int folderId, String srcTableName, String destTableName) { 307 return copyEntryAndUpdate(db, context, null, id, folderId, srcTableName, destTableName); 308 } 309 copyEntryAndUpdate(SQLiteDatabase db, Context context, DbEntry entry, int id, int folderId, String srcTableName, String destTableName)310 private static int copyEntryAndUpdate(SQLiteDatabase db, Context context, 311 DbEntry entry, int id, int folderId, String srcTableName, String destTableName) { 312 int newId = -1; 313 Cursor c = db.query(srcTableName, null, 314 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'", 315 null, null, null, null); 316 while (c.moveToNext()) { 317 ContentValues values = new ContentValues(); 318 DatabaseUtils.cursorRowToContentValues(c, values); 319 if (entry != null) { 320 entry.updateContentValues(values); 321 } else { 322 values.put(LauncherSettings.Favorites.CONTAINER, folderId); 323 } 324 newId = LauncherSettings.Settings.call(context.getContentResolver(), 325 LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt( 326 LauncherSettings.Settings.EXTRA_VALUE); 327 values.put(LauncherSettings.Favorites._ID, newId); 328 db.insert(destTableName, null, values); 329 } 330 c.close(); 331 return newId; 332 } 333 removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds)334 private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) { 335 db.delete(tableName, 336 Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null); 337 } 338 getValidPackages(Context context)339 private static HashSet<String> getValidPackages(Context context) { 340 // Initialize list of valid packages. This contain all the packages which are already on 341 // the device and packages which are being installed. Any item which doesn't belong to 342 // this set is removed. 343 // Since the loader removes such items anyway, removing these items here doesn't cause 344 // any extra data loss and gives us more free space on the grid for better migration. 345 HashSet<String> validPackages = new HashSet<>(); 346 for (PackageInfo info : context.getPackageManager() 347 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { 348 validPackages.add(info.packageName); 349 } 350 InstallSessionHelper.INSTANCE.get(context) 351 .getActiveSessions().keySet() 352 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName)); 353 return validPackages; 354 } 355 356 protected static class GridPlacementSolution { 357 358 private final SQLiteDatabase mDb; 359 private final DbReader mSrcReader; 360 private final DbReader mDestReader; 361 private final Context mContext; 362 private final GridOccupancy mOccupied; 363 private final int mScreenId; 364 private final int mTrgX; 365 private final int mTrgY; 366 private final List<DbEntry> mItemsToPlace; 367 368 private int mNextStartX; 369 private int mNextStartY; 370 GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader, Context context, int screenId, int trgX, int trgY, List<DbEntry> itemsToPlace)371 GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader, 372 Context context, int screenId, int trgX, int trgY, List<DbEntry> itemsToPlace) { 373 mDb = db; 374 mSrcReader = srcReader; 375 mDestReader = destReader; 376 mContext = context; 377 mOccupied = new GridOccupancy(trgX, trgY); 378 mScreenId = screenId; 379 mTrgX = trgX; 380 mTrgY = trgY; 381 mNextStartX = 0; 382 mNextStartY = mTrgY - 1; 383 List<DbEntry> existedEntries = mDestReader.mWorkspaceEntriesByScreenId.get(screenId); 384 if (existedEntries != null) { 385 for (DbEntry entry : existedEntries) { 386 mOccupied.markCells(entry, true); 387 } 388 } 389 mItemsToPlace = itemsToPlace; 390 } 391 find()392 public void find() { 393 Iterator<DbEntry> iterator = mItemsToPlace.iterator(); 394 while (iterator.hasNext()) { 395 final DbEntry entry = iterator.next(); 396 if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { 397 iterator.remove(); 398 continue; 399 } 400 if (findPlacement(entry)) { 401 insertEntryInDb(mDb, mContext, entry, mSrcReader.mTableName, 402 mDestReader.mTableName); 403 iterator.remove(); 404 } 405 } 406 } 407 408 /** 409 * Search for the next possible placement of an icon. (mNextStartX, mNextStartY) serves as 410 * a memoization of last placement, we can start our search for next placement from there 411 * to speed up the search. 412 */ findPlacement(DbEntry entry)413 private boolean findPlacement(DbEntry entry) { 414 for (int y = mNextStartY; y >= (mScreenId == 0 ? 1 /* smartspace */ : 0); y--) { 415 for (int x = mNextStartX; x < mTrgX; x++) { 416 boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY); 417 boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX, 418 entry.minSpanY); 419 if (minFits) { 420 entry.spanX = entry.minSpanX; 421 entry.spanY = entry.minSpanY; 422 } 423 if (fits || minFits) { 424 entry.screenId = mScreenId; 425 entry.cellX = x; 426 entry.cellY = y; 427 mOccupied.markCells(entry, true); 428 mNextStartX = x + entry.spanX; 429 mNextStartY = y; 430 return true; 431 } 432 } 433 mNextStartX = 0; 434 } 435 return false; 436 } 437 } 438 439 protected static class HotseatPlacementSolution { 440 441 private final SQLiteDatabase mDb; 442 private final DbReader mSrcReader; 443 private final DbReader mDestReader; 444 private final Context mContext; 445 private final HotseatOccupancy mOccupied; 446 private final List<DbEntry> mItemsToPlace; 447 HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader, Context context, int hotseatSize, List<DbEntry> placedHotseatItems, List<DbEntry> itemsToPlace)448 HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, DbReader destReader, 449 Context context, int hotseatSize, List<DbEntry> placedHotseatItems, 450 List<DbEntry> itemsToPlace) { 451 mDb = db; 452 mSrcReader = srcReader; 453 mDestReader = destReader; 454 mContext = context; 455 mOccupied = new HotseatOccupancy(hotseatSize); 456 for (DbEntry entry : placedHotseatItems) { 457 mOccupied.markCells(entry, true); 458 } 459 mItemsToPlace = itemsToPlace; 460 } 461 find()462 public void find() { 463 for (int i = 0; i < mOccupied.mCells.length; i++) { 464 if (!mOccupied.mCells[i] && !mItemsToPlace.isEmpty()) { 465 DbEntry entry = mItemsToPlace.remove(0); 466 entry.screenId = i; 467 // These values does not affect the item position, but we should set them 468 // to something other than -1. 469 entry.cellX = i; 470 entry.cellY = 0; 471 insertEntryInDb(mDb, mContext, entry, mSrcReader.mTableName, 472 mDestReader.mTableName); 473 mOccupied.markCells(entry, true); 474 } 475 } 476 } 477 478 private class HotseatOccupancy { 479 480 private final boolean[] mCells; 481 HotseatOccupancy(int hotseatSize)482 private HotseatOccupancy(int hotseatSize) { 483 mCells = new boolean[hotseatSize]; 484 } 485 markCells(ItemInfo item, boolean value)486 private void markCells(ItemInfo item, boolean value) { 487 mCells[item.screenId] = value; 488 } 489 } 490 } 491 492 protected static class DbReader { 493 494 private final SQLiteDatabase mDb; 495 private final String mTableName; 496 private final Context mContext; 497 private final HashSet<String> mValidPackages; 498 private int mLastScreenId = -1; 499 500 private final ArrayList<DbEntry> mHotseatEntries = new ArrayList<>(); 501 private final ArrayList<DbEntry> mWorkspaceEntries = new ArrayList<>(); 502 private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId = 503 new ArrayMap<>(); 504 DbReader(SQLiteDatabase db, String tableName, Context context, HashSet<String> validPackages)505 DbReader(SQLiteDatabase db, String tableName, Context context, 506 HashSet<String> validPackages) { 507 mDb = db; 508 mTableName = tableName; 509 mContext = context; 510 mValidPackages = validPackages; 511 } 512 loadHotseatEntries()513 protected ArrayList<DbEntry> loadHotseatEntries() { 514 Cursor c = queryWorkspace( 515 new String[]{ 516 LauncherSettings.Favorites._ID, // 0 517 LauncherSettings.Favorites.ITEM_TYPE, // 1 518 LauncherSettings.Favorites.INTENT, // 2 519 LauncherSettings.Favorites.SCREEN}, // 3 520 LauncherSettings.Favorites.CONTAINER + " = " 521 + LauncherSettings.Favorites.CONTAINER_HOTSEAT); 522 523 final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 524 final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 525 final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); 526 final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 527 528 IntArray entriesToRemove = new IntArray(); 529 while (c.moveToNext()) { 530 DbEntry entry = new DbEntry(); 531 entry.id = c.getInt(indexId); 532 entry.itemType = c.getInt(indexItemType); 533 entry.screenId = c.getInt(indexScreen); 534 535 try { 536 // calculate weight 537 switch (entry.itemType) { 538 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 539 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: 540 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: { 541 entry.mIntent = c.getString(indexIntent); 542 verifyIntent(c.getString(indexIntent)); 543 break; 544 } 545 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: { 546 int total = getFolderItemsCount(entry); 547 if (total == 0) { 548 throw new Exception("Folder is empty"); 549 } 550 break; 551 } 552 default: 553 throw new Exception("Invalid item type"); 554 } 555 } catch (Exception e) { 556 if (DEBUG) { 557 Log.d(TAG, "Removing item " + entry.id, e); 558 } 559 entriesToRemove.add(entry.id); 560 continue; 561 } 562 mHotseatEntries.add(entry); 563 } 564 removeEntryFromDb(mDb, mTableName, entriesToRemove); 565 c.close(); 566 return mHotseatEntries; 567 } 568 loadAllWorkspaceEntries()569 protected ArrayList<DbEntry> loadAllWorkspaceEntries() { 570 Cursor c = queryWorkspace( 571 new String[]{ 572 LauncherSettings.Favorites._ID, // 0 573 LauncherSettings.Favorites.ITEM_TYPE, // 1 574 LauncherSettings.Favorites.SCREEN, // 2 575 LauncherSettings.Favorites.CELLX, // 3 576 LauncherSettings.Favorites.CELLY, // 4 577 LauncherSettings.Favorites.SPANX, // 5 578 LauncherSettings.Favorites.SPANY, // 6 579 LauncherSettings.Favorites.INTENT, // 7 580 LauncherSettings.Favorites.APPWIDGET_PROVIDER, // 8 581 LauncherSettings.Favorites.APPWIDGET_ID}, // 9 582 LauncherSettings.Favorites.CONTAINER + " = " 583 + LauncherSettings.Favorites.CONTAINER_DESKTOP); 584 return loadWorkspaceEntries(c); 585 } 586 loadWorkspaceEntries(Cursor c)587 private ArrayList<DbEntry> loadWorkspaceEntries(Cursor c) { 588 final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 589 final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 590 final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 591 final int indexCellX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 592 final int indexCellY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 593 final int indexSpanX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); 594 final int indexSpanY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); 595 final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); 596 final int indexAppWidgetProvider = c.getColumnIndexOrThrow( 597 LauncherSettings.Favorites.APPWIDGET_PROVIDER); 598 final int indexAppWidgetId = c.getColumnIndexOrThrow( 599 LauncherSettings.Favorites.APPWIDGET_ID); 600 601 IntArray entriesToRemove = new IntArray(); 602 WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext); 603 while (c.moveToNext()) { 604 DbEntry entry = new DbEntry(); 605 entry.id = c.getInt(indexId); 606 entry.itemType = c.getInt(indexItemType); 607 entry.screenId = c.getInt(indexScreen); 608 mLastScreenId = Math.max(mLastScreenId, entry.screenId); 609 entry.cellX = c.getInt(indexCellX); 610 entry.cellY = c.getInt(indexCellY); 611 entry.spanX = c.getInt(indexSpanX); 612 entry.spanY = c.getInt(indexSpanY); 613 614 try { 615 // calculate weight 616 switch (entry.itemType) { 617 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 618 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: 619 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: { 620 entry.mIntent = c.getString(indexIntent); 621 verifyIntent(entry.mIntent); 622 break; 623 } 624 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: { 625 entry.mProvider = c.getString(indexAppWidgetProvider); 626 ComponentName cn = ComponentName.unflattenFromString(entry.mProvider); 627 verifyPackage(cn.getPackageName()); 628 629 int widgetId = c.getInt(indexAppWidgetId); 630 LauncherAppWidgetProviderInfo pInfo = 631 widgetManagerHelper.getLauncherAppWidgetInfo(widgetId); 632 Point spans = null; 633 if (pInfo != null) { 634 spans = pInfo.getMinSpans(); 635 } 636 if (spans != null) { 637 entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; 638 entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; 639 } else { 640 // Assume that the widget be resized down to 2x2 641 entry.minSpanX = entry.minSpanY = 2; 642 } 643 644 break; 645 } 646 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: { 647 int total = getFolderItemsCount(entry); 648 if (total == 0) { 649 throw new Exception("Folder is empty"); 650 } 651 break; 652 } 653 default: 654 throw new Exception("Invalid item type"); 655 } 656 } catch (Exception e) { 657 if (DEBUG) { 658 Log.d(TAG, "Removing item " + entry.id, e); 659 } 660 entriesToRemove.add(entry.id); 661 continue; 662 } 663 mWorkspaceEntries.add(entry); 664 if (!mWorkspaceEntriesByScreenId.containsKey(entry.screenId)) { 665 mWorkspaceEntriesByScreenId.put(entry.screenId, new ArrayList<>()); 666 } 667 mWorkspaceEntriesByScreenId.get(entry.screenId).add(entry); 668 } 669 removeEntryFromDb(mDb, mTableName, entriesToRemove); 670 c.close(); 671 return mWorkspaceEntries; 672 } 673 getFolderItemsCount(DbEntry entry)674 private int getFolderItemsCount(DbEntry entry) { 675 Cursor c = queryWorkspace( 676 new String[]{LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT}, 677 LauncherSettings.Favorites.CONTAINER + " = " + entry.id); 678 679 int total = 0; 680 while (c.moveToNext()) { 681 try { 682 int id = c.getInt(0); 683 String intent = c.getString(1); 684 verifyIntent(intent); 685 total++; 686 if (!entry.mFolderItems.containsKey(intent)) { 687 entry.mFolderItems.put(intent, new HashSet<>()); 688 } 689 entry.mFolderItems.get(intent).add(id); 690 } catch (Exception e) { 691 removeEntryFromDb(mDb, mTableName, IntArray.wrap(c.getInt(0))); 692 } 693 } 694 c.close(); 695 return total; 696 } 697 queryWorkspace(String[] columns, String where)698 private Cursor queryWorkspace(String[] columns, String where) { 699 return mDb.query(mTableName, columns, where, null, null, null, null); 700 } 701 702 /** Verifies if the mIntent should be restored. */ verifyIntent(String intentStr)703 private void verifyIntent(String intentStr) 704 throws Exception { 705 Intent intent = Intent.parseUri(intentStr, 0); 706 if (intent.getComponent() != null) { 707 verifyPackage(intent.getComponent().getPackageName()); 708 } else if (intent.getPackage() != null) { 709 // Only verify package if the component was null. 710 verifyPackage(intent.getPackage()); 711 } 712 } 713 714 /** Verifies if the package should be restored */ verifyPackage(String packageName)715 private void verifyPackage(String packageName) 716 throws Exception { 717 if (!mValidPackages.contains(packageName)) { 718 // TODO(b/151468819): Handle promise app icon restoration during grid migration. 719 throw new Exception("Package not available"); 720 } 721 } 722 } 723 724 protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> { 725 726 private String mIntent; 727 private String mProvider; 728 private Map<String, Set<Integer>> mFolderItems = new HashMap<>(); 729 730 /** Comparator according to the reading order */ 731 @Override compareTo(DbEntry another)732 public int compareTo(DbEntry another) { 733 if (screenId != another.screenId) { 734 return Integer.compare(screenId, another.screenId); 735 } 736 if (cellY != another.cellY) { 737 return -Integer.compare(cellY, another.cellY); 738 } 739 return Integer.compare(cellX, another.cellX); 740 } 741 742 @Override equals(Object o)743 public boolean equals(Object o) { 744 if (this == o) return true; 745 if (o == null || getClass() != o.getClass()) return false; 746 DbEntry entry = (DbEntry) o; 747 return Objects.equals(mIntent, entry.mIntent); 748 } 749 750 @Override hashCode()751 public int hashCode() { 752 return Objects.hash(mIntent); 753 } 754 updateContentValues(ContentValues values)755 public void updateContentValues(ContentValues values) { 756 values.put(LauncherSettings.Favorites.SCREEN, screenId); 757 values.put(LauncherSettings.Favorites.CELLX, cellX); 758 values.put(LauncherSettings.Favorites.CELLY, cellY); 759 values.put(LauncherSettings.Favorites.SPANX, spanX); 760 values.put(LauncherSettings.Favorites.SPANY, spanY); 761 } 762 } 763 } 764