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