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