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