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