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.service.notification.StatusBarNotification;
21 import android.util.Log;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 
26 import com.android.launcher3.dot.DotInfo;
27 import com.android.launcher3.model.WidgetItem;
28 import com.android.launcher3.model.data.ItemInfo;
29 import com.android.launcher3.notification.NotificationKeyData;
30 import com.android.launcher3.notification.NotificationListener;
31 import com.android.launcher3.util.ComponentKey;
32 import com.android.launcher3.util.PackageUserKey;
33 import com.android.launcher3.util.ShortcutUtil;
34 import com.android.launcher3.widget.PendingAddWidgetInfo;
35 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
36 import com.android.launcher3.widget.model.WidgetsListContentEntry;
37 import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
38 
39 import java.io.PrintWriter;
40 import java.util.Arrays;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.function.Consumer;
46 import java.util.function.Function;
47 import java.util.function.Predicate;
48 import java.util.stream.Collectors;
49 
50 /**
51  * Provides data for the popup menu that appears after long-clicking on apps.
52  */
53 public class PopupDataProvider implements NotificationListener.NotificationsChangedListener {
54 
55     private static final boolean LOGD = false;
56     private static final String TAG = "PopupDataProvider";
57 
58     private final Consumer<Predicate<PackageUserKey>> mNotificationDotsChangeListener;
59 
60     /** Maps launcher activity components to a count of how many shortcuts they have. */
61     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
62     /** Maps packages to their DotInfo's . */
63     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
64 
65     /** All installed widgets. */
66     private List<WidgetsListBaseEntry> mAllWidgets = List.of();
67     /** Widgets that can be recommended to the users. */
68     private List<ItemInfo> mRecommendedWidgets = List.of();
69 
70     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
71 
PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener)72     public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
73         mNotificationDotsChangeListener = notificationDotsChangeListener;
74     }
75 
updateNotificationDots(Predicate<PackageUserKey> updatedDots)76     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
77         mNotificationDotsChangeListener.accept(updatedDots);
78     }
79 
80     @Override
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey)81     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
82             NotificationKeyData notificationKey) {
83         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
84         if (dotInfo == null) {
85             dotInfo = new DotInfo();
86             mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
87         }
88         if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
89             updateNotificationDots(postedPackageUserKey::equals);
90         }
91     }
92 
93     @Override
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)94     public void onNotificationRemoved(PackageUserKey removedPackageUserKey,
95             NotificationKeyData notificationKey) {
96         DotInfo oldDotInfo = mPackageUserToDotInfos.get(removedPackageUserKey);
97         if (oldDotInfo != null && oldDotInfo.removeNotificationKey(notificationKey)) {
98             if (oldDotInfo.getNotificationKeys().size() == 0) {
99                 mPackageUserToDotInfos.remove(removedPackageUserKey);
100             }
101             updateNotificationDots(removedPackageUserKey::equals);
102         }
103     }
104 
105     @Override
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)106     public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
107         if (activeNotifications == null) return;
108         // This will contain the PackageUserKeys which have updated dots.
109         HashMap<PackageUserKey, DotInfo> updatedDots = new HashMap<>(mPackageUserToDotInfos);
110         mPackageUserToDotInfos.clear();
111         for (StatusBarNotification notification : activeNotifications) {
112             PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification);
113             DotInfo dotInfo = mPackageUserToDotInfos.get(packageUserKey);
114             if (dotInfo == null) {
115                 dotInfo = new DotInfo();
116                 mPackageUserToDotInfos.put(packageUserKey, dotInfo);
117             }
118             dotInfo.addOrUpdateNotificationKey(NotificationKeyData.fromNotification(notification));
119         }
120 
121         // Add and remove from updatedDots so it contains the PackageUserKeys of updated dots.
122         for (PackageUserKey packageUserKey : mPackageUserToDotInfos.keySet()) {
123             DotInfo prevDot = updatedDots.get(packageUserKey);
124             DotInfo newDot = mPackageUserToDotInfos.get(packageUserKey);
125             if (prevDot == null
126                     || prevDot.getNotificationCount() != newDot.getNotificationCount()) {
127                 updatedDots.put(packageUserKey, newDot);
128             } else {
129                 // No need to update the dot if it already existed (no visual change).
130                 // Note that if the dot was removed entirely, we wouldn't reach this point because
131                 // this loop only includes active notifications added above.
132                 updatedDots.remove(packageUserKey);
133             }
134         }
135 
136         if (!updatedDots.isEmpty()) {
137             updateNotificationDots(updatedDots::containsKey);
138         }
139     }
140 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)141     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
142         mDeepShortcutMap = deepShortcutMapCopy;
143         if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
144     }
145 
getShortcutCountForItem(ItemInfo info)146     public int getShortcutCountForItem(ItemInfo info) {
147         if (!ShortcutUtil.supportsDeepShortcuts(info)) {
148             return 0;
149         }
150         ComponentName component = info.getTargetComponent();
151         if (component == null) {
152             return 0;
153         }
154 
155         Integer count = mDeepShortcutMap.get(new ComponentKey(component, info.user));
156         return count == null ? 0 : count;
157     }
158 
getDotInfoForItem(@onNull ItemInfo info)159     public @Nullable DotInfo getDotInfoForItem(@NonNull ItemInfo info) {
160         if (!ShortcutUtil.supportsShortcuts(info)) {
161             return null;
162         }
163         DotInfo dotInfo = mPackageUserToDotInfos.get(PackageUserKey.fromItemInfo(info));
164         if (dotInfo == null) {
165             return null;
166         }
167 
168         // If the item represents a pinned shortcut, ensure that there is a notification
169         // for this shortcut
170         String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info);
171         if (shortcutId == null) {
172             return dotInfo;
173         }
174         String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info);
175         return (dotInfo.getNotificationKeys().stream().anyMatch(notification -> {
176             if (notification.shortcutId != null) {
177                 return notification.shortcutId.equals(shortcutId);
178             }
179             if (notification.personKeysFromNotification.length != 0) {
180                 return Arrays.equals(notification.personKeysFromNotification, personKeys);
181             }
182             return false;
183         })) ? dotInfo : null;
184     }
185 
186     /**
187      * Sets a list of recommended widgets ordered by their order of appearance in the widgets
188      * recommendation UI.
189      */
190     public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
191         mRecommendedWidgets = recommendedWidgets;
192         mChangeListener.onRecommendedWidgetsBound();
193     }
194 
195     public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
196         mAllWidgets = allWidgets;
197         mChangeListener.onWidgetsBound();
198     }
199 
200     public void setChangeListener(PopupDataChangeListener listener) {
201         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
202     }
203 
204     public List<WidgetsListBaseEntry> getAllWidgets() {
205         return mAllWidgets;
206     }
207 
208     /** Returns a list of recommended widgets. */
209     public List<WidgetItem> getRecommendedWidgets() {
210         HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
211         mAllWidgets.stream()
212                 .filter(entry -> entry instanceof WidgetsListContentEntry)
213                 .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
214                         .forEach(widget -> allWidgetItems.put(
215                                 new ComponentKey(widget.componentName, widget.user), widget)));
216         return mRecommendedWidgets.stream()
217                 .map(recommendedWidget -> allWidgetItems.get(
218                         new ComponentKey(recommendedWidget.getTargetComponent(),
219                                 recommendedWidget.user)))
220                 .filter(Objects::nonNull)
221                 .collect(Collectors.toList());
222     }
223 
224     /** Returns the recommended widgets mapped by their category. */
225     @NonNull
226     public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
227         Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
228                 .filter(entry -> entry instanceof WidgetsListContentEntry)
229                 .flatMap(entry -> entry.mWidgets.stream())
230                 .distinct()
231                 .collect(Collectors.toMap(
232                         widget -> new ComponentKey(widget.componentName, widget.user),
233                         Function.identity()
234                 ));
235         return mRecommendedWidgets.stream()
236                 .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
237                         && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
238                 .collect(Collectors.groupingBy(
239                         it -> ((PendingAddWidgetInfo) it).recommendationCategory,
240                         Collectors.collectingAndThen(
241                                 Collectors.toList(),
242                                 list -> list.stream()
243                                         .map(it -> allWidgetItems.get(
244                                                 new ComponentKey(it.getTargetComponent(),
245                                                         it.user)))
246                                         .filter(Objects::nonNull)
247                                         .collect(Collectors.toList())
248                         )
249                 ));
250     }
251 
252     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
253         return mAllWidgets.stream()
254                 .filter(row -> row instanceof WidgetsListContentEntry
255                         && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
256                 .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
257                 .filter(widget -> packageUserKey.mUser.equals(widget.user))
258                 .collect(Collectors.toList());
259     }
260 
261     /** Gets the WidgetsListContentEntry for the currently selected header. */
262     public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) {
263         return (WidgetsListContentEntry) mAllWidgets.stream()
264                 .filter(row -> row instanceof WidgetsListContentEntry
265                         && PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey))
266                 .findAny()
267                 .orElse(null);
268     }
269 
270     public void dump(String prefix, PrintWriter writer) {
271         writer.println(prefix + "PopupDataProvider:");
272         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
273     }
274 
275     public interface PopupDataChangeListener {
276 
277         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
278 
279         default void onWidgetsBound() { }
280 
281         /** A callback to get notified when recommended widgets are bound. */
282         default void onRecommendedWidgetsBound() { }
283     }
284 }
285