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