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