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