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.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 19 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ShortcutInfo; 24 import android.os.Process; 25 import android.os.UserHandle; 26 import android.util.Log; 27 28 import com.android.launcher3.InstallShortcutReceiver; 29 import com.android.launcher3.ItemInfo; 30 import com.android.launcher3.LauncherAppState; 31 import com.android.launcher3.LauncherAppWidgetInfo; 32 import com.android.launcher3.LauncherSettings.Favorites; 33 import com.android.launcher3.SessionCommitReceiver; 34 import com.android.launcher3.Utilities; 35 import com.android.launcher3.WorkspaceItemInfo; 36 import com.android.launcher3.compat.LauncherAppsCompat; 37 import com.android.launcher3.compat.UserManagerCompat; 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.shortcuts.DeepShortcutManager; 44 import com.android.launcher3.testing.TestProtocol; 45 import com.android.launcher3.util.FlagOp; 46 import com.android.launcher3.util.IntSparseArrayMap; 47 import com.android.launcher3.util.ItemInfoMatcher; 48 import com.android.launcher3.util.PackageManagerHelper; 49 import com.android.launcher3.util.PackageUserKey; 50 import com.android.launcher3.util.SafeCloseable; 51 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.Collections; 55 import java.util.HashSet; 56 import java.util.List; 57 58 import static com.android.launcher3.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 59 import static com.android.launcher3.WorkspaceItemInfo.FLAG_RESTORED_ICON; 60 61 /** 62 * Handles updates due to changes in package manager (app installed/updated/removed) 63 * or when a user availability changes. 64 */ 65 public class PackageUpdatedTask extends BaseModelUpdateTask { 66 67 private static final boolean DEBUG = false; 68 private static final String TAG = "PackageUpdatedTask"; 69 70 public static final int OP_NONE = 0; 71 public static final int OP_ADD = 1; 72 public static final int OP_UPDATE = 2; 73 public static final int OP_REMOVE = 3; // uninstalled 74 public static final int OP_UNAVAILABLE = 4; // external media unmounted 75 public static final int OP_SUSPEND = 5; // package suspended 76 public static final int OP_UNSUSPEND = 6; // package unsuspended 77 public static final int OP_USER_AVAILABILITY_CHANGE = 7; // user available/unavailable 78 79 private final int mOp; 80 private final UserHandle mUser; 81 private final String[] mPackages; 82 PackageUpdatedTask(int op, UserHandle user, String... packages)83 public PackageUpdatedTask(int op, UserHandle user, String... packages) { 84 mOp = op; 85 mUser = user; 86 mPackages = packages; 87 } 88 89 @Override execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList)90 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) { 91 if (TestProtocol.sDebugTracing) { 92 Log.d(TestProtocol.APP_NOT_DISABLED, "PackageUpdatedTask: " + mOp + ", " + 93 Arrays.toString(mPackages)); 94 } 95 final Context context = app.getContext(); 96 final IconCache iconCache = app.getIconCache(); 97 98 final String[] packages = mPackages; 99 final int N = packages.length; 100 FlagOp flagOp = FlagOp.NO_OP; 101 final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages)); 102 ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, mUser); 103 final HashSet<ComponentName> removedComponents = new HashSet<>(); 104 105 switch (mOp) { 106 case OP_ADD: { 107 for (int i = 0; i < N; i++) { 108 if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); 109 iconCache.updateIconsForPkg(packages[i], mUser); 110 if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS) { 111 appsList.removePackage(packages[i], mUser); 112 } 113 appsList.addPackage(context, packages[i], mUser); 114 115 // Automatically add homescreen icon for work profile apps for below O device. 116 if (!Utilities.ATLEAST_OREO && !Process.myUserHandle().equals(mUser)) { 117 SessionCommitReceiver.queueAppIconAddition(context, packages[i], mUser); 118 } 119 } 120 flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 121 break; 122 } 123 case OP_UPDATE: 124 try (SafeCloseable t = 125 appsList.trackRemoves(a -> removedComponents.add(a.componentName))) { 126 for (int i = 0; i < N; i++) { 127 if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); 128 iconCache.updateIconsForPkg(packages[i], mUser); 129 appsList.updatePackage(context, packages[i], mUser); 130 app.getWidgetCache().removePackage(packages[i], mUser); 131 } 132 } 133 // Since package was just updated, the target must be available now. 134 flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 135 break; 136 case OP_REMOVE: { 137 for (int i = 0; i < N; i++) { 138 FileLog.d(TAG, "Removing app icon" + packages[i]); 139 iconCache.removeIconsForPkg(packages[i], mUser); 140 } 141 // Fall through 142 } 143 case OP_UNAVAILABLE: 144 for (int i = 0; i < N; i++) { 145 if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); 146 appsList.removePackage(packages[i], mUser); 147 app.getWidgetCache().removePackage(packages[i], mUser); 148 } 149 flagOp = FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); 150 break; 151 case OP_SUSPEND: 152 case OP_UNSUSPEND: 153 flagOp = mOp == OP_SUSPEND ? 154 FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED) : 155 FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_SUSPENDED); 156 if (DEBUG) Log.d(TAG, "mAllAppsList.(un)suspend " + N); 157 appsList.updateDisabledFlags(matcher, flagOp); 158 break; 159 case OP_USER_AVAILABILITY_CHANGE: 160 flagOp = UserManagerCompat.getInstance(context).isQuietModeEnabled(mUser) 161 ? FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER) 162 : FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER); 163 // We want to update all packages for this user. 164 matcher = ItemInfoMatcher.ofUser(mUser); 165 appsList.updateDisabledFlags(matcher, flagOp); 166 break; 167 } 168 169 bindApplicationsIfNeeded(); 170 171 final IntSparseArrayMap<Boolean> removedShortcuts = new IntSparseArrayMap<>(); 172 173 // Update shortcut infos 174 if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { 175 final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>(); 176 final ArrayList<LauncherAppWidgetInfo> widgets = new ArrayList<>(); 177 178 // For system apps, package manager send OP_UPDATE when an app is enabled. 179 final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE; 180 synchronized (dataModel) { 181 for (ItemInfo info : dataModel.itemsIdMap) { 182 if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) { 183 WorkspaceItemInfo si = (WorkspaceItemInfo) info; 184 boolean infoUpdated = false; 185 boolean shortcutUpdated = false; 186 187 // Update shortcuts which use iconResource. 188 if ((si.iconResource != null) 189 && packageSet.contains(si.iconResource.packageName)) { 190 LauncherIcons li = LauncherIcons.obtain(context); 191 BitmapInfo iconInfo = li.createIconBitmap(si.iconResource); 192 li.recycle(); 193 if (iconInfo != null) { 194 si.applyFrom(iconInfo); 195 infoUpdated = true; 196 } 197 } 198 199 ComponentName cn = si.getTargetComponent(); 200 if (cn != null && matcher.matches(si, cn)) { 201 String packageName = cn.getPackageName(); 202 203 if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) { 204 removedShortcuts.put(si.id, false); 205 if (mOp == OP_REMOVE) { 206 continue; 207 } 208 } 209 210 if (si.isPromise() && isNewApkAvailable) { 211 boolean isTargetValid = true; 212 if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 213 List<ShortcutInfo> shortcut = DeepShortcutManager 214 .getInstance(context).queryForPinnedShortcuts( 215 cn.getPackageName(), 216 Arrays.asList(si.getDeepShortcutId()), mUser); 217 if (shortcut.isEmpty()) { 218 isTargetValid = false; 219 } else { 220 si.updateFromDeepShortcutInfo(shortcut.get(0), context); 221 infoUpdated = true; 222 } 223 } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) { 224 isTargetValid = LauncherAppsCompat.getInstance(context) 225 .isActivityEnabledForProfile(cn, mUser); 226 } 227 if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) { 228 if (updateWorkspaceItemIntent(context, si, packageName)) { 229 infoUpdated = true; 230 } else if (si.hasPromiseIconUi()) { 231 removedShortcuts.put(si.id, true); 232 continue; 233 } 234 } else if (!isTargetValid) { 235 removedShortcuts.put(si.id, true); 236 FileLog.e(TAG, "Restored shortcut no longer valid " 237 + si.intent); 238 continue; 239 } else { 240 si.status = WorkspaceItemInfo.DEFAULT; 241 infoUpdated = true; 242 } 243 } else if (isNewApkAvailable && removedComponents.contains(cn)) { 244 if (updateWorkspaceItemIntent(context, si, packageName)) { 245 infoUpdated = true; 246 } 247 } 248 249 if (isNewApkAvailable && 250 si.itemType == Favorites.ITEM_TYPE_APPLICATION) { 251 iconCache.getTitleAndIcon(si, si.usingLowResIcon()); 252 infoUpdated = true; 253 } 254 255 int oldRuntimeFlags = si.runtimeStatusFlags; 256 si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags); 257 if (si.runtimeStatusFlags != oldRuntimeFlags) { 258 shortcutUpdated = true; 259 } 260 } 261 262 if (infoUpdated || shortcutUpdated) { 263 updatedWorkspaceItems.add(si); 264 } 265 if (infoUpdated) { 266 getModelWriter().updateItemInDatabase(si); 267 } 268 } else if (info instanceof LauncherAppWidgetInfo && isNewApkAvailable) { 269 LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info; 270 if (mUser.equals(widgetInfo.user) 271 && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) 272 && packageSet.contains(widgetInfo.providerName.getPackageName())) { 273 widgetInfo.restoreStatus &= 274 ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY & 275 ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; 276 277 // adding this flag ensures that launcher shows 'click to setup' 278 // if the widget has a config activity. In case there is no config 279 // activity, it will be marked as 'restored' during bind. 280 widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; 281 282 widgets.add(widgetInfo); 283 getModelWriter().updateItemInDatabase(widgetInfo); 284 } 285 } 286 } 287 } 288 289 bindUpdatedWorkspaceItems(updatedWorkspaceItems); 290 if (!removedShortcuts.isEmpty()) { 291 deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts, false)); 292 } 293 294 if (!widgets.isEmpty()) { 295 scheduleCallbackTask(c -> c.bindWidgetsRestored(widgets)); 296 } 297 } 298 299 final HashSet<String> removedPackages = new HashSet<>(); 300 if (mOp == OP_REMOVE) { 301 // Mark all packages in the broadcast to be removed 302 Collections.addAll(removedPackages, packages); 303 304 // No need to update the removedComponents as 305 // removedPackages is a super-set of removedComponents 306 } else if (mOp == OP_UPDATE) { 307 // Mark disabled packages in the broadcast to be removed 308 final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); 309 for (int i=0; i<N; i++) { 310 if (!launcherApps.isPackageEnabledForProfile(packages[i], mUser)) { 311 removedPackages.add(packages[i]); 312 } 313 } 314 } 315 316 if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { 317 ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) 318 .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) 319 .and(ItemInfoMatcher.ofItemIds(removedShortcuts, true)); 320 deleteAndBindComponentsRemoved(removeMatch); 321 322 // Remove any queued items from the install queue 323 InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser); 324 } 325 326 if (Utilities.ATLEAST_OREO && mOp == OP_ADD) { 327 // Load widgets for the new package. Changes due to app updates are handled through 328 // AppWidgetHost events, this is just to initialize the long-press options. 329 for (int i = 0; i < N; i++) { 330 dataModel.widgetsModel.update(app, new PackageUserKey(packages[i], mUser)); 331 } 332 bindUpdatedWidgets(dataModel); 333 } 334 } 335 336 /** 337 * Updates {@param si}'s intent to point to a new ComponentName. 338 * @return Whether the shortcut intent was changed. 339 */ updateWorkspaceItemIntent(Context context, WorkspaceItemInfo si, String packageName)340 private boolean updateWorkspaceItemIntent(Context context, 341 WorkspaceItemInfo si, String packageName) { 342 // Try to find the best match activity. 343 Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser); 344 if (intent != null) { 345 si.intent = intent; 346 si.status = WorkspaceItemInfo.DEFAULT; 347 return true; 348 } 349 return false; 350 } 351 } 352