1 /*
2  * Copyright (C) 2017 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.popup;
18 
19 import android.content.ComponentName;
20 import android.content.pm.ShortcutInfo;
21 import android.os.Handler;
22 import android.os.UserHandle;
23 import android.service.notification.StatusBarNotification;
24 
25 import com.android.launcher3.ItemInfo;
26 import com.android.launcher3.Launcher;
27 import com.android.launcher3.WorkspaceItemInfo;
28 import com.android.launcher3.icons.LauncherIcons;
29 import com.android.launcher3.notification.NotificationInfo;
30 import com.android.launcher3.notification.NotificationKeyData;
31 import com.android.launcher3.shortcuts.DeepShortcutManager;
32 import com.android.launcher3.shortcuts.DeepShortcutView;
33 import com.android.launcher3.util.PackageUserKey;
34 
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.Iterator;
39 import java.util.List;
40 
41 import androidx.annotation.Nullable;
42 import androidx.annotation.VisibleForTesting;
43 
44 /**
45  * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
46  * this class determines which items appear in the container, and in what order.
47  */
48 public class PopupPopulator {
49 
50     public static final int MAX_SHORTCUTS = 4;
51     @VisibleForTesting static final int NUM_DYNAMIC = 2;
52     public static final int MAX_SHORTCUTS_IF_NOTIFICATIONS = 2;
53 
54     /**
55      * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts.
56      */
57     private static final Comparator<ShortcutInfo> SHORTCUT_RANK_COMPARATOR
58             = new Comparator<ShortcutInfo>() {
59         @Override
60         public int compare(ShortcutInfo a, ShortcutInfo b) {
61             if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
62                 return -1;
63             }
64             if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
65                 return 1;
66             }
67             return Integer.compare(a.getRank(), b.getRank());
68         }
69     };
70 
71     /**
72      * Filters the shortcuts so that only MAX_SHORTCUTS or fewer shortcuts are retained.
73      * We want the filter to include both static and dynamic shortcuts, so we always
74      * include NUM_DYNAMIC dynamic shortcuts, if at least that many are present.
75      *
76      * @param shortcutIdToRemoveFirst An id that should be filtered out first, if any.
77      * @return a subset of shortcuts, in sorted order, with size <= MAX_SHORTCUTS.
78      */
sortAndFilterShortcuts( List<ShortcutInfo> shortcuts, @Nullable String shortcutIdToRemoveFirst)79     public static List<ShortcutInfo> sortAndFilterShortcuts(
80             List<ShortcutInfo> shortcuts, @Nullable String shortcutIdToRemoveFirst) {
81         // Remove up to one specific shortcut before sorting and doing somewhat fancy filtering.
82         if (shortcutIdToRemoveFirst != null) {
83             Iterator<ShortcutInfo> shortcutIterator = shortcuts.iterator();
84             while (shortcutIterator.hasNext()) {
85                 if (shortcutIterator.next().getId().equals(shortcutIdToRemoveFirst)) {
86                     shortcutIterator.remove();
87                     break;
88                 }
89             }
90         }
91 
92         Collections.sort(shortcuts, SHORTCUT_RANK_COMPARATOR);
93         if (shortcuts.size() <= MAX_SHORTCUTS) {
94             return shortcuts;
95         }
96 
97         // The list of shortcuts is now sorted with static shortcuts followed by dynamic
98         // shortcuts. We want to preserve this order, but only keep MAX_SHORTCUTS.
99         List<ShortcutInfo> filteredShortcuts = new ArrayList<>(MAX_SHORTCUTS);
100         int numDynamic = 0;
101         int size = shortcuts.size();
102         for (int i = 0; i < size; i++) {
103             ShortcutInfo shortcut = shortcuts.get(i);
104             int filteredSize = filteredShortcuts.size();
105             if (filteredSize < MAX_SHORTCUTS) {
106                 // Always add the first MAX_SHORTCUTS to the filtered list.
107                 filteredShortcuts.add(shortcut);
108                 if (shortcut.isDynamic()) {
109                     numDynamic++;
110                 }
111                 continue;
112             }
113             // At this point, we have MAX_SHORTCUTS already, but they may all be static.
114             // If there are dynamic shortcuts, remove static shortcuts to add them.
115             if (shortcut.isDynamic() && numDynamic < NUM_DYNAMIC) {
116                 numDynamic++;
117                 int lastStaticIndex = filteredSize - numDynamic;
118                 filteredShortcuts.remove(lastStaticIndex);
119                 filteredShortcuts.add(shortcut);
120             }
121         }
122         return filteredShortcuts;
123     }
124 
createUpdateRunnable(final Launcher launcher, final ItemInfo originalInfo, final Handler uiHandler, final PopupContainerWithArrow container, final List<DeepShortcutView> shortcutViews, final List<NotificationKeyData> notificationKeys)125     public static Runnable createUpdateRunnable(final Launcher launcher, final ItemInfo originalInfo,
126             final Handler uiHandler, final PopupContainerWithArrow container,
127             final List<DeepShortcutView> shortcutViews,
128             final List<NotificationKeyData> notificationKeys) {
129         final ComponentName activity = originalInfo.getTargetComponent();
130         final UserHandle user = originalInfo.user;
131         return () -> {
132             if (!notificationKeys.isEmpty()) {
133                 List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
134                         .getStatusBarNotificationsForKeys(notificationKeys);
135                 List<NotificationInfo> infos = new ArrayList<>(notifications.size());
136                 for (int i = 0; i < notifications.size(); i++) {
137                     StatusBarNotification notification = notifications.get(i);
138                     infos.add(new NotificationInfo(launcher, notification));
139                 }
140                 uiHandler.post(() -> container.applyNotificationInfos(infos));
141             }
142 
143             List<ShortcutInfo> shortcuts = DeepShortcutManager.getInstance(launcher)
144                     .queryForShortcutsContainer(activity, user);
145             String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
146                     : notificationKeys.get(0).shortcutId;
147             shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
148             for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
149                 final ShortcutInfo shortcut = shortcuts.get(i);
150                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, launcher);
151                 // Use unbadged icon for the menu.
152                 LauncherIcons li = LauncherIcons.obtain(launcher);
153                 si.applyFrom(li.createShortcutIcon(shortcut, false /* badged */));
154                 li.recycle();
155                 si.rank = i;
156 
157                 final DeepShortcutView view = shortcutViews.get(i);
158                 uiHandler.post(() -> view.applyShortcutInfo(si, shortcut, container));
159             }
160 
161             // This ensures that mLauncher.getWidgetsForPackageUser()
162             // doesn't return null (it puts all the widgets in memory).
163             uiHandler.post(() -> launcher.refreshAndBindWidgetsForPackageUser(
164                     PackageUserKey.fromItemInfo(originalInfo)));
165         };
166     }
167 }
168