• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.model.WidgetsListBaseEntry;
35 import com.android.launcher3.widget.model.WidgetsListContentEntry;
36 
37 import java.io.PrintWriter;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Objects;
44 import java.util.function.Consumer;
45 import java.util.function.Predicate;
46 import java.util.stream.Collectors;
47 
48 /**
49  * Provides data for the popup menu that appears after long-clicking on apps.
50  */
51 public class PopupDataProvider implements NotificationListener.NotificationsChangedListener {
52 
53     private static final boolean LOGD = false;
54     private static final String TAG = "PopupDataProvider";
55 
56     private final Consumer<Predicate<PackageUserKey>> mNotificationDotsChangeListener;
57 
58     /** Maps launcher activity components to a count of how many shortcuts they have. */
59     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
60     /** Maps packages to their DotInfo's . */
61     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
62 
63     /** All installed widgets. */
64     private List<WidgetsListBaseEntry> mAllWidgets = List.of();
65     /** Widgets that can be recommended to the users. */
66     private List<ItemInfo> mRecommendedWidgets = List.of();
67 
68     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
69 
PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener)70     public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
71         mNotificationDotsChangeListener = notificationDotsChangeListener;
72     }
73 
updateNotificationDots(Predicate<PackageUserKey> updatedDots)74     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
75         mNotificationDotsChangeListener.accept(updatedDots);
76         mChangeListener.onNotificationDotsUpdated(updatedDots);
77     }
78 
79     @Override
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey)80     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
81             NotificationKeyData notificationKey) {
82         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
83         if (dotInfo == null) {
84             dotInfo = new DotInfo();
85             mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
86         }
87         if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
88             updateNotificationDots(postedPackageUserKey::equals);
89         }
90     }
91 
92     @Override
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)93     public void onNotificationRemoved(PackageUserKey removedPackageUserKey,
94             NotificationKeyData notificationKey) {
95         DotInfo oldDotInfo = mPackageUserToDotInfos.get(removedPackageUserKey);
96         if (oldDotInfo != null && oldDotInfo.removeNotificationKey(notificationKey)) {
97             if (oldDotInfo.getNotificationKeys().size() == 0) {
98                 mPackageUserToDotInfos.remove(removedPackageUserKey);
99             }
100             updateNotificationDots(removedPackageUserKey::equals);
101             trimNotifications(mPackageUserToDotInfos);
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         trimNotifications(updatedDots);
140     }
141 
trimNotifications(Map<PackageUserKey, DotInfo> updatedDots)142     private void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
143         mChangeListener.trimNotifications(updatedDots);
144     }
145 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)146     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
147         mDeepShortcutMap = deepShortcutMapCopy;
148         if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
149     }
150 
getShortcutCountForItem(ItemInfo info)151     public int getShortcutCountForItem(ItemInfo info) {
152         if (!ShortcutUtil.supportsDeepShortcuts(info)) {
153             return 0;
154         }
155         ComponentName component = info.getTargetComponent();
156         if (component == null) {
157             return 0;
158         }
159 
160         Integer count = mDeepShortcutMap.get(new ComponentKey(component, info.user));
161         return count == null ? 0 : count;
162     }
163 
getDotInfoForItem(@onNull ItemInfo info)164     public @Nullable DotInfo getDotInfoForItem(@NonNull ItemInfo info) {
165         if (!ShortcutUtil.supportsShortcuts(info)) {
166             return null;
167         }
168         DotInfo dotInfo = mPackageUserToDotInfos.get(PackageUserKey.fromItemInfo(info));
169         if (dotInfo == null) {
170             return null;
171         }
172         List<NotificationKeyData> notifications = getNotificationsForItem(
173                 info, dotInfo.getNotificationKeys());
174         if (notifications.isEmpty()) {
175             return null;
176         }
177         return dotInfo;
178     }
179 
getNotificationKeysForItem(ItemInfo info)180     public @NonNull List<NotificationKeyData> getNotificationKeysForItem(ItemInfo info) {
181         DotInfo dotInfo = getDotInfoForItem(info);
182         return dotInfo == null ? Collections.EMPTY_LIST
183                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
184     }
185 
cancelNotification(String notificationKey)186     public void cancelNotification(String notificationKey) {
187         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
188         if (notificationListener == null) {
189             return;
190         }
191         notificationListener.cancelNotificationFromLauncher(notificationKey);
192     }
193 
194     /**
195      * Sets a list of recommended widgets ordered by their order of appearance in the widgets
196      * recommendation UI.
197      */
setRecommendedWidgets(List<ItemInfo> recommendedWidgets)198     public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
199         mRecommendedWidgets = recommendedWidgets;
200         mChangeListener.onRecommendedWidgetsBound();
201     }
202 
setAllWidgets(List<WidgetsListBaseEntry> allWidgets)203     public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
204         mAllWidgets = allWidgets;
205         mChangeListener.onWidgetsBound();
206     }
207 
setChangeListener(PopupDataChangeListener listener)208     public void setChangeListener(PopupDataChangeListener listener) {
209         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
210     }
211 
getAllWidgets()212     public List<WidgetsListBaseEntry> getAllWidgets() {
213         return mAllWidgets;
214     }
215 
216     /** Returns a list of recommended widgets. */
getRecommendedWidgets()217     public List<WidgetItem> getRecommendedWidgets() {
218         HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
219         mAllWidgets.stream()
220                 .filter(entry -> entry instanceof WidgetsListContentEntry)
221                 .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
222                         .forEach(widget -> allWidgetItems.put(
223                                 new ComponentKey(widget.componentName, widget.user), widget)));
224         return mRecommendedWidgets.stream()
225                 .map(recommendedWidget -> allWidgetItems.get(
226                         new ComponentKey(recommendedWidget.getTargetComponent(),
227                                 recommendedWidget.user)))
228                 .filter(Objects::nonNull)
229                 .collect(Collectors.toList());
230     }
231 
getWidgetsForPackageUser(PackageUserKey packageUserKey)232     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
233         return mAllWidgets.stream()
234                 .filter(row -> row instanceof WidgetsListContentEntry
235                         && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
236                 .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
237                 .filter(widget -> packageUserKey.mUser.equals(widget.user))
238                 .collect(Collectors.toList());
239     }
240 
241     /**
242      * Returns a list of notifications that are relevant to given ItemInfo.
243      */
getNotificationsForItem( @onNull ItemInfo info, @NonNull List<NotificationKeyData> notifications)244     public static @NonNull List<NotificationKeyData> getNotificationsForItem(
245             @NonNull ItemInfo info, @NonNull List<NotificationKeyData> notifications) {
246         String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info);
247         if (shortcutId == null) {
248             return notifications;
249         }
250         String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info);
251         return notifications.stream().filter((NotificationKeyData notification) -> {
252                     if (notification.shortcutId != null) {
253                         return notification.shortcutId.equals(shortcutId);
254                     }
255                     if (notification.personKeysFromNotification.length != 0) {
256                         return Arrays.equals(notification.personKeysFromNotification, personKeys);
257                     }
258                     return false;
259                 }).collect(Collectors.toList());
260     }
261 
262     public void dump(String prefix, PrintWriter writer) {
263         writer.println(prefix + "PopupDataProvider:");
264         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
265     }
266 
267     public interface PopupDataChangeListener {
268 
269         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
270 
271         default void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) { }
272 
273         default void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { }
274 
275         default void onWidgetsBound() { }
276 
277         /** A callback to get notified when recommended widgets are bound. */
278         default void onRecommendedWidgetsBound() { }
279     }
280 }
281