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