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