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.WidgetListRowEntry;
35 
36 import java.io.PrintWriter;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Map;
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     /** Maps packages to their Widgets */
63     private ArrayList<WidgetListRowEntry> mAllWidgets = new ArrayList<>();
64 
65     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
66 
PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener)67     public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
68         mNotificationDotsChangeListener = notificationDotsChangeListener;
69     }
70 
updateNotificationDots(Predicate<PackageUserKey> updatedDots)71     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
72         mNotificationDotsChangeListener.accept(updatedDots);
73         mChangeListener.onNotificationDotsUpdated(updatedDots);
74     }
75 
76     @Override
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey)77     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
78             NotificationKeyData notificationKey) {
79         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
80         if (dotInfo == null) {
81             dotInfo = new DotInfo();
82             mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
83         }
84         if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
85             updateNotificationDots(postedPackageUserKey::equals);
86         }
87     }
88 
89     @Override
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)90     public void onNotificationRemoved(PackageUserKey removedPackageUserKey,
91             NotificationKeyData notificationKey) {
92         DotInfo oldDotInfo = mPackageUserToDotInfos.get(removedPackageUserKey);
93         if (oldDotInfo != null && oldDotInfo.removeNotificationKey(notificationKey)) {
94             if (oldDotInfo.getNotificationKeys().size() == 0) {
95                 mPackageUserToDotInfos.remove(removedPackageUserKey);
96             }
97             updateNotificationDots(removedPackageUserKey::equals);
98             trimNotifications(mPackageUserToDotInfos);
99         }
100     }
101 
102     @Override
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)103     public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
104         if (activeNotifications == null) return;
105         // This will contain the PackageUserKeys which have updated dots.
106         HashMap<PackageUserKey, DotInfo> updatedDots = new HashMap<>(mPackageUserToDotInfos);
107         mPackageUserToDotInfos.clear();
108         for (StatusBarNotification notification : activeNotifications) {
109             PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification);
110             DotInfo dotInfo = mPackageUserToDotInfos.get(packageUserKey);
111             if (dotInfo == null) {
112                 dotInfo = new DotInfo();
113                 mPackageUserToDotInfos.put(packageUserKey, dotInfo);
114             }
115             dotInfo.addOrUpdateNotificationKey(NotificationKeyData.fromNotification(notification));
116         }
117 
118         // Add and remove from updatedDots so it contains the PackageUserKeys of updated dots.
119         for (PackageUserKey packageUserKey : mPackageUserToDotInfos.keySet()) {
120             DotInfo prevDot = updatedDots.get(packageUserKey);
121             DotInfo newDot = mPackageUserToDotInfos.get(packageUserKey);
122             if (prevDot == null
123                     || prevDot.getNotificationCount() != newDot.getNotificationCount()) {
124                 updatedDots.put(packageUserKey, newDot);
125             } else {
126                 // No need to update the dot if it already existed (no visual change).
127                 // Note that if the dot was removed entirely, we wouldn't reach this point because
128                 // this loop only includes active notifications added above.
129                 updatedDots.remove(packageUserKey);
130             }
131         }
132 
133         if (!updatedDots.isEmpty()) {
134             updateNotificationDots(updatedDots::containsKey);
135         }
136         trimNotifications(updatedDots);
137     }
138 
trimNotifications(Map<PackageUserKey, DotInfo> updatedDots)139     private void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
140         mChangeListener.trimNotifications(updatedDots);
141     }
142 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)143     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
144         mDeepShortcutMap = deepShortcutMapCopy;
145         if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
146     }
147 
getShortcutCountForItem(ItemInfo info)148     public int getShortcutCountForItem(ItemInfo info) {
149         if (!ShortcutUtil.supportsDeepShortcuts(info)) {
150             return 0;
151         }
152         ComponentName component = info.getTargetComponent();
153         if (component == null) {
154             return 0;
155         }
156 
157         Integer count = mDeepShortcutMap.get(new ComponentKey(component, info.user));
158         return count == null ? 0 : count;
159     }
160 
getDotInfoForItem(@onNull ItemInfo info)161     public @Nullable DotInfo getDotInfoForItem(@NonNull ItemInfo info) {
162         if (!ShortcutUtil.supportsShortcuts(info)) {
163             return null;
164         }
165         DotInfo dotInfo = mPackageUserToDotInfos.get(PackageUserKey.fromItemInfo(info));
166         if (dotInfo == null) {
167             return null;
168         }
169         List<NotificationKeyData> notifications = getNotificationsForItem(
170                 info, dotInfo.getNotificationKeys());
171         if (notifications.isEmpty()) {
172             return null;
173         }
174         return dotInfo;
175     }
176 
getNotificationKeysForItem(ItemInfo info)177     public @NonNull List<NotificationKeyData> getNotificationKeysForItem(ItemInfo info) {
178         DotInfo dotInfo = getDotInfoForItem(info);
179         return dotInfo == null ? Collections.EMPTY_LIST
180                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
181     }
182 
cancelNotification(String notificationKey)183     public void cancelNotification(String notificationKey) {
184         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
185         if (notificationListener == null) {
186             return;
187         }
188         notificationListener.cancelNotificationFromLauncher(notificationKey);
189     }
190 
setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets)191     public void setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets) {
192         mAllWidgets = allWidgets;
193         mChangeListener.onWidgetsBound();
194     }
195 
setChangeListener(PopupDataChangeListener listener)196     public void setChangeListener(PopupDataChangeListener listener) {
197         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
198     }
199 
getAllWidgets()200     public ArrayList<WidgetListRowEntry> getAllWidgets() {
201         return mAllWidgets;
202     }
203 
getWidgetsForPackageUser(PackageUserKey packageUserKey)204     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
205         for (WidgetListRowEntry entry : mAllWidgets) {
206             if (entry.pkgItem.packageName.equals(packageUserKey.mPackageName)) {
207                 ArrayList<WidgetItem> widgets = new ArrayList<>(entry.widgets);
208                 // Remove widgets not associated with the correct user.
209                 Iterator<WidgetItem> iterator = widgets.iterator();
210                 while (iterator.hasNext()) {
211                     if (!iterator.next().user.equals(packageUserKey.mUser)) {
212                         iterator.remove();
213                     }
214                 }
215                 return widgets.isEmpty() ? null : widgets;
216             }
217         }
218         return null;
219     }
220 
221     /**
222      * Returns a list of notifications that are relevant to given ItemInfo.
223      */
getNotificationsForItem( @onNull ItemInfo info, @NonNull List<NotificationKeyData> notifications)224     public static @NonNull List<NotificationKeyData> getNotificationsForItem(
225             @NonNull ItemInfo info, @NonNull List<NotificationKeyData> notifications) {
226         String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info);
227         if (shortcutId == null) {
228             return notifications;
229         }
230         String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info);
231         return notifications.stream().filter((NotificationKeyData notification) -> {
232                     if (notification.shortcutId != null) {
233                         return notification.shortcutId.equals(shortcutId);
234                     }
235                     if (notification.personKeysFromNotification.length != 0) {
236                         return Arrays.equals(notification.personKeysFromNotification, personKeys);
237                     }
238                     return false;
239                 }).collect(Collectors.toList());
240     }
241 
242     public void dump(String prefix, PrintWriter writer) {
243         writer.println(prefix + "PopupDataProvider:");
244         writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
245     }
246 
247     public interface PopupDataChangeListener {
248 
249         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
250 
251         default void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) { }
252 
253         default void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { }
254 
255         default void onWidgetsBound() { }
256     }
257 }
258