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