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