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