• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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