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