1 /* 2 * Copyright (C) 2017 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.LauncherSettings.Favorites.TABLE_NAME; 20 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET; 21 22 import android.content.ComponentName; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.LauncherActivityInfo; 27 import android.content.pm.LauncherApps; 28 import android.database.Cursor; 29 import android.database.CursorWrapper; 30 import android.os.UserHandle; 31 import android.provider.BaseColumns; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.util.LongSparseArray; 35 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.launcher3.InvariantDeviceProfile; 40 import com.android.launcher3.LauncherAppState; 41 import com.android.launcher3.LauncherSettings.Favorites; 42 import com.android.launcher3.Utilities; 43 import com.android.launcher3.Workspace; 44 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger; 45 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError; 46 import com.android.launcher3.config.FeatureFlags; 47 import com.android.launcher3.icons.IconCache; 48 import com.android.launcher3.logging.FileLog; 49 import com.android.launcher3.model.data.AppInfo; 50 import com.android.launcher3.model.data.IconRequestInfo; 51 import com.android.launcher3.model.data.ItemInfo; 52 import com.android.launcher3.model.data.WorkspaceItemInfo; 53 import com.android.launcher3.pm.UserCache; 54 import com.android.launcher3.shortcuts.ShortcutKey; 55 import com.android.launcher3.util.ApiWrapper; 56 import com.android.launcher3.util.ContentWriter; 57 import com.android.launcher3.util.GridOccupancy; 58 import com.android.launcher3.util.IntArray; 59 import com.android.launcher3.util.IntSparseArrayMap; 60 import com.android.launcher3.util.PackageManagerHelper; 61 import com.android.launcher3.util.UserIconInfo; 62 63 import java.net.URISyntaxException; 64 import java.security.InvalidParameterException; 65 66 /** 67 * Extension of {@link Cursor} with utility methods for workspace loading. 68 */ 69 public class LoaderCursor extends CursorWrapper { 70 71 private static final String TAG = "LoaderCursor"; 72 73 private final LongSparseArray<UserHandle> allUsers; 74 75 private final LauncherAppState mApp; 76 private final Context mContext; 77 private final PackageManagerHelper mPmHelper; 78 private final IconCache mIconCache; 79 private final InvariantDeviceProfile mIDP; 80 private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger; 81 82 private final IntArray mItemsToRemove = new IntArray(); 83 private final IntArray mRestoredRows = new IntArray(); 84 private final IntSparseArrayMap<GridOccupancy> mOccupied = new IntSparseArrayMap<>(); 85 86 private final int mIconIndex; 87 public final int mTitleIndex; 88 89 private final int mIdIndex; 90 private final int mContainerIndex; 91 private final int mItemTypeIndex; 92 private final int mScreenIndex; 93 private final int mCellXIndex; 94 private final int mCellYIndex; 95 private final int mProfileIdIndex; 96 private final int mRestoredIndex; 97 private final int mIntentIndex; 98 99 private final int mAppWidgetIdIndex; 100 private final int mAppWidgetProviderIndex; 101 private final int mSpanXIndex; 102 private final int mSpanYIndex; 103 private final int mRankIndex; 104 private final int mOptionsIndex; 105 private final int mAppWidgetSourceIndex; 106 107 @Nullable 108 private LauncherActivityInfo mActivityInfo; 109 110 // Properties loaded per iteration 111 public long serialNumber; 112 public UserHandle user; 113 public int id; 114 public int container; 115 public int itemType; 116 public int restoreFlag; 117 LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState, PackageManagerHelper pmHelper, @Nullable LauncherRestoreEventLogger restoreEventLogger)118 public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState, 119 PackageManagerHelper pmHelper, 120 @Nullable LauncherRestoreEventLogger restoreEventLogger) { 121 super(cursor); 122 123 mApp = app; 124 allUsers = userManagerState.allUsers; 125 mContext = app.getContext(); 126 mIconCache = app.getIconCache(); 127 mPmHelper = pmHelper; 128 mIDP = app.getInvariantDeviceProfile(); 129 mRestoreEventLogger = restoreEventLogger; 130 131 // Init column indices 132 mIconIndex = getColumnIndexOrThrow(Favorites.ICON); 133 mTitleIndex = getColumnIndexOrThrow(Favorites.TITLE); 134 135 mIdIndex = getColumnIndexOrThrow(Favorites._ID); 136 mContainerIndex = getColumnIndexOrThrow(Favorites.CONTAINER); 137 mItemTypeIndex = getColumnIndexOrThrow(Favorites.ITEM_TYPE); 138 mScreenIndex = getColumnIndexOrThrow(Favorites.SCREEN); 139 mCellXIndex = getColumnIndexOrThrow(Favorites.CELLX); 140 mCellYIndex = getColumnIndexOrThrow(Favorites.CELLY); 141 mProfileIdIndex = getColumnIndexOrThrow(Favorites.PROFILE_ID); 142 mRestoredIndex = getColumnIndexOrThrow(Favorites.RESTORED); 143 mIntentIndex = getColumnIndexOrThrow(Favorites.INTENT); 144 145 mAppWidgetIdIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_ID); 146 mAppWidgetProviderIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); 147 mSpanXIndex = getColumnIndexOrThrow(Favorites.SPANX); 148 mSpanYIndex = getColumnIndexOrThrow(Favorites.SPANY); 149 mRankIndex = getColumnIndexOrThrow(Favorites.RANK); 150 mOptionsIndex = getColumnIndexOrThrow(Favorites.OPTIONS); 151 mAppWidgetSourceIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_SOURCE); 152 } 153 154 @Override moveToNext()155 public boolean moveToNext() { 156 boolean result = super.moveToNext(); 157 if (result) { 158 mActivityInfo = null; 159 160 // Load common properties. 161 itemType = getInt(mItemTypeIndex); 162 container = getInt(mContainerIndex); 163 id = getInt(mIdIndex); 164 serialNumber = getInt(mProfileIdIndex); 165 user = allUsers.get(serialNumber); 166 restoreFlag = getInt(mRestoredIndex); 167 } 168 return result; 169 } 170 parseIntent()171 public Intent parseIntent() { 172 String intentDescription = getString(mIntentIndex); 173 try { 174 return TextUtils.isEmpty(intentDescription) ? 175 null : Intent.parseUri(intentDescription, 0); 176 } catch (URISyntaxException e) { 177 Log.e(TAG, "Error parsing Intent"); 178 return null; 179 } 180 } 181 182 @VisibleForTesting loadSimpleWorkspaceItem()183 public WorkspaceItemInfo loadSimpleWorkspaceItem() { 184 final WorkspaceItemInfo info = new WorkspaceItemInfo(); 185 info.intent = new Intent(); 186 // Non-app shortcuts are only supported for current user. 187 info.user = user; 188 info.itemType = itemType; 189 info.title = getTitle(); 190 // the fallback icon 191 if (!loadIcon(info)) { 192 info.bitmap = mIconCache.getDefaultIcon(info.user); 193 } 194 195 // TODO: If there's an explicit component and we can't install that, delete it. 196 197 return info; 198 } 199 200 /** 201 * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource. 202 */ loadIcon(WorkspaceItemInfo info)203 protected boolean loadIcon(WorkspaceItemInfo info) { 204 return createIconRequestInfo(info, false).loadWorkspaceIcon(mContext); 205 } 206 createIconRequestInfo( WorkspaceItemInfo wai, boolean useLowResIcon)207 public IconRequestInfo<WorkspaceItemInfo> createIconRequestInfo( 208 WorkspaceItemInfo wai, boolean useLowResIcon) { 209 byte[] iconBlob = itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0 210 ? getIconBlob() : null; 211 212 return new IconRequestInfo<>(wai, mActivityInfo, iconBlob, useLowResIcon); 213 } 214 215 /** 216 * Returns the icon data for at the current position 217 */ getIconBlob()218 public byte[] getIconBlob() { 219 return getBlob(mIconIndex); 220 } 221 222 /** 223 * Returns the title or empty string 224 */ getTitle()225 public String getTitle() { 226 return Utilities.trim(getString(mTitleIndex)); 227 } 228 229 /** 230 * When loading an app widget for the workspace, returns it's app widget id 231 */ getAppWidgetId()232 public int getAppWidgetId() { 233 return getInt(mAppWidgetIdIndex); 234 } 235 236 /** 237 * When loading an app widget for the workspace, returns the widget provider 238 */ getAppWidgetProvider()239 public String getAppWidgetProvider() { 240 return getString(mAppWidgetProviderIndex); 241 } 242 243 /** 244 * Returns the x position for the item in the cell layout's grid 245 */ getSpanX()246 public int getSpanX() { 247 return getInt(mSpanXIndex); 248 } 249 250 /** 251 * Returns the y position for the item in the cell layout's grid 252 */ getSpanY()253 public int getSpanY() { 254 return getInt(mSpanYIndex); 255 } 256 257 /** 258 * Returns the rank for the item 259 */ getRank()260 public int getRank() { 261 return getInt(mRankIndex); 262 } 263 264 /** 265 * Returns the options for the item 266 */ getOptions()267 public int getOptions() { 268 return getInt(mOptionsIndex); 269 } 270 271 /** 272 * When loading an app widget for the workspace, returns it's app widget source 273 */ getAppWidgetSource()274 public int getAppWidgetSource() { 275 return getInt(mAppWidgetSourceIndex); 276 } 277 278 /** 279 * Returns the screen that the item is on 280 */ getScreen()281 public int getScreen() { 282 return getInt(mScreenIndex); 283 } 284 285 /** 286 * Returns the UX container that the item is in 287 */ getContainer()288 public int getContainer() { 289 return getInt(mContainerIndex); 290 } 291 292 /** 293 * Make an WorkspaceItemInfo object for a restored application or shortcut item that points 294 * to a package that is not yet installed on the system. 295 */ getRestoredItemInfo(Intent intent)296 public WorkspaceItemInfo getRestoredItemInfo(Intent intent) { 297 final WorkspaceItemInfo info = new WorkspaceItemInfo(); 298 info.user = user; 299 info.intent = intent; 300 301 // the fallback icon 302 if (!loadIcon(info)) { 303 mIconCache.getTitleAndIcon(info, false /* useLowResIcon */); 304 } 305 306 if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) { 307 String title = getTitle(); 308 if (!TextUtils.isEmpty(title)) { 309 info.title = Utilities.trim(title); 310 } 311 } else if (hasRestoreFlag(WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON)) { 312 if (TextUtils.isEmpty(info.title)) { 313 info.title = getTitle(); 314 } 315 } else { 316 throw new InvalidParameterException("Invalid restoreType " + restoreFlag); 317 } 318 319 info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user); 320 info.itemType = itemType; 321 info.status = restoreFlag; 322 return info; 323 } 324 getLauncherActivityInfo()325 public LauncherActivityInfo getLauncherActivityInfo() { 326 return mActivityInfo; 327 } 328 329 /** 330 * Make an WorkspaceItemInfo object for a shortcut that is an application. 331 */ getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)332 public WorkspaceItemInfo getAppShortcutInfo( 333 Intent intent, boolean allowMissingTarget, boolean useLowResIcon) { 334 return getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, true); 335 } 336 getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon, boolean loadIcon)337 public WorkspaceItemInfo getAppShortcutInfo( 338 Intent intent, boolean allowMissingTarget, boolean useLowResIcon, boolean loadIcon) { 339 if (user == null) { 340 Log.d(TAG, "Null user found in getShortcutInfo"); 341 return null; 342 } 343 344 ComponentName componentName = intent.getComponent(); 345 if (componentName == null) { 346 Log.d(TAG, "Missing component found in getShortcutInfo"); 347 return null; 348 } 349 350 Intent newIntent = new Intent(Intent.ACTION_MAIN, null); 351 newIntent.addCategory(Intent.CATEGORY_LAUNCHER); 352 newIntent.setComponent(componentName); 353 mActivityInfo = mContext.getSystemService(LauncherApps.class) 354 .resolveActivity(newIntent, user); 355 if ((mActivityInfo == null) && !allowMissingTarget) { 356 Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName); 357 return null; 358 } 359 360 final WorkspaceItemInfo info = new WorkspaceItemInfo(); 361 info.user = user; 362 info.intent = newIntent; 363 UserCache userCache = UserCache.getInstance(mContext); 364 UserIconInfo userIconInfo = userCache.getUserInfo(user); 365 366 if (loadIcon) { 367 mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon); 368 if (mIconCache.isDefaultIcon(info.bitmap, user)) { 369 loadIcon(info); 370 } 371 } 372 373 if (mActivityInfo != null) { 374 AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo, 375 ApiWrapper.INSTANCE.get(mContext), mPmHelper); 376 } 377 378 // from the db 379 if (TextUtils.isEmpty(info.title)) { 380 if (loadIcon) { 381 info.title = getTitle(); 382 383 // fall back to the class name of the activity 384 if (info.title == null) { 385 info.title = componentName.getClassName(); 386 } 387 } else { 388 info.title = ""; 389 } 390 } 391 392 info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user); 393 return info; 394 } 395 396 /** 397 * Returns a {@link ContentWriter} which can be used to update the current item. 398 */ updater()399 public ContentWriter updater() { 400 return new ContentWriter(mContext, new ContentWriter.CommitParams( 401 mApp.getModel().getModelDbController(), 402 BaseColumns._ID + "= ?", new String[]{Integer.toString(id)})); 403 } 404 405 /** 406 * Marks the current item for removal 407 */ markDeleted(String reason, @RestoreError String errorType)408 public void markDeleted(String reason, @RestoreError String errorType) { 409 FileLog.e(TAG, reason); 410 mItemsToRemove.add(id); 411 if (mRestoreEventLogger != null) { 412 mRestoreEventLogger.logSingleFavoritesItemRestoreFailed(itemType, errorType); 413 } 414 } 415 416 /** 417 * Removes any items marked for removal. 418 * @return true is any item was removed. 419 */ commitDeleted()420 public boolean commitDeleted() { 421 if (mItemsToRemove.size() > 0) { 422 // Remove dead items 423 mApp.getModel().getModelDbController().delete(TABLE_NAME, 424 Utilities.createDbSelectionQuery(Favorites._ID, mItemsToRemove), null); 425 return true; 426 } 427 return false; 428 } 429 430 /** 431 * Marks the current item as restored 432 */ markRestored()433 public void markRestored() { 434 if (restoreFlag != 0) { 435 mRestoredRows.add(id); 436 restoreFlag = 0; 437 } 438 } 439 hasRestoreFlag(int flagMask)440 public boolean hasRestoreFlag(int flagMask) { 441 return (restoreFlag & flagMask) != 0; 442 } 443 commitRestoredItems()444 public void commitRestoredItems() { 445 if (mRestoredRows.size() > 0) { 446 // Update restored items that no longer require special handling 447 ContentValues values = new ContentValues(); 448 values.put(Favorites.RESTORED, 0); 449 mApp.getModel().getModelDbController().update(TABLE_NAME, values, 450 Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null); 451 } 452 if (mRestoreEventLogger != null) { 453 mRestoreEventLogger.reportLauncherRestoreResults(); 454 } 455 } 456 457 /** 458 * Returns true is the item is on workspace or hotseat 459 */ isOnWorkspaceOrHotseat()460 public boolean isOnWorkspaceOrHotseat() { 461 return container == Favorites.CONTAINER_DESKTOP || container == Favorites.CONTAINER_HOTSEAT; 462 } 463 464 /** 465 * Applies the following properties: 466 * {@link ItemInfo#id} 467 * {@link ItemInfo#container} 468 * {@link ItemInfo#screenId} 469 * {@link ItemInfo#cellX} 470 * {@link ItemInfo#cellY} 471 */ applyCommonProperties(ItemInfo info)472 public void applyCommonProperties(ItemInfo info) { 473 info.id = id; 474 info.container = container; 475 info.screenId = getInt(mScreenIndex); 476 info.cellX = getInt(mCellXIndex); 477 info.cellY = getInt(mCellYIndex); 478 } 479 checkAndAddItem(ItemInfo info, BgDataModel dataModel)480 public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) { 481 checkAndAddItem(info, dataModel, null); 482 } 483 484 /** 485 * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item, 486 * otherwise marks it for deletion. 487 */ checkAndAddItem( ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger)488 public void checkAndAddItem( 489 ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger) { 490 if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 491 // Ensure that it is a valid intent. An exception here will 492 // cause the item loading to get skipped 493 ShortcutKey.fromItemInfo(info); 494 } 495 if (checkItemPlacement(info, dataModel.isFirstPagePinnedItemEnabled)) { 496 dataModel.addItem(mContext, info, false, logger); 497 if (mRestoreEventLogger != null) { 498 mRestoreEventLogger.logSingleFavoritesItemRestored(itemType); 499 } 500 } else { 501 markDeleted("Item position overlap", RestoreError.INVALID_LOCATION); 502 } 503 } 504 505 /** 506 * check & update map of what's occupied; used to discard overlapping/invalid items 507 */ checkItemPlacement(ItemInfo item, boolean isFirstPagePinnedItemEnabled)508 protected boolean checkItemPlacement(ItemInfo item, boolean isFirstPagePinnedItemEnabled) { 509 int containerIndex = item.screenId; 510 if (item.container == Favorites.CONTAINER_HOTSEAT) { 511 final GridOccupancy hotseatOccupancy = 512 mOccupied.get(Favorites.CONTAINER_HOTSEAT); 513 514 if (item.screenId >= mIDP.numDatabaseHotseatIcons) { 515 Log.e(TAG, "Error loading shortcut " + item 516 + " into hotseat position " + item.screenId 517 + ", position out of bounds: (0 to " + (mIDP.numDatabaseHotseatIcons - 1) 518 + ")"); 519 return false; 520 } 521 522 if (hotseatOccupancy != null) { 523 if (hotseatOccupancy.cells[(int) item.screenId][0]) { 524 Log.e(TAG, "Error loading shortcut into hotseat " + item 525 + " into position (" + item.screenId + ":" + item.cellX + "," 526 + item.cellY + ") already occupied"); 527 return false; 528 } else { 529 hotseatOccupancy.cells[item.screenId][0] = true; 530 return true; 531 } 532 } else { 533 final GridOccupancy occupancy = new GridOccupancy(mIDP.numDatabaseHotseatIcons, 1); 534 occupancy.cells[item.screenId][0] = true; 535 mOccupied.put(Favorites.CONTAINER_HOTSEAT, occupancy); 536 return true; 537 } 538 } else if (item.container != Favorites.CONTAINER_DESKTOP) { 539 // Skip further checking if it is not the hotseat or workspace container 540 return true; 541 } 542 543 final int countX = mIDP.numColumns; 544 final int countY = mIDP.numRows; 545 if (item.container == Favorites.CONTAINER_DESKTOP && item.cellX < 0 || item.cellY < 0 546 || item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) { 547 Log.e(TAG, "Error loading shortcut " + item 548 + " into cell (" + containerIndex + "-" + item.screenId + ":" 549 + item.cellX + "," + item.cellY 550 + ") out of screen bounds ( " + countX + "x" + countY + ")"); 551 return false; 552 } 553 554 if (!mOccupied.containsKey(item.screenId)) { 555 GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1); 556 if (item.screenId == Workspace.FIRST_SCREEN_ID && (FeatureFlags.QSB_ON_FIRST_SCREEN 557 && !SHOULD_SHOW_FIRST_PAGE_WIDGET 558 && isFirstPagePinnedItemEnabled)) { 559 // Mark the first X columns (X is width of the search container) in the first row as 560 // occupied (if the feature is enabled) in order to account for the search 561 // container. 562 int spanX = mIDP.numSearchContainerColumns; 563 int spanY = 1; 564 screen.markCells(0, 0, spanX, spanY, true); 565 } 566 mOccupied.put(item.screenId, screen); 567 } 568 final GridOccupancy occupancy = mOccupied.get(item.screenId); 569 570 // Check if any workspace icons overlap with each other 571 if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) { 572 occupancy.markCells(item, true); 573 return true; 574 } else { 575 Log.e(TAG, "Error loading shortcut " + item 576 + " into cell (" + containerIndex + "-" + item.screenId + ":" 577 + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY 578 + ") already occupied"); 579 return false; 580 } 581 } 582 } 583