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