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