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