1 /*
2  * Copyright (C) 2008 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 
17 package com.android.launcher3.model;
18 
19 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
20 import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
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.os.LocaleList;
28 import android.os.UserHandle;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 
34 import com.android.launcher3.AppFilter;
35 import com.android.launcher3.compat.AlphabeticIndexCompat;
36 import com.android.launcher3.icons.IconCache;
37 import com.android.launcher3.model.BgDataModel.Callbacks;
38 import com.android.launcher3.model.data.AppInfo;
39 import com.android.launcher3.model.data.ItemInfo;
40 import com.android.launcher3.pm.PackageInstallInfo;
41 import com.android.launcher3.pm.UserCache;
42 import com.android.launcher3.util.ApiWrapper;
43 import com.android.launcher3.util.FlagOp;
44 import com.android.launcher3.util.PackageManagerHelper;
45 import com.android.launcher3.util.SafeCloseable;
46 
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.function.Consumer;
52 import java.util.function.Predicate;
53 
54 
55 /**
56  * Stores the list of all applications for the all apps view.
57  */
58 @SuppressWarnings("NewApi")
59 public class AllAppsList {
60 
61     private static final String TAG = "AllAppsList";
62     private static final Consumer<AppInfo> NO_OP_CONSUMER = a -> { };
63     private static final boolean DEBUG = true;
64 
65     public static final int DEFAULT_APPLICATIONS_NUMBER = 42;
66 
67     /** The list off all apps. */
68     public final ArrayList<AppInfo> data = new ArrayList<>(DEFAULT_APPLICATIONS_NUMBER);
69 
70     @NonNull
71     private IconCache mIconCache;
72 
73     @NonNull
74     private AppFilter mAppFilter;
75 
76     private boolean mDataChanged = false;
77     private Consumer<AppInfo> mRemoveListener = NO_OP_CONSUMER;
78 
79     private AlphabeticIndexCompat mIndex;
80 
81     /**
82      * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
83      * @see Callbacks#FLAG_QUIET_MODE_ENABLED
84      * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
85      * @see Callbacks#FLAG_WORK_PROFILE_QUIET_MODE_ENABLED
86      * @see Callbacks#FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED
87      */
88     private int mFlags;
89 
90     /**
91      * Boring constructor.
92      */
AllAppsList(IconCache iconCache, AppFilter appFilter)93     public AllAppsList(IconCache iconCache, AppFilter appFilter) {
94         mIconCache = iconCache;
95         mAppFilter = appFilter;
96         mIndex = new AlphabeticIndexCompat(LocaleList.getDefault());
97     }
98 
99     /**
100      * Returns true if there have been any changes since last call.
101      */
getAndResetChangeFlag()102     public boolean getAndResetChangeFlag() {
103         boolean result = mDataChanged;
104         mDataChanged = false;
105         return result;
106     }
107 
108     /**
109      * Helper to checking {@link Callbacks#FLAG_HAS_SHORTCUT_PERMISSION}
110      */
hasShortcutHostPermission()111     public boolean hasShortcutHostPermission() {
112         return (mFlags & Callbacks.FLAG_HAS_SHORTCUT_PERMISSION) != 0;
113     }
114 
115     /**
116      * Sets or clears the provided flag
117      */
setFlags(int flagMask, boolean enabled)118     public void setFlags(int flagMask, boolean enabled) {
119         if (enabled) {
120             mFlags |= flagMask;
121         } else {
122             mFlags &= ~flagMask;
123         }
124         mDataChanged = true;
125     }
126 
127     /**
128      * Returns the model flags
129      */
getFlags()130     public int getFlags() {
131         return mFlags;
132     }
133 
134 
135     /**
136      * Add the supplied ApplicationInfo objects to the list, and enqueue it into the
137      * list to broadcast when notify() is called.
138      *
139      * If the app is already in the list, doesn't add it.
140      */
add(AppInfo info, LauncherActivityInfo activityInfo)141     public void add(AppInfo info, LauncherActivityInfo activityInfo) {
142         add(info, activityInfo, true);
143     }
144 
add(AppInfo info, LauncherActivityInfo activityInfo, boolean loadIcon)145     public void add(AppInfo info, LauncherActivityInfo activityInfo, boolean loadIcon) {
146         if (!mAppFilter.shouldShowApp(info.componentName)) {
147             return;
148         }
149         if (findAppInfo(info.componentName, info.user) != null) {
150             return;
151         }
152         if (loadIcon) {
153             mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */);
154             info.sectionName = mIndex.computeSectionName(info.title);
155         } else {
156             info.title = "";
157         }
158 
159         data.add(info);
160         mDataChanged = true;
161     }
162 
163     @Nullable
addPromiseApp(Context context, PackageInstallInfo installInfo)164     public AppInfo addPromiseApp(Context context, PackageInstallInfo installInfo) {
165         return addPromiseApp(context, installInfo, true);
166     }
167 
168     @Nullable
addPromiseApp( Context context, PackageInstallInfo installInfo, boolean loadIcon)169     public AppInfo addPromiseApp(
170             Context context, PackageInstallInfo installInfo, boolean loadIcon) {
171         // only if not yet installed
172         if (PackageManagerHelper.INSTANCE.get(context)
173                 .isAppInstalled(installInfo.packageName, installInfo.user)) {
174             return null;
175         }
176         AppInfo promiseAppInfo = new AppInfo(installInfo);
177 
178         if (loadIcon) {
179             mIconCache.getTitleAndIcon(promiseAppInfo, promiseAppInfo.usingLowResIcon());
180             promiseAppInfo.sectionName = mIndex.computeSectionName(promiseAppInfo.title);
181         } else {
182             promiseAppInfo.title = "";
183         }
184 
185         data.add(promiseAppInfo);
186         mDataChanged = true;
187 
188         return promiseAppInfo;
189     }
190 
updateSectionName(AppInfo appInfo)191     public void updateSectionName(AppInfo appInfo) {
192         appInfo.sectionName = mIndex.computeSectionName(appInfo.title);
193 
194     }
195 
196     /** Updates the given PackageInstallInfo's associated AppInfo's installation info. */
updatePromiseInstallInfo(PackageInstallInfo installInfo)197     public List<AppInfo> updatePromiseInstallInfo(PackageInstallInfo installInfo) {
198         List<AppInfo> updatedAppInfos = new ArrayList<>();
199         UserHandle user = installInfo.user;
200         for (int i = data.size() - 1; i >= 0; i--) {
201             final AppInfo appInfo = data.get(i);
202             final ComponentName tgtComp = appInfo.getTargetComponent();
203             if (tgtComp != null && tgtComp.getPackageName().equals(installInfo.packageName)
204                     && appInfo.user.equals(user)) {
205                 if (installInfo.state == PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING
206                         || installInfo.state == PackageInstallInfo.STATUS_INSTALLING
207                         // In case unarchival fails, we would want to keep the icon and update
208                         // back the progress to 0 for the all apps view without removing the
209                         // icon, which is contrary to what happens during normal app installation
210                         // flow.
211                         || (installInfo.state == PackageInstallInfo.STATUS_FAILED
212                                 && appInfo.isArchived())) {
213                     if (appInfo.isAppStartable()
214                             && installInfo.state == PackageInstallInfo.STATUS_INSTALLING
215                             && !appInfo.isArchived()) {
216                         continue;
217                     }
218                     appInfo.setProgressLevel(installInfo);
219 
220                     updatedAppInfos.add(appInfo);
221                 } else if (installInfo.state == PackageInstallInfo.STATUS_FAILED
222                         && !appInfo.isAppStartable()) {
223                     if (DEBUG) {
224                         Log.w(TAG, "updatePromiseInstallInfo: removing app due to install"
225                                 + " failure and appInfo not startable."
226                                 + " package=" + appInfo.getTargetPackage());
227                     }
228                     removeApp(i);
229                 }
230             }
231         }
232         return updatedAppInfos;
233     }
234 
removeApp(int index)235     private void removeApp(int index) {
236         AppInfo removed = data.remove(index);
237         if (removed != null) {
238             mDataChanged = true;
239             mRemoveListener.accept(removed);
240         }
241     }
242 
clear()243     public void clear() {
244         data.clear();
245         mDataChanged = false;
246         // Reset the index as locales might have changed
247         mIndex = new AlphabeticIndexCompat(LocaleList.getDefault());
248     }
249 
250     /**
251      * Add the icons for the supplied apk called packageName.
252      */
addPackage( Context context, String packageName, UserHandle user)253     public List<LauncherActivityInfo> addPackage(
254             Context context, String packageName, UserHandle user) {
255         List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
256                 .getActivityList(packageName, user);
257 
258         for (LauncherActivityInfo info : activities) {
259             add(new AppInfo(context, info, user), info);
260         }
261 
262         return activities;
263     }
264 
265     /**
266      * Remove the apps for the given apk identified by packageName.
267      */
removePackage(String packageName, UserHandle user)268     public void removePackage(String packageName, UserHandle user) {
269         final List<AppInfo> data = this.data;
270         for (int i = data.size() - 1; i >= 0; i--) {
271             AppInfo info = data.get(i);
272             if (info.user.equals(user) && packageName.equals(info.componentName.getPackageName())) {
273                 removeApp(i);
274             }
275         }
276     }
277 
278     /**
279      * Updates the disabled flags of apps matching {@param matcher} based on {@param op}.
280      */
updateDisabledFlags(Predicate<ItemInfo> matcher, FlagOp op)281     public void updateDisabledFlags(Predicate<ItemInfo> matcher, FlagOp op) {
282         final List<AppInfo> data = this.data;
283         for (int i = data.size() - 1; i >= 0; i--) {
284             AppInfo info = data.get(i);
285             if (matcher.test(info)) {
286                 info.runtimeStatusFlags = op.apply(info.runtimeStatusFlags);
287                 mDataChanged = true;
288             }
289         }
290     }
291 
updateIconsAndLabels(HashSet<String> packages, UserHandle user)292     public void updateIconsAndLabels(HashSet<String> packages, UserHandle user) {
293         for (AppInfo info : data) {
294             if (info.user.equals(user) && packages.contains(info.componentName.getPackageName())) {
295                 mIconCache.updateTitleAndIcon(info);
296                 info.sectionName = mIndex.computeSectionName(info.title);
297                 mDataChanged = true;
298             }
299         }
300     }
301 
302     /**
303      * Add and remove icons for this package which has been updated.
304      */
updatePackage( Context context, String packageName, UserHandle user)305     public List<LauncherActivityInfo> updatePackage(
306             Context context, String packageName, UserHandle user) {
307         final ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(context);
308         final UserCache userCache = UserCache.getInstance(context);
309         final PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
310         final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
311                 .getActivityList(packageName, user);
312         if (matches.size() > 0) {
313             // Find disabled/removed activities and remove them from data and add them
314             // to the removed list.
315             for (int i = data.size() - 1; i >= 0; i--) {
316                 final AppInfo applicationInfo = data.get(i);
317                 if (user.equals(applicationInfo.user)
318                         && packageName.equals(applicationInfo.componentName.getPackageName())) {
319                     if (!findActivity(matches, applicationInfo.componentName)) {
320                         if (DEBUG) {
321                             Log.w(TAG, "Changing shortcut target due to app component name change."
322                                     + " package=" + packageName);
323                         }
324                         removeApp(i);
325                     }
326                 }
327             }
328 
329             // Find enabled activities and add them to the adapter
330             // Also updates existing activities with new labels/icons
331             for (final LauncherActivityInfo info : matches) {
332                 AppInfo applicationInfo = findAppInfo(info.getComponentName(), user);
333                 if (applicationInfo == null) {
334                     add(new AppInfo(context, info, user), info);
335                 } else {
336                     Intent launchIntent = AppInfo.makeLaunchIntent(info);
337 
338                     mIconCache.getTitleAndIcon(applicationInfo, info, false /* useLowResIcon */);
339                     applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
340                     applicationInfo.intent = launchIntent;
341                     AppInfo.updateRuntimeFlagsForActivityTarget(applicationInfo, info,
342                             userCache.getUserInfo(user), apiWrapper, pmHelper);
343                     mDataChanged = true;
344                 }
345             }
346         } else {
347             // Remove all data for this package.
348             if (DEBUG) {
349                 Log.w(TAG, "updatePromiseInstallInfo: no Activities matched updated package,"
350                         + " removing all apps from package=" + packageName);
351             }
352             for (int i = data.size() - 1; i >= 0; i--) {
353                 final AppInfo applicationInfo = data.get(i);
354                 if (user.equals(applicationInfo.user)
355                         && packageName.equals(applicationInfo.componentName.getPackageName())) {
356                     mIconCache.remove(applicationInfo.componentName, user);
357                     removeApp(i);
358                 }
359             }
360         }
361 
362         return matches;
363     }
364 
365     /**
366      * Returns whether <em>apps</em> contains <em>component</em>.
367      */
findActivity(List<LauncherActivityInfo> apps, ComponentName component)368     private static boolean findActivity(List<LauncherActivityInfo> apps,
369             ComponentName component) {
370         for (LauncherActivityInfo info : apps) {
371             if (info.getComponentName().equals(component)) {
372                 return true;
373             }
374         }
375         return false;
376     }
377 
378     /**
379      * Find an AppInfo object for the given componentName
380      *
381      * @return the corresponding AppInfo or null
382      */
findAppInfo(@onNull ComponentName componentName, @NonNull UserHandle user)383     public @Nullable AppInfo findAppInfo(@NonNull ComponentName componentName,
384                                           @NonNull UserHandle user) {
385         for (AppInfo info: data) {
386             if (componentName.equals(info.componentName) && user.equals(info.user)) {
387                 return info;
388             }
389         }
390         return null;
391     }
392 
copyData()393     public AppInfo[] copyData() {
394         AppInfo[] result = data.toArray(EMPTY_ARRAY);
395         Arrays.sort(result, COMPONENT_KEY_COMPARATOR);
396         return result;
397     }
398 
trackRemoves(Consumer<AppInfo> removeListener)399     public SafeCloseable trackRemoves(Consumer<AppInfo> removeListener) {
400         mRemoveListener = removeListener;
401 
402         return () -> mRemoveListener = NO_OP_CONSUMER;
403     }
404 }
405