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