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.database.Cursor; 26 import android.database.CursorWrapper; 27 import android.graphics.Bitmap; 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.IconCache; 36 import com.android.launcher3.InvariantDeviceProfile; 37 import com.android.launcher3.ItemInfo; 38 import com.android.launcher3.LauncherAppState; 39 import com.android.launcher3.LauncherSettings; 40 import com.android.launcher3.ShortcutInfo; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.Workspace; 43 import com.android.launcher3.compat.LauncherAppsCompat; 44 import com.android.launcher3.compat.UserManagerCompat; 45 import com.android.launcher3.config.FeatureFlags; 46 import com.android.launcher3.graphics.LauncherIcons; 47 import com.android.launcher3.logging.FileLog; 48 import com.android.launcher3.util.ContentWriter; 49 import com.android.launcher3.util.GridOccupancy; 50 import com.android.launcher3.util.LongArrayMap; 51 import com.android.launcher3.util.PackageManagerHelper; 52 53 import java.net.URISyntaxException; 54 import java.security.InvalidParameterException; 55 import java.util.ArrayList; 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 UserManagerCompat mUserManager; 68 private final IconCache mIconCache; 69 private final InvariantDeviceProfile mIDP; 70 71 private final ArrayList<Long> itemsToRemove = new ArrayList<>(); 72 private final ArrayList<Long> restoredRows = new ArrayList<>(); 73 private final LongArrayMap<GridOccupancy> occupied = new LongArrayMap<>(); 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 long id; 94 public long 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 mUserManager = UserManagerCompat.getInstance(mContext); 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 = getLong(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 loadSimpleShortcut()148 public ShortcutInfo loadSimpleShortcut() { 149 final ShortcutInfo info = new ShortcutInfo(); 150 // Non-app shortcuts are only supported for current user. 151 info.user = user; 152 info.itemType = itemType; 153 info.title = getTitle(); 154 info.iconBitmap = loadIcon(info); 155 // the fallback icon 156 if (info.iconBitmap == null) { 157 info.iconBitmap = mIconCache.getDefaultIcon(info.user); 158 } 159 160 // TODO: If there's an explicit component and we can't install that, delete it. 161 162 return info; 163 } 164 165 /** 166 * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource. 167 */ loadIcon(ShortcutInfo info)168 protected Bitmap loadIcon(ShortcutInfo info) { 169 Bitmap icon = null; 170 if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { 171 String packageName = getString(iconPackageIndex); 172 String resourceName = getString(iconResourceIndex); 173 if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { 174 info.iconResource = new ShortcutIconResource(); 175 info.iconResource.packageName = packageName; 176 info.iconResource.resourceName = resourceName; 177 icon = LauncherIcons.createIconBitmap(info.iconResource, mContext); 178 } 179 } 180 if (icon == null) { 181 // Failed to load from resource, try loading from DB. 182 byte[] data = getBlob(iconIndex); 183 try { 184 icon = LauncherIcons.createIconBitmap( 185 BitmapFactory.decodeByteArray(data, 0, data.length), mContext); 186 } catch (Exception e) { 187 Log.e(TAG, "Failed to load icon for info " + info, e); 188 return null; 189 } 190 } 191 if (icon == null) { 192 Log.e(TAG, "Failed to load icon for info " + info); 193 } 194 return icon; 195 } 196 197 /** 198 * Returns the title or empty string 199 */ getTitle()200 private String getTitle() { 201 String title = getString(titleIndex); 202 return TextUtils.isEmpty(title) ? "" : Utilities.trim(title); 203 } 204 205 206 /** 207 * Make an ShortcutInfo object for a restored application or shortcut item that points 208 * to a package that is not yet installed on the system. 209 */ getRestoredItemInfo(Intent intent)210 public ShortcutInfo getRestoredItemInfo(Intent intent) { 211 final ShortcutInfo info = new ShortcutInfo(); 212 info.user = user; 213 info.intent = intent; 214 215 info.iconBitmap = loadIcon(info); 216 // the fallback icon 217 if (info.iconBitmap == null) { 218 mIconCache.getTitleAndIcon(info, false /* useLowResIcon */); 219 } 220 221 if (hasRestoreFlag(ShortcutInfo.FLAG_RESTORED_ICON)) { 222 String title = getTitle(); 223 if (!TextUtils.isEmpty(title)) { 224 info.title = Utilities.trim(title); 225 } 226 } else if (hasRestoreFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON)) { 227 if (TextUtils.isEmpty(info.title)) { 228 info.title = getTitle(); 229 } 230 } else { 231 throw new InvalidParameterException("Invalid restoreType " + restoreFlag); 232 } 233 234 info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); 235 info.itemType = itemType; 236 info.status = restoreFlag; 237 return info; 238 } 239 240 /** 241 * Make an ShortcutInfo object for a shortcut that is an application. 242 */ getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon)243 public ShortcutInfo getAppShortcutInfo( 244 Intent intent, boolean allowMissingTarget, boolean useLowResIcon) { 245 if (user == null) { 246 Log.d(TAG, "Null user found in getShortcutInfo"); 247 return null; 248 } 249 250 ComponentName componentName = intent.getComponent(); 251 if (componentName == null) { 252 Log.d(TAG, "Missing component found in getShortcutInfo"); 253 return null; 254 } 255 256 Intent newIntent = new Intent(Intent.ACTION_MAIN, null); 257 newIntent.addCategory(Intent.CATEGORY_LAUNCHER); 258 newIntent.setComponent(componentName); 259 LauncherActivityInfo lai = LauncherAppsCompat.getInstance(mContext) 260 .resolveActivity(newIntent, user); 261 if ((lai == null) && !allowMissingTarget) { 262 Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName); 263 return null; 264 } 265 266 final ShortcutInfo info = new ShortcutInfo(); 267 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 268 info.user = user; 269 info.intent = newIntent; 270 271 mIconCache.getTitleAndIcon(info, lai, useLowResIcon); 272 if (mIconCache.isDefaultIcon(info.iconBitmap, user)) { 273 Bitmap icon = loadIcon(info); 274 info.iconBitmap = icon != null ? icon : info.iconBitmap; 275 } 276 277 if (lai != null && PackageManagerHelper.isAppSuspended(lai.getApplicationInfo())) { 278 info.isDisabled = ShortcutInfo.FLAG_DISABLED_SUSPENDED; 279 } 280 281 // from the db 282 if (TextUtils.isEmpty(info.title)) { 283 info.title = getTitle(); 284 } 285 286 // fall back to the class name of the activity 287 if (info.title == null) { 288 info.title = componentName.getClassName(); 289 } 290 291 info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); 292 return info; 293 } 294 295 /** 296 * Returns a {@link ContentWriter} which can be used to update the current item. 297 */ updater()298 public ContentWriter updater() { 299 return new ContentWriter(mContext, new ContentWriter.CommitParams( 300 BaseColumns._ID + "= ?", new String[]{Long.toString(id)})); 301 } 302 303 /** 304 * Marks the current item for removal 305 */ markDeleted(String reason)306 public void markDeleted(String reason) { 307 FileLog.e(TAG, reason); 308 itemsToRemove.add(id); 309 } 310 311 /** 312 * Removes any items marked for removal. 313 * @return true is any item was removed. 314 */ commitDeleted()315 public boolean commitDeleted() { 316 if (itemsToRemove.size() > 0) { 317 // Remove dead items 318 mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, 319 Utilities.createDbSelectionQuery( 320 LauncherSettings.Favorites._ID, itemsToRemove), null); 321 return true; 322 } 323 return false; 324 } 325 326 /** 327 * Marks the current item as restored 328 */ markRestored()329 public void markRestored() { 330 if (restoreFlag != 0) { 331 restoredRows.add(id); 332 restoreFlag = 0; 333 } 334 } 335 hasRestoreFlag(int flagMask)336 public boolean hasRestoreFlag(int flagMask) { 337 return (restoreFlag & flagMask) != 0; 338 } 339 commitRestoredItems()340 public void commitRestoredItems() { 341 if (restoredRows.size() > 0) { 342 // Update restored items that no longer require special handling 343 ContentValues values = new ContentValues(); 344 values.put(LauncherSettings.Favorites.RESTORED, 0); 345 mContext.getContentResolver().update(LauncherSettings.Favorites.CONTENT_URI, values, 346 Utilities.createDbSelectionQuery( 347 LauncherSettings.Favorites._ID, restoredRows), null); 348 } 349 } 350 351 /** 352 * Returns true is the item is on workspace or hotseat 353 */ isOnWorkspaceOrHotseat()354 public boolean isOnWorkspaceOrHotseat() { 355 return container == LauncherSettings.Favorites.CONTAINER_DESKTOP || 356 container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; 357 } 358 359 /** 360 * Applies the following properties: 361 * {@link ItemInfo#id} 362 * {@link ItemInfo#container} 363 * {@link ItemInfo#screenId} 364 * {@link ItemInfo#cellX} 365 * {@link ItemInfo#cellY} 366 */ applyCommonProperties(ItemInfo info)367 public void applyCommonProperties(ItemInfo info) { 368 info.id = id; 369 info.container = container; 370 info.screenId = getInt(screenIndex); 371 info.cellX = getInt(cellXIndex); 372 info.cellY = getInt(cellYIndex); 373 } 374 375 /** 376 * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item, 377 * otherwise marks it for deletion. 378 */ checkAndAddItem(ItemInfo info, BgDataModel dataModel)379 public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) { 380 if (checkItemPlacement(info, dataModel.workspaceScreens)) { 381 dataModel.addItem(mContext, info, false); 382 } else { 383 markDeleted("Item position overlap"); 384 } 385 } 386 387 /** 388 * check & update map of what's occupied; used to discard overlapping/invalid items 389 */ checkItemPlacement(ItemInfo item, ArrayList<Long> workspaceScreens)390 protected boolean checkItemPlacement(ItemInfo item, ArrayList<Long> workspaceScreens) { 391 long containerIndex = item.screenId; 392 if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 393 // Return early if we detect that an item is under the hotseat button 394 if (!FeatureFlags.NO_ALL_APPS_ICON && 395 mIDP.isAllAppsButtonRank((int) item.screenId)) { 396 Log.e(TAG, "Error loading shortcut into hotseat " + item 397 + " into position (" + item.screenId + ":" + item.cellX + "," 398 + item.cellY + ") occupied by all apps"); 399 return false; 400 } 401 402 final GridOccupancy hotseatOccupancy = 403 occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT); 404 405 if (item.screenId >= mIDP.numHotseatIcons) { 406 Log.e(TAG, "Error loading shortcut " + item 407 + " into hotseat position " + item.screenId 408 + ", position out of bounds: (0 to " + (mIDP.numHotseatIcons - 1) 409 + ")"); 410 return false; 411 } 412 413 if (hotseatOccupancy != null) { 414 if (hotseatOccupancy.cells[(int) item.screenId][0]) { 415 Log.e(TAG, "Error loading shortcut into hotseat " + item 416 + " into position (" + item.screenId + ":" + item.cellX + "," 417 + item.cellY + ") already occupied"); 418 return false; 419 } else { 420 hotseatOccupancy.cells[(int) item.screenId][0] = true; 421 return true; 422 } 423 } else { 424 final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1); 425 occupancy.cells[(int) item.screenId][0] = true; 426 occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy); 427 return true; 428 } 429 } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 430 if (!workspaceScreens.contains((Long) item.screenId)) { 431 // The item has an invalid screen id. 432 return false; 433 } 434 } else { 435 // Skip further checking if it is not the hotseat or workspace container 436 return true; 437 } 438 439 final int countX = mIDP.numColumns; 440 final int countY = mIDP.numRows; 441 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && 442 item.cellX < 0 || item.cellY < 0 || 443 item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) { 444 Log.e(TAG, "Error loading shortcut " + item 445 + " into cell (" + containerIndex + "-" + item.screenId + ":" 446 + item.cellX + "," + item.cellY 447 + ") out of screen bounds ( " + countX + "x" + countY + ")"); 448 return false; 449 } 450 451 if (!occupied.containsKey(item.screenId)) { 452 GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1); 453 if (item.screenId == Workspace.FIRST_SCREEN_ID) { 454 // Mark the first row as occupied (if the feature is enabled) 455 // in order to account for the QSB. 456 screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN); 457 } 458 occupied.put(item.screenId, screen); 459 } 460 final GridOccupancy occupancy = occupied.get(item.screenId); 461 462 // Check if any workspace icons overlap with each other 463 if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) { 464 occupancy.markCells(item, true); 465 return true; 466 } else { 467 Log.e(TAG, "Error loading shortcut " + item 468 + " into cell (" + containerIndex + "-" + item.screenId + ":" 469 + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY 470 + ") already occupied"); 471 return false; 472 } 473 } 474 } 475