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