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