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_QUIET_MODE_ENABLED; 19 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 20 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON; 21 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.LauncherActivityInfo; 26 import android.content.pm.LauncherApps; 27 import android.content.pm.ShortcutInfo; 28 import android.os.UserHandle; 29 import android.os.UserManager; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 34 import com.android.launcher3.Launcher; 35 import com.android.launcher3.LauncherAppState; 36 import com.android.launcher3.LauncherSettings; 37 import com.android.launcher3.LauncherSettings.Favorites; 38 import com.android.launcher3.config.FeatureFlags; 39 import com.android.launcher3.icons.BitmapInfo; 40 import com.android.launcher3.icons.IconCache; 41 import com.android.launcher3.icons.LauncherIcons; 42 import com.android.launcher3.logging.FileLog; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 45 import com.android.launcher3.model.data.WorkspaceItemInfo; 46 import com.android.launcher3.pm.PackageInstallInfo; 47 import com.android.launcher3.pm.UserCache; 48 import com.android.launcher3.shortcuts.ShortcutRequest; 49 import com.android.launcher3.util.FlagOp; 50 import com.android.launcher3.util.IntSet; 51 import com.android.launcher3.util.ItemInfoMatcher; 52 import com.android.launcher3.util.PackageManagerHelper; 53 import com.android.launcher3.util.PackageUserKey; 54 import com.android.launcher3.util.SafeCloseable; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collections; 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.Objects; 63 import java.util.function.Predicate; 64 import java.util.stream.Collectors; 65 66 /** 67 * Handles updates due to changes in package manager (app installed/updated/removed) 68 * or when a user availability changes. 69 */ 70 public class PackageUpdatedTask extends BaseModelUpdateTask { 71 72 private static final boolean DEBUG = false; 73 private static final String TAG = "PackageUpdatedTask"; 74 75 public static final int OP_NONE = 0; 76 public static final int OP_ADD = 1; 77 public static final int OP_UPDATE = 2; 78 public static final int OP_REMOVE = 3; // uninstalled 79 public static final int OP_UNAVAILABLE = 4; // external media unmounted 80 public static final int OP_SUSPEND = 5; // package suspended 81 public static final int OP_UNSUSPEND = 6; // package unsuspended 82 public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable 83 84 private final int mOp; 85 86 @NonNull 87 private final UserHandle mUser; 88 89 @NonNull 90 private final String[] mPackages; 91 PackageUpdatedTask(final int op, @NonNull final UserHandle user, @NonNull final String... packages)92 public PackageUpdatedTask(final int op, @NonNull final UserHandle user, 93 @NonNull final String... packages) { 94 mOp = op; 95 mUser = user; 96 mPackages = packages; 97 } 98 99 @Override execute(@onNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList appsList)100 public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, 101 @NonNull final AllAppsList appsList) { 102 final Context context = app.getContext(); 103 final IconCache iconCache = app.getIconCache(); 104 105 final String[] packages = mPackages; 106 final int N = packages.length; 107 final FlagOp flagOp; 108 final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages)); 109 final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE 110 ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user 111 : ItemInfoMatcher.ofPackages(packageSet, mUser); 112 final HashSet<ComponentName> removedComponents = new HashSet<>(); 113 final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>(); 114 115 switch (mOp) { 116 case OP_ADD: { 117 for (int i = 0; i < N; i++) { 118 if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); 119 iconCache.updateIconsForPkg(packages[i], mUser); 120 if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) { 121 appsList.removePackage(packages[i], mUser); 122 } 123 activitiesLists.put( 124 packages[i], appsList.addPackage(context, packages[i], mUser)); 125 } 126 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 127 break; 128 } 129 case OP_UPDATE: 130 try (SafeCloseable t = 131 appsList.trackRemoves(a -> removedComponents.add(a.componentName))) { 132 for (int i = 0; i < N; i++) { 133 if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); 134 iconCache.updateIconsForPkg(packages[i], mUser); 135 activitiesLists.put( 136 packages[i], appsList.updatePackage(context, packages[i], mUser)); 137 138 // The update may have changed which shortcuts/widgets are available. 139 // Refresh the widgets for the package if we have an activity running. 140 Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity(); 141 if (launcher != null) { 142 launcher.refreshAndBindWidgetsForPackageUser( 143 new PackageUserKey(packages[i], mUser)); 144 } 145 } 146 } 147 // Since package was just updated, the target must be available now. 148 flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 149 break; 150 case OP_REMOVE: { 151 for (int i = 0; i < N; i++) { 152 FileLog.d(TAG, "Removing app icon" + packages[i]); 153 iconCache.removeIconsForPkg(packages[i], mUser); 154 } 155 // Fall through 156 } 157 case OP_UNAVAILABLE: 158 for (int i = 0; i < N; i++) { 159 if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); 160 appsList.removePackage(packages[i], mUser); 161 } 162 flagOp = FlagOp.NO_OP.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 163 break; 164 case OP_SUSPEND: 165 case OP_UNSUSPEND: 166 flagOp = FlagOp.NO_OP.setFlag( 167 WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED, mOp == OP_SUSPEND); 168 if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N); 169 appsList.updateDisabledFlags(matcher, flagOp); 170 break; 171 case OP_USER_AVAILABILITY_CHANGE: { 172 UserManagerState ums = new UserManagerState(); 173 ums.init(UserCache.INSTANCE.get(context), 174 context.getSystemService(UserManager.class)); 175 flagOp = FlagOp.NO_OP.setFlag( 176 WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER, ums.isUserQuiet(mUser)); 177 appsList.updateDisabledFlags(matcher, flagOp); 178 179 // We are not synchronizing here, as int operations are atomic 180 appsList.setFlags(FLAG_QUIET_MODE_ENABLED, ums.isAnyProfileQuietModeEnabled()); 181 break; 182 } 183 default: 184 flagOp = FlagOp.NO_OP; 185 break; 186 } 187 188 bindApplicationsIfNeeded(); 189 190 final IntSet removedShortcuts = new IntSet(); 191 // Shortcuts to keep even if the corresponding app was removed 192 final IntSet forceKeepShortcuts = new IntSet(); 193 194 // Update shortcut infos 195 if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { 196 final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>(); 197 final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>(); 198 199 // For system apps, package manager send OP_UPDATE when an app is enabled. 200 final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; 201 synchronized (dataModel) { 202 dataModel.forAllWorkspaceItemInfos(mUser, si -> { 203 204 boolean infoUpdated = false; 205 boolean shortcutUpdated = false; 206 207 // Update shortcuts which use iconResource. 208 if ((si.iconResource != null) 209 && packageSet.contains(si.iconResource.packageName)) { 210 LauncherIcons li = LauncherIcons.obtain(context); 211 BitmapInfo iconInfo = li.createIconBitmap(si.iconResource); 212 li.recycle(); 213 if (iconInfo != null) { 214 si.bitmap = iconInfo; 215 infoUpdated = true; 216 } 217 } 218 219 ComponentName cn = si.getTargetComponent(); 220 if (cn != null && matcher.test(si)) { 221 String packageName = cn.getPackageName(); 222 223 if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { 224 forceKeepShortcuts.add(si.id); 225 if (mOp == OP_REMOVE) { 226 return; 227 } 228 } 229 230 if (si.isPromise() && isNewApkAvailable) { 231 boolean isTargetValid = !cn.getClassName().equals( 232 IconCache.EMPTY_CLASS_NAME); 233 if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 234 List<ShortcutInfo> shortcut = 235 new ShortcutRequest(context, mUser) 236 .forPackage(cn.getPackageName(), 237 si.getDeepShortcutId()) 238 .query(ShortcutRequest.PINNED); 239 if (shortcut.isEmpty()) { 240 isTargetValid = false; 241 } else { 242 si.updateFromDeepShortcutInfo(shortcut.get(0), context); 243 infoUpdated = true; 244 } 245 } else if (isTargetValid) { 246 isTargetValid = context.getSystemService(LauncherApps.class) 247 .isActivityEnabled(cn, mUser); 248 } 249 if (!isTargetValid && si.hasStatusFlag( 250 FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) { 251 if (updateWorkspaceItemIntent(context, si, packageName)) { 252 infoUpdated = true; 253 } else if (si.hasPromiseIconUi()) { 254 removedShortcuts.add(si.id); 255 return; 256 } 257 } else if (!isTargetValid) { 258 removedShortcuts.add(si.id); 259 FileLog.e(TAG, "Restored shortcut no longer valid " 260 + si.getIntent()); 261 return; 262 } else { 263 si.status = WorkspaceItemInfo.DEFAULT; 264 infoUpdated = true; 265 } 266 } else if (isNewApkAvailable && removedComponents.contains(cn)) { 267 if (updateWorkspaceItemIntent(context, si, packageName)) { 268 infoUpdated = true; 269 } 270 } 271 272 if (isNewApkAvailable) { 273 List<LauncherActivityInfo> activities = activitiesLists.get( 274 packageName); 275 si.setProgressLevel( 276 activities == null || activities.isEmpty() 277 ? 100 278 : PackageManagerHelper.getLoadingProgress( 279 activities.get(0)), 280 PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); 281 if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) { 282 iconCache.getTitleAndIcon(si, si.usingLowResIcon()); 283 infoUpdated = true; 284 } 285 } 286 287 int oldRuntimeFlags = si.runtimeStatusFlags; 288 si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags); 289 if (si.runtimeStatusFlags != oldRuntimeFlags) { 290 shortcutUpdated = true; 291 } 292 } 293 294 if (infoUpdated || shortcutUpdated) { 295 updatedWorkspaceItems.add(si); 296 } 297 if (infoUpdated && si.id != ItemInfo.NO_ID) { 298 getModelWriter().updateItemInDatabase(si); 299 } 300 }); 301 302 for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) { 303 if (mUser.equals(widgetInfo.user) 304 && widgetInfo.hasRestoreFlag( 305 LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) 306 && packageSet.contains(widgetInfo.providerName.getPackageName())) { 307 widgetInfo.restoreStatus &= 308 ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY 309 & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; 310 311 // adding this flag ensures that launcher shows 'click to setup' 312 // if the widget has a config activity. In case there is no config 313 // activity, it will be marked as 'restored' during bind. 314 widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; 315 316 widgets.add(widgetInfo); 317 getModelWriter().updateItemInDatabase(widgetInfo); 318 } 319 } 320 } 321 322 bindUpdatedWorkspaceItems(updatedWorkspaceItems); 323 if (!removedShortcuts.isEmpty()) { 324 deleteAndBindComponentsRemoved( 325 ItemInfoMatcher.ofItemIds(removedShortcuts), 326 "removed because the target component is invalid"); 327 } 328 329 if (!widgets.isEmpty()) { 330 scheduleCallbackTask(c -> c.bindWidgetsRestored(widgets)); 331 } 332 } 333 334 final HashSet<String> removedPackages = new HashSet<>(); 335 if (mOp == OP_REMOVE) { 336 // Mark all packages in the broadcast to be removed 337 Collections.addAll(removedPackages, packages); 338 339 // No need to update the removedComponents as 340 // removedPackages is a super-set of removedComponents 341 } else if (mOp == OP_UPDATE) { 342 // Mark disabled packages in the broadcast to be removed 343 final LauncherApps launcherApps = context.getSystemService(LauncherApps.class); 344 for (int i=0; i<N; i++) { 345 if (!launcherApps.isPackageEnabled(packages[i], mUser)) { 346 removedPackages.add(packages[i]); 347 } 348 } 349 } 350 351 if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { 352 Predicate<ItemInfo> removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) 353 .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) 354 .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); 355 deleteAndBindComponentsRemoved(removeMatch, 356 "removed because the corresponding package or component is removed. " 357 + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect( 358 Collectors.joining(",", "[", "]")) 359 + " removedComponents=" + removedComponents.stream() 360 .filter(Objects::nonNull).map(ComponentName::toShortString) 361 .collect(Collectors.joining(",", "[", "]"))); 362 363 // Remove any queued items from the install queue 364 ItemInstallQueue.INSTANCE.get(context) 365 .removeFromInstallQueue(removedPackages, mUser); 366 } 367 368 if (mOp == OP_ADD) { 369 // Load widgets for the new package. Changes due to app updates are handled through 370 // AppWidgetHost events, this is just to initialize the long-press options. 371 for (int i = 0; i < N; i++) { 372 dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser)); 373 } 374 bindUpdatedWidgets(dataModel); 375 } 376 } 377 378 /** 379 * Updates {@param si}'s intent to point to a new ComponentName. 380 * @return Whether the shortcut intent was changed. 381 */ updateWorkspaceItemIntent(Context context, WorkspaceItemInfo si, String packageName)382 private boolean updateWorkspaceItemIntent(Context context, 383 WorkspaceItemInfo si, String packageName) { 384 if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 385 // Do not update intent for deep shortcuts as they contain additional information 386 // about the shortcut. 387 return false; 388 } 389 // Try to find the best match activity. 390 Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser); 391 if (intent != null) { 392 si.intent = intent; 393 si.status = WorkspaceItemInfo.DEFAULT; 394 return true; 395 } 396 return false; 397 } 398 } 399