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