1 /* 2 * Copyright (C) 2016 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 package com.android.launcher3.model; 17 18 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 19 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED; 20 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; 21 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED; 22 import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER; 23 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; 24 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 25 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 26 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON; 27 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.LauncherActivityInfo; 32 import android.content.pm.LauncherApps; 33 import android.content.pm.ShortcutInfo; 34 import android.os.UserHandle; 35 import android.os.UserManager; 36 import android.util.Log; 37 38 import androidx.annotation.NonNull; 39 40 import com.android.launcher3.Flags; 41 import com.android.launcher3.LauncherModel.ModelUpdateTask; 42 import com.android.launcher3.LauncherSettings.Favorites; 43 import com.android.launcher3.config.FeatureFlags; 44 import com.android.launcher3.icons.IconCache; 45 import com.android.launcher3.logging.FileLog; 46 import com.android.launcher3.model.data.ItemInfo; 47 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 48 import com.android.launcher3.model.data.WorkspaceItemInfo; 49 import com.android.launcher3.pm.PackageInstallInfo; 50 import com.android.launcher3.pm.UserCache; 51 import com.android.launcher3.shortcuts.ShortcutRequest; 52 import com.android.launcher3.util.ApiWrapper; 53 import com.android.launcher3.util.FlagOp; 54 import com.android.launcher3.util.IntSet; 55 import com.android.launcher3.util.ItemInfoMatcher; 56 import com.android.launcher3.util.PackageManagerHelper; 57 import com.android.launcher3.util.PackageUserKey; 58 import com.android.launcher3.util.SafeCloseable; 59 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.Collections; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.function.Predicate; 68 import java.util.stream.Collectors; 69 70 /** 71 * Handles updates due to changes in package manager (app installed/updated/removed) 72 * or when a user availability changes. 73 */ 74 @SuppressWarnings("NewApi") 75 public class PackageUpdatedTask implements ModelUpdateTask { 76 77 // TODO(b/290090023): Set to false after root causing is done. 78 private static final String TAG = "PackageUpdatedTask"; 79 private static final boolean DEBUG = true; 80 81 public static final int OP_NONE = 0; 82 public static final int OP_ADD = 1; 83 public static final int OP_UPDATE = 2; 84 public static final int OP_REMOVE = 3; // uninstalled 85 public static final int OP_UNAVAILABLE = 4; // external media unmounted 86 public static final int OP_SUSPEND = 5; // package suspended 87 public static final int OP_UNSUSPEND = 6; // package unsuspended 88 public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable 89 90 private final int mOp; 91 92 @NonNull 93 private final UserHandle mUser; 94 95 @NonNull 96 private final String[] mPackages; 97 PackageUpdatedTask(final int op, @NonNull final UserHandle user, @NonNull final String... packages)98 public PackageUpdatedTask(final int op, @NonNull final UserHandle user, 99 @NonNull final String... packages) { 100 mOp = op; 101 mUser = user; 102 mPackages = packages; 103 } 104 105 @Override execute(@onNull ModelTaskController taskController, @NonNull BgDataModel dataModel, @NonNull AllAppsList appsList)106 public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel, 107 @NonNull AllAppsList appsList) { 108 final Context context = taskController.getContext(); 109 final IconCache iconCache = taskController.getIconCache(); 110 111 final String[] packages = mPackages; 112 final int packageCount = packages.length; 113 final FlagOp flagOp; 114 final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages)); 115 final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE 116 ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user 117 : ItemInfoMatcher.ofPackages(packageSet, mUser); 118 final HashSet<ComponentName> removedComponents = new HashSet<>(); 119 final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>(); 120 if (DEBUG) { 121 Log.d(TAG, "Package updated: mOp=" + getOpString() 122 + " packages=" + Arrays.toString(packages) 123 + ", user=" + mUser); 124 } 125 switch (mOp) { 126 case OP_ADD: { 127 for (int i = 0; i < packageCount; i++) { 128 iconCache.updateIconsForPkg(packages[i], mUser); 129 if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) { 130 if (DEBUG) { 131 Log.d(TAG, "OP_ADD: PROMISE_APPS_IN_ALL_APPS enabled:" 132 + " removing promise icon apps from package=" + packages[i]); 133 } 134 appsList.removePackage(packages[i], mUser); 135 } 136 activitiesLists.put(packages[i], 137 appsList.addPackage(context, packages[i], mUser)); 138 } 139 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 140 break; 141 } 142 case OP_UPDATE: 143 try (SafeCloseable t = appsList.trackRemoves(a -> { 144 Log.d(TAG, "OP_UPDATE - AllAppsList.trackRemoves callback:" 145 + " removed component=" + a.componentName 146 + " id=" + a.id 147 + " Look for earlier AllAppsList logs to find more information."); 148 removedComponents.add(a.componentName); 149 })) { 150 for (int i = 0; i < packageCount; i++) { 151 iconCache.updateIconsForPkg(packages[i], mUser); 152 activitiesLists.put(packages[i], 153 appsList.updatePackage(context, packages[i], mUser)); 154 } 155 } 156 // Since package was just updated, the target must be available now. 157 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 158 break; 159 case OP_REMOVE: { 160 for (int i = 0; i < packageCount; i++) { 161 iconCache.removeIconsForPkg(packages[i], mUser); 162 } 163 // Fall through 164 } 165 case OP_UNAVAILABLE: 166 for (int i = 0; i < packageCount; i++) { 167 if (DEBUG) { 168 Log.d(TAG, getOpString() + ": removing package=" + packages[i]); 169 } 170 appsList.removePackage(packages[i], mUser); 171 } 172 flagOp = FlagOp.NO_OP.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 173 break; 174 case OP_SUSPEND: 175 case OP_UNSUSPEND: 176 flagOp = FlagOp.NO_OP.setFlag( 177 WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED, mOp == OP_SUSPEND); 178 appsList.updateDisabledFlags(matcher, flagOp); 179 break; 180 case OP_USER_AVAILABILITY_CHANGE: { 181 UserManagerState ums = new UserManagerState(); 182 UserManager userManager = context.getSystemService(UserManager.class); 183 ums.init(UserCache.INSTANCE.get(context), userManager); 184 boolean isUserQuiet = ums.isUserQuiet(mUser); 185 flagOp = FlagOp.NO_OP.setFlag( 186 WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER, isUserQuiet); 187 appsList.updateDisabledFlags(matcher, flagOp); 188 189 if (Flags.enablePrivateSpace()) { 190 UserCache userCache = UserCache.INSTANCE.get(context); 191 if (userCache.getUserInfo(mUser).isWork()) { 192 appsList.setFlags(FLAG_WORK_PROFILE_QUIET_MODE_ENABLED, isUserQuiet); 193 } else if (userCache.getUserInfo(mUser).isPrivate()) { 194 appsList.setFlags(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED, isUserQuiet); 195 } 196 } else { 197 // We are not synchronizing here, as int operations are atomic 198 appsList.setFlags(FLAG_QUIET_MODE_ENABLED, ums.isAnyProfileQuietModeEnabled()); 199 } 200 break; 201 } 202 default: 203 flagOp = FlagOp.NO_OP; 204 break; 205 } 206 207 taskController.bindApplicationsIfNeeded(); 208 209 final IntSet removedShortcuts = new IntSet(); 210 // Shortcuts to keep even if the corresponding app was removed 211 final IntSet forceKeepShortcuts = new IntSet(); 212 213 // Update shortcut infos 214 if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { 215 final ArrayList<ItemInfo> updatedWorkspaceItems = new ArrayList<>(); 216 217 // For system apps, package manager send OP_UPDATE when an app is enabled. 218 final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; 219 synchronized (dataModel) { 220 dataModel.forAllWorkspaceItemInfos(mUser, itemInfo -> { 221 222 boolean infoUpdated = false; 223 boolean shortcutUpdated = false; 224 225 ComponentName cn = itemInfo.getTargetComponent(); 226 if (cn != null && matcher.test(itemInfo)) { 227 String packageName = cn.getPackageName(); 228 229 if (itemInfo.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { 230 forceKeepShortcuts.add(itemInfo.id); 231 if (mOp == OP_REMOVE) { 232 return; 233 } 234 } 235 236 if (itemInfo.isPromise() && isNewApkAvailable) { 237 boolean isTargetValid = !cn.getClassName().equals( 238 IconCache.EMPTY_CLASS_NAME); 239 if (itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) { 240 int requestQuery = ShortcutRequest.PINNED; 241 if (Flags.restoreArchivedShortcuts()) { 242 // Avoid race condition where shortcut service has no record of 243 // unarchived shortcut being pinned after restore. 244 // Launcher should be source-of-truth for if shortcut is pinned. 245 requestQuery = ShortcutRequest.ALL; 246 } 247 List<ShortcutInfo> shortcut = 248 new ShortcutRequest(context, mUser) 249 .forPackage(cn.getPackageName(), 250 itemInfo.getDeepShortcutId()) 251 .query(requestQuery); 252 if (shortcut.isEmpty()) { 253 isTargetValid = false; 254 if (DEBUG) { 255 Log.d(TAG, "Shortcut not found for updated" 256 + " package=" + itemInfo.getTargetPackage() 257 + ", isArchived=" + itemInfo.isArchived()); 258 } 259 } else { 260 if (DEBUG) { 261 Log.d(TAG, "Found shortcut for updated" 262 + " package=" + itemInfo.getTargetPackage() 263 + ", isTargetValid=" + isTargetValid 264 + ", isArchived=" + itemInfo.isArchived()); 265 } 266 itemInfo.updateFromDeepShortcutInfo(shortcut.get(0), context); 267 infoUpdated = true; 268 } 269 } else if (isTargetValid) { 270 isTargetValid = context.getSystemService(LauncherApps.class) 271 .isActivityEnabled(cn, mUser); 272 } 273 274 if (!isTargetValid && (itemInfo.hasStatusFlag( 275 FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON) 276 || itemInfo.isArchived())) { 277 if (updateWorkspaceItemIntent(context, itemInfo, packageName)) { 278 infoUpdated = true; 279 } else if (shouldRemoveRestoredShortcut(itemInfo)) { 280 removedShortcuts.add(itemInfo.id); 281 if (DEBUG) { 282 FileLog.w(TAG, "Removing restored shortcut promise icon" 283 + " that no longer points to valid component." 284 + " id=" + itemInfo.id 285 + ", package=" + itemInfo.getTargetPackage() 286 + ", status=" + itemInfo.status 287 + ", isArchived=" + itemInfo.isArchived()); 288 } 289 return; 290 } 291 } else if (!isTargetValid) { 292 removedShortcuts.add(itemInfo.id); 293 if (DEBUG) { 294 FileLog.w(TAG, "Removing shortcut that no longer points to" 295 + " valid component." 296 + " id=" + itemInfo.id 297 + " package=" + itemInfo.getTargetPackage() 298 + " status=" + itemInfo.status); 299 } 300 return; 301 } else { 302 itemInfo.status = WorkspaceItemInfo.DEFAULT; 303 infoUpdated = true; 304 } 305 } else if (isNewApkAvailable && removedComponents.contains(cn)) { 306 if (updateWorkspaceItemIntent(context, itemInfo, packageName)) { 307 infoUpdated = true; 308 } 309 } 310 311 if (isNewApkAvailable) { 312 List<LauncherActivityInfo> activities = activitiesLists.get( 313 packageName); 314 // TODO: See if we can migrate this to 315 // AppInfo#updateRuntimeFlagsForActivityTarget 316 itemInfo.setProgressLevel( 317 activities == null || activities.isEmpty() 318 ? 100 319 : PackageManagerHelper.getLoadingProgress( 320 activities.get(0)), 321 PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); 322 // In case an app is archived, we need to make sure that archived state 323 // in WorkspaceItemInfo is refreshed. 324 if (Flags.enableSupportForArchiving() && !activities.isEmpty()) { 325 boolean newArchivalState = activities.get(0) 326 .getActivityInfo().isArchived; 327 if (newArchivalState != itemInfo.isArchived()) { 328 itemInfo.runtimeStatusFlags ^= FLAG_ARCHIVED; 329 infoUpdated = true; 330 } 331 } 332 if (itemInfo.itemType == Favorites.ITEM_TYPE_APPLICATION) { 333 if (activities != null && !activities.isEmpty()) { 334 itemInfo.setNonResizeable(ApiWrapper.INSTANCE.get(context) 335 .isNonResizeableActivity(activities.get(0))); 336 } 337 iconCache.getTitleAndIcon( 338 itemInfo, itemInfo.getMatchingLookupFlag()); 339 infoUpdated = true; 340 } 341 } 342 343 int oldRuntimeFlags = itemInfo.runtimeStatusFlags; 344 itemInfo.runtimeStatusFlags = flagOp.apply(itemInfo.runtimeStatusFlags); 345 if (itemInfo.runtimeStatusFlags != oldRuntimeFlags) { 346 shortcutUpdated = true; 347 } 348 } 349 350 if (infoUpdated || shortcutUpdated) { 351 updatedWorkspaceItems.add(itemInfo); 352 } 353 if (infoUpdated && itemInfo.id != ItemInfo.NO_ID) { 354 taskController.getModelWriter().updateItemInDatabase(itemInfo); 355 } 356 }); 357 358 dataModel.itemsIdMap.stream() 359 .filter(WIDGET_FILTER) 360 .filter(item -> mUser.equals(item.user)) 361 .map(item -> (LauncherAppWidgetInfo) item) 362 .filter(widget -> widget.hasRestoreFlag(FLAG_PROVIDER_NOT_READY) 363 && packageSet.contains(widget.providerName.getPackageName())) 364 .forEach(widgetInfo -> { 365 widgetInfo.restoreStatus &= 366 ~FLAG_PROVIDER_NOT_READY 367 & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; 368 369 // adding this flag ensures that launcher shows 'click to setup' 370 // if the widget has a config activity. In case there is no config 371 // activity, it will be marked as 'restored' during bind. 372 widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; 373 widgetInfo.installProgress = 100; 374 updatedWorkspaceItems.add(widgetInfo); 375 taskController.getModelWriter().updateItemInDatabase(widgetInfo); 376 }); 377 } 378 379 taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItems); 380 if (!removedShortcuts.isEmpty()) { 381 taskController.deleteAndBindComponentsRemoved( 382 ItemInfoMatcher.ofItemIds(removedShortcuts), 383 "removing shortcuts with invalid target components." 384 + " ids=" + removedShortcuts); 385 } 386 } 387 388 final HashSet<String> removedPackages = new HashSet<>(); 389 if (mOp == OP_REMOVE) { 390 // Mark all packages in the broadcast to be removed 391 Collections.addAll(removedPackages, packages); 392 if (DEBUG) { 393 Log.d(TAG, "OP_REMOVE: removing packages=" + Arrays.toString(packages)); 394 } 395 396 // No need to update the removedComponents as 397 // removedPackages is a super-set of removedComponents 398 } else if (mOp == OP_UPDATE) { 399 // Mark disabled packages in the broadcast to be removed 400 final LauncherApps launcherApps = context.getSystemService(LauncherApps.class); 401 for (int i = 0; i < packageCount; i++) { 402 if (!launcherApps.isPackageEnabled(packages[i], mUser)) { 403 if (DEBUG) { 404 Log.d(TAG, "OP_UPDATE:" 405 + " package " + packages[i] + " is disabled, removing package."); 406 } 407 removedPackages.add(packages[i]); 408 } 409 } 410 } 411 412 if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { 413 Predicate<ItemInfo> removeMatch = 414 ItemInfoMatcher.ofPackages(removedPackages, mUser) 415 .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) 416 .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); 417 taskController.deleteAndBindComponentsRemoved(removeMatch, 418 "removed because the corresponding package or component is removed. " 419 + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect( 420 Collectors.joining(",", "[", "]")) 421 + " removedComponents=" + removedComponents.stream() 422 .filter(Objects::nonNull).map(ComponentName::toShortString) 423 .collect(Collectors.joining(",", "[", "]"))); 424 425 // Remove any queued items from the install queue 426 ItemInstallQueue.INSTANCE.get(context) 427 .removeFromInstallQueue(removedPackages, mUser); 428 } 429 430 if (mOp == OP_ADD) { 431 // Load widgets for the new package. Changes due to app updates are handled through 432 // AppWidgetHost events, this is just to initialize the long-press options. 433 for (int i = 0; i < packageCount; i++) { 434 dataModel.widgetsModel.update(new PackageUserKey(packages[i], mUser)); 435 } 436 taskController.bindUpdatedWidgets(dataModel); 437 } 438 } 439 440 /** 441 * Updates {@param si}'s intent to point to a new ComponentName. 442 * @return Whether the shortcut intent was changed. 443 */ updateWorkspaceItemIntent(Context context, WorkspaceItemInfo si, String packageName)444 private boolean updateWorkspaceItemIntent(Context context, 445 WorkspaceItemInfo si, String packageName) { 446 if (si.itemType == ITEM_TYPE_DEEP_SHORTCUT) { 447 // Do not update intent for deep shortcuts as they contain additional information 448 // about the shortcut. 449 return false; 450 } 451 // Try to find the best match activity. 452 Intent intent = PackageManagerHelper.INSTANCE.get(context) 453 .getAppLaunchIntent(packageName, mUser); 454 if (intent != null) { 455 si.intent = intent; 456 si.status = WorkspaceItemInfo.DEFAULT; 457 return true; 458 } 459 return false; 460 } 461 shouldRemoveRestoredShortcut(WorkspaceItemInfo itemInfo)462 private boolean shouldRemoveRestoredShortcut(WorkspaceItemInfo itemInfo) { 463 if (itemInfo.hasPromiseIconUi() && !Flags.restoreArchivedShortcuts()) { 464 return true; 465 } 466 return Flags.restoreArchivedShortcuts() 467 && !itemInfo.isArchived() 468 && itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT; 469 } 470 getOpString()471 private String getOpString() { 472 return switch (mOp) { 473 case OP_NONE -> "NONE"; 474 case OP_ADD -> "ADD"; 475 case OP_UPDATE -> "UPDATE"; 476 case OP_REMOVE -> "REMOVE"; 477 case OP_UNAVAILABLE -> "UNAVAILABLE"; 478 case OP_SUSPEND -> "SUSPEND"; 479 case OP_UNSUSPEND -> "UNSUSPEND"; 480 case OP_USER_AVAILABILITY_CHANGE -> "USER_AVAILABILITY_CHANGE"; 481 default -> "UNKNOWN"; 482 }; 483 } 484 } 485