1 
2 package com.android.launcher3.model;
3 
4 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
5 
6 import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
7 import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
8 import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
9 
10 import static java.util.stream.Collectors.groupingBy;
11 import static java.util.stream.Collectors.mapping;
12 import static java.util.stream.Collectors.toList;
13 
14 import android.appwidget.AppWidgetProviderInfo;
15 import android.content.ComponentName;
16 import android.content.Context;
17 import android.content.pm.PackageManager;
18 import android.os.UserHandle;
19 import android.util.Log;
20 import android.util.Pair;
21 
22 import androidx.annotation.Nullable;
23 import androidx.collection.ArrayMap;
24 
25 import com.android.launcher3.AppFilter;
26 import com.android.launcher3.InvariantDeviceProfile;
27 import com.android.launcher3.LauncherAppState;
28 import com.android.launcher3.Utilities;
29 import com.android.launcher3.compat.AlphabeticIndexCompat;
30 import com.android.launcher3.config.FeatureFlags;
31 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
32 import com.android.launcher3.icons.IconCache;
33 import com.android.launcher3.model.data.PackageItemInfo;
34 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
35 import com.android.launcher3.util.ComponentKey;
36 import com.android.launcher3.util.IntSet;
37 import com.android.launcher3.util.PackageUserKey;
38 import com.android.launcher3.util.Preconditions;
39 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
40 import com.android.launcher3.widget.WidgetManagerHelper;
41 import com.android.launcher3.widget.WidgetSections;
42 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
43 import com.android.launcher3.widget.model.WidgetsListContentEntry;
44 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
45 import com.android.wm.shell.Flags;
46 
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Map.Entry;
56 import java.util.Set;
57 import java.util.function.Predicate;
58 
59 /**
60  * Widgets data model that is used by the adapters of the widget views and controllers.
61  *
62  * <p> The widgets and shortcuts are organized using package name as its index.
63  */
64 public class WidgetsModel {
65 
66     private static final String TAG = "WidgetsModel";
67     private static final boolean DEBUG = false;
68 
69     /* Map of widgets and shortcuts that are tracked per package. */
70     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
71 
72     /**
73      * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
74      * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
75      * list of {@link WidgetsListBaseEntry}s is not sorted.
76      *
77      * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
78      */
getFilteredWidgetsListForPicker( Context context, Predicate<WidgetItem> widgetItemFilter)79     public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
80             Context context,
81             Predicate<WidgetItem> widgetItemFilter) {
82         if (!WIDGETS_ENABLED) {
83             return new ArrayList<>();
84         }
85         ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
86         AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
87 
88         for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
89             PackageItemInfo pkgItem = entry.getKey();
90             List<WidgetItem> widgetItems = entry.getValue()
91                     .stream()
92                     .filter(widgetItemFilter).toList();
93             if (!widgetItems.isEmpty()) {
94                 String sectionName = (pkgItem.title == null) ? "" :
95                         indexer.computeSectionName(pkgItem.title);
96                 result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
97                 result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
98             }
99         }
100         return result;
101     }
102 
103     /**
104      * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
105      * are sorted (based on label and user), but the overall list of
106      * {@link WidgetsListBaseEntry}s is not sorted.
107      *
108      * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
109      */
getWidgetsListForPicker(Context context)110     public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
111         // return all items
112         return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
113     }
114 
115     /** Returns a mapping of packages to their widgets without static shortcuts. */
getAllWidgetsWithoutShortcuts()116     public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
117         if (!WIDGETS_ENABLED) {
118             return Collections.emptyMap();
119         }
120         Map<PackageUserKey, List<WidgetItem>> packagesToWidgets = new HashMap<>();
121         mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> {
122             List<WidgetItem> widgets = widgetsAndShortcuts.stream()
123                     .filter(item -> item.widgetInfo != null)
124                     .collect(toList());
125             if (widgets.size() > 0) {
126                 packagesToWidgets.put(
127                         new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user),
128                         widgets);
129             }
130         });
131         return packagesToWidgets;
132     }
133 
134     /**
135      * Returns a map of widget component keys to corresponding widget items. Excludes the
136      * shortcuts.
137      */
getAllWidgetComponentsWithoutShortcuts()138     public synchronized Map<ComponentKey, WidgetItem> getAllWidgetComponentsWithoutShortcuts() {
139         if (!WIDGETS_ENABLED) {
140             return Collections.emptyMap();
141         }
142         Map<ComponentKey, WidgetItem> widgetsMap = new HashMap<>();
143         mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) ->
144                 widgetsAndShortcuts.stream().filter(item -> item.widgetInfo != null).forEach(
145                         item -> widgetsMap.put(new ComponentKey(item.componentName, item.user),
146                                 item)));
147         return widgetsMap;
148     }
149 
150     /**
151      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
152      *                    only widgets and shortcuts associated with the package/user are.
153      */
update( LauncherAppState app, @Nullable PackageUserKey packageUser)154     public List<ComponentWithLabelAndIcon> update(
155             LauncherAppState app, @Nullable PackageUserKey packageUser) {
156         if (!WIDGETS_ENABLED) {
157             return Collections.emptyList();
158         }
159         Preconditions.assertWorkerThread();
160 
161         Context context = app.getContext();
162         final ArrayList<WidgetItem> widgetsAndShortcuts = new ArrayList<>();
163         List<ComponentWithLabelAndIcon> updatedItems = new ArrayList<>();
164         try {
165             InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
166             PackageManager pm = app.getContext().getPackageManager();
167 
168             // Widgets
169             WidgetManagerHelper widgetManager = new WidgetManagerHelper(context);
170             for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(packageUser)) {
171                 LauncherAppWidgetProviderInfo launcherWidgetInfo =
172                         LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
173 
174                 widgetsAndShortcuts.add(new WidgetItem(
175                         launcherWidgetInfo, idp, app.getIconCache(), app.getContext(),
176                         widgetManager));
177                 updatedItems.add(launcherWidgetInfo);
178             }
179 
180             // Shortcuts
181             for (ShortcutConfigActivityInfo info :
182                     queryList(context, packageUser)) {
183                 widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
184                 updatedItems.add(info);
185             }
186             setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
187         } catch (Exception e) {
188             if (!FeatureFlags.IS_STUDIO_BUILD && Utilities.isBinderSizeError(e)) {
189                 // the returned value may be incomplete and will not be refreshed until the next
190                 // time Launcher starts.
191                 // TODO: after figuring out a repro step, introduce a dirty bit to check when
192                 // onResume is called to refresh the widget provider list.
193             } else {
194                 throw e;
195             }
196         }
197 
198         return updatedItems;
199     }
200 
setWidgetsAndShortcuts(ArrayList<WidgetItem> rawWidgetsShortcuts, LauncherAppState app, @Nullable PackageUserKey packageUser)201     private synchronized void setWidgetsAndShortcuts(ArrayList<WidgetItem> rawWidgetsShortcuts,
202             LauncherAppState app, @Nullable PackageUserKey packageUser) {
203         if (DEBUG) {
204             Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
205         }
206 
207         // Temporary cache for {@link PackageItemInfos} to avoid having to go through
208         // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
209         PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
210 
211         if (packageUser == null) {
212             // Clear the list if this is an update on all widgets and shortcuts.
213             mWidgetsList.clear();
214         } else {
215             // Otherwise, only clear the widgets and shortcuts for the changed package.
216             mWidgetsList.remove(packageItemInfoCache.getOrCreate(packageUser));
217         }
218 
219         // add and update.
220         mWidgetsList.putAll(rawWidgetsShortcuts.stream()
221                 .filter(new WidgetValidityCheck(app))
222                 .filter(new WidgetFlagCheck())
223                 .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
224                         .map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
225                 .collect(groupingBy(pair -> pair.first, mapping(pair -> pair.second, toList()))));
226 
227         // Update each package entry
228         IconCache iconCache = app.getIconCache();
229         for (PackageItemInfo p : packageItemInfoCache.values()) {
230             iconCache.getTitleAndIconForApp(p, true /* userLowResIcon */);
231         }
232     }
233 
onPackageIconsUpdated(Set<String> packageNames, UserHandle user, LauncherAppState app)234     public void onPackageIconsUpdated(Set<String> packageNames, UserHandle user,
235             LauncherAppState app) {
236         if (!WIDGETS_ENABLED) {
237             return;
238         }
239         WidgetManagerHelper widgetManager = new WidgetManagerHelper(app.getContext());
240         for (Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
241             if (packageNames.contains(entry.getKey().packageName)) {
242                 List<WidgetItem> items = entry.getValue();
243                 int count = items.size();
244                 for (int i = 0; i < count; i++) {
245                     WidgetItem item = items.get(i);
246                     if (item.user.equals(user)) {
247                         if (item.activityInfo != null) {
248                             items.set(i, new WidgetItem(item.activityInfo, app.getIconCache(),
249                                     app.getContext().getPackageManager()));
250                         } else {
251                             items.set(i, new WidgetItem(item.widgetInfo,
252                                     app.getInvariantDeviceProfile(), app.getIconCache(),
253                                     app.getContext(), widgetManager));
254                         }
255                     }
256                 }
257             }
258         }
259     }
260 
createPackageItemInfo( ComponentName providerName, UserHandle user, int category )261     private PackageItemInfo createPackageItemInfo(
262             ComponentName providerName,
263             UserHandle user,
264             int category
265     ) {
266         if (category == NO_CATEGORY) {
267             return new PackageItemInfo(providerName.getPackageName(), user);
268         } else {
269             return new PackageItemInfo("" , category, user);
270         }
271     }
272 
getCategories(ComponentName providerName, Context context)273     private IntSet getCategories(ComponentName providerName, Context context) {
274         IntSet categories = WidgetSections.getWidgetsToCategory(context).get(providerName);
275         if (categories != null) {
276             return categories;
277         }
278         categories = new IntSet();
279         categories.add(NO_CATEGORY);
280         return categories;
281     }
282 
getWidgetProviderInfoByProviderName( ComponentName providerName, UserHandle user, Context context)283     public WidgetItem getWidgetProviderInfoByProviderName(
284             ComponentName providerName, UserHandle user, Context context) {
285         if (!WIDGETS_ENABLED) {
286             return null;
287         }
288         IntSet categories = getCategories(providerName, context);
289 
290         // Checking if we have a provider in any of the categories.
291         for (Integer category: categories) {
292             PackageItemInfo key = createPackageItemInfo(providerName, user, category);
293             List<WidgetItem> widgets = mWidgetsList.get(key);
294             if (widgets != null) {
295                 return widgets.stream().filter(
296                                 item -> item.componentName.equals(providerName)
297                         )
298                         .findFirst()
299                         .orElse(null);
300             }
301         }
302         return null;
303     }
304 
305     /** Returns {@link PackageItemInfo} of a pending widget. */
newPendingItemInfo(Context context, ComponentName provider, UserHandle user)306     public static PackageItemInfo newPendingItemInfo(Context context, ComponentName provider,
307             UserHandle user) {
308         Map<ComponentName, IntSet> widgetsToCategories =
309                 WidgetSections.getWidgetsToCategory(context);
310         if (widgetsToCategories.containsKey(provider)) {
311             Iterator<Integer> categoriesIterator = widgetsToCategories.get(provider).iterator();
312             int firstCategory = NO_CATEGORY;
313             while (categoriesIterator.hasNext() && firstCategory == NO_CATEGORY) {
314                 firstCategory = categoriesIterator.next();
315             }
316             return new PackageItemInfo(provider.getPackageName(), firstCategory, user);
317         }
318         return new PackageItemInfo(provider.getPackageName(), user);
319     }
320 
getPackageUserKeys(Context context, WidgetItem item)321     private List<PackageUserKey> getPackageUserKeys(Context context, WidgetItem item) {
322         Map<ComponentName, IntSet> widgetsToCategories =
323                 WidgetSections.getWidgetsToCategory(context);
324         IntSet categories = widgetsToCategories.get(item.componentName);
325         if (categories == null || categories.isEmpty()) {
326             return Arrays.asList(
327                     new PackageUserKey(item.componentName.getPackageName(), item.user));
328         }
329         List<PackageUserKey> packageUserKeys = new ArrayList<>();
330         categories.forEach(category -> {
331             if (category == NO_CATEGORY) {
332                 packageUserKeys.add(
333                         new PackageUserKey(item.componentName.getPackageName(),
334                                 item.user));
335             } else {
336                 packageUserKeys.add(new PackageUserKey(category, item.user));
337             }
338         });
339         return packageUserKeys;
340     }
341 
342     private static class WidgetValidityCheck implements Predicate<WidgetItem> {
343 
344         private final InvariantDeviceProfile mIdp;
345         private final AppFilter mAppFilter;
346 
WidgetValidityCheck(LauncherAppState app)347         WidgetValidityCheck(LauncherAppState app) {
348             mIdp = app.getInvariantDeviceProfile();
349             mAppFilter = new AppFilter(app.getContext());
350         }
351 
352         @Override
test(WidgetItem item)353         public boolean test(WidgetItem item) {
354             if (item.widgetInfo != null) {
355                 if ((item.widgetInfo.getWidgetFeatures() & WIDGET_FEATURE_HIDE_FROM_PICKER) != 0) {
356                     // Widget is hidden from picker
357                     return false;
358                 }
359 
360                 // Ensure that all widgets we show can be added on a workspace of this size
361                 if (!item.widgetInfo.isMinSizeFulfilled()) {
362                     if (DEBUG) {
363                         Log.d(TAG, String.format(
364                                 "Widget %s : can't fit on this device with a grid size: %dx%d",
365                                 item.componentName, mIdp.numColumns, mIdp.numRows));
366                     }
367                     return false;
368                 }
369             }
370             if (!mAppFilter.shouldShowApp(item.componentName)) {
371                 if (DEBUG) {
372                     Log.d(TAG, String.format("%s is filtered and not added to the widget tray.",
373                             item.componentName));
374                 }
375                 return false;
376             }
377 
378             return true;
379         }
380     }
381 
382     private static class WidgetFlagCheck implements Predicate<WidgetItem> {
383 
384         private static final String BUBBLES_SHORTCUT_WIDGET =
385                 "com.android.systemui/com.android.wm.shell.bubbles.shortcut"
386                         + ".CreateBubbleShortcutActivity";
387 
388         @Override
test(WidgetItem widgetItem)389         public boolean test(WidgetItem widgetItem) {
390             if (BUBBLES_SHORTCUT_WIDGET.equals(widgetItem.componentName.flattenToString())) {
391                 return Flags.enableRetrievableBubbles();
392             }
393             return true;
394         }
395     }
396 
397     private static final class PackageItemInfoCache {
398         private final Map<PackageUserKey, PackageItemInfo> mMap = new ArrayMap<>();
399 
getOrCreate(PackageUserKey key)400         PackageItemInfo getOrCreate(PackageUserKey key) {
401             PackageItemInfo pInfo = mMap.get(key);
402             if (pInfo == null) {
403                 pInfo = new PackageItemInfo(key.mPackageName, key.mWidgetCategory, key.mUser);
404                 pInfo.user = key.mUser;
405                 mMap.put(key,  pInfo);
406             }
407             return pInfo;
408         }
409 
values()410         Collection<PackageItemInfo> values() {
411             return mMap.values();
412         }
413     }
414 }
415