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