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 com.android.launcher3.ItemInfo;
24 import com.android.launcher3.Launcher;
25 import com.android.launcher3.dot.DotInfo;
26 import com.android.launcher3.model.WidgetItem;
27 import com.android.launcher3.notification.NotificationKeyData;
28 import com.android.launcher3.notification.NotificationListener;
29 import com.android.launcher3.shortcuts.DeepShortcutManager;
30 import com.android.launcher3.util.ComponentKey;
31 import com.android.launcher3.util.PackageUserKey;
32 import com.android.launcher3.util.ShortcutUtil;
33 import com.android.launcher3.widget.WidgetListRowEntry;
34 
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.Iterator;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 import java.util.function.Predicate;
44 import java.util.stream.Collectors;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 
49 /**
50  * Provides data for the popup menu that appears after long-clicking on apps.
51  */
52 public class PopupDataProvider implements NotificationListener.NotificationsChangedListener {
53 
54     private static final boolean LOGD = false;
55     private static final String TAG = "PopupDataProvider";
56 
57     private final Launcher mLauncher;
58 
59     /** Maps launcher activity components to a count of how many shortcuts they have. */
60     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
61     /** Maps packages to their DotInfo's . */
62     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
63     /** Maps packages to their Widgets */
64     private ArrayList<WidgetListRowEntry> mAllWidgets = new ArrayList<>();
65 
66     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
67 
PopupDataProvider(Launcher launcher)68     public PopupDataProvider(Launcher launcher) {
69         mLauncher = launcher;
70     }
71 
updateNotificationDots(Predicate<PackageUserKey> updatedDots)72     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
73         mLauncher.updateNotificationDots(updatedDots);
74         mChangeListener.onNotificationDotsUpdated(updatedDots);
75     }
76 
77     @Override
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut)78     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
79             NotificationKeyData notificationKey, boolean shouldBeFilteredOut) {
80         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
81         boolean dotShouldBeRefreshed;
82         if (dotInfo == null) {
83             if (!shouldBeFilteredOut) {
84                 DotInfo newDotInfo = new DotInfo();
85                 newDotInfo.addOrUpdateNotificationKey(notificationKey);
86                 mPackageUserToDotInfos.put(postedPackageUserKey, newDotInfo);
87                 dotShouldBeRefreshed = true;
88             } else {
89                 dotShouldBeRefreshed = false;
90             }
91         } else {
92             dotShouldBeRefreshed = shouldBeFilteredOut
93                     ? dotInfo.removeNotificationKey(notificationKey)
94                     : dotInfo.addOrUpdateNotificationKey(notificationKey);
95             if (dotInfo.getNotificationKeys().size() == 0) {
96                 mPackageUserToDotInfos.remove(postedPackageUserKey);
97             }
98         }
99         if (dotShouldBeRefreshed) {
100             updateNotificationDots(t -> postedPackageUserKey.equals(t));
101         }
102     }
103 
104     @Override
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)105     public void onNotificationRemoved(PackageUserKey removedPackageUserKey,
106             NotificationKeyData notificationKey) {
107         DotInfo oldDotInfo = mPackageUserToDotInfos.get(removedPackageUserKey);
108         if (oldDotInfo != null && oldDotInfo.removeNotificationKey(notificationKey)) {
109             if (oldDotInfo.getNotificationKeys().size() == 0) {
110                 mPackageUserToDotInfos.remove(removedPackageUserKey);
111             }
112             updateNotificationDots(t -> removedPackageUserKey.equals(t));
113             trimNotifications(mPackageUserToDotInfos);
114         }
115     }
116 
117     @Override
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)118     public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
119         if (activeNotifications == null) return;
120         // This will contain the PackageUserKeys which have updated dots.
121         HashMap<PackageUserKey, DotInfo> updatedDots = new HashMap<>(mPackageUserToDotInfos);
122         mPackageUserToDotInfos.clear();
123         for (StatusBarNotification notification : activeNotifications) {
124             PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification);
125             DotInfo dotInfo = mPackageUserToDotInfos.get(packageUserKey);
126             if (dotInfo == null) {
127                 dotInfo = new DotInfo();
128                 mPackageUserToDotInfos.put(packageUserKey, dotInfo);
129             }
130             dotInfo.addOrUpdateNotificationKey(NotificationKeyData.fromNotification(notification));
131         }
132 
133         // Add and remove from updatedDots so it contains the PackageUserKeys of updated dots.
134         for (PackageUserKey packageUserKey : mPackageUserToDotInfos.keySet()) {
135             DotInfo prevDot = updatedDots.get(packageUserKey);
136             DotInfo newDot = mPackageUserToDotInfos.get(packageUserKey);
137             if (prevDot == null
138                     || prevDot.getNotificationCount() != newDot.getNotificationCount()) {
139                 updatedDots.put(packageUserKey, newDot);
140             } else {
141                 // No need to update the dot if it already existed (no visual change).
142                 // Note that if the dot was removed entirely, we wouldn't reach this point because
143                 // this loop only includes active notifications added above.
144                 updatedDots.remove(packageUserKey);
145             }
146         }
147 
148         if (!updatedDots.isEmpty()) {
149             updateNotificationDots(updatedDots::containsKey);
150         }
151         trimNotifications(updatedDots);
152     }
153 
trimNotifications(Map<PackageUserKey, DotInfo> updatedDots)154     private void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
155         mChangeListener.trimNotifications(updatedDots);
156     }
157 
setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy)158     public void setDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
159         mDeepShortcutMap = deepShortcutMapCopy;
160         if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
161     }
162 
getShortcutCountForItem(ItemInfo info)163     public int getShortcutCountForItem(ItemInfo info) {
164         if (!ShortcutUtil.supportsDeepShortcuts(info)) {
165             return 0;
166         }
167         ComponentName component = info.getTargetComponent();
168         if (component == null) {
169             return 0;
170         }
171 
172         Integer count = mDeepShortcutMap.get(new ComponentKey(component, info.user));
173         return count == null ? 0 : count;
174     }
175 
getDotInfoForItem(@onNull ItemInfo info)176     public @Nullable DotInfo getDotInfoForItem(@NonNull ItemInfo info) {
177         if (!ShortcutUtil.supportsShortcuts(info)) {
178             return null;
179         }
180         DotInfo dotInfo = mPackageUserToDotInfos.get(PackageUserKey.fromItemInfo(info));
181         if (dotInfo == null) {
182             return null;
183         }
184         List<NotificationKeyData> notifications = getNotificationsForItem(
185                 info, dotInfo.getNotificationKeys());
186         if (notifications.isEmpty()) {
187             return null;
188         }
189         return dotInfo;
190     }
191 
getNotificationKeysForItem(ItemInfo info)192     public @NonNull List<NotificationKeyData> getNotificationKeysForItem(ItemInfo info) {
193         DotInfo dotInfo = getDotInfoForItem(info);
194         return dotInfo == null ? Collections.EMPTY_LIST
195                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
196     }
197 
198     /** This makes a potentially expensive binder call and should be run on a background thread. */
getStatusBarNotificationsForKeys( List<NotificationKeyData> notificationKeys)199     public @NonNull List<StatusBarNotification> getStatusBarNotificationsForKeys(
200             List<NotificationKeyData> notificationKeys) {
201         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
202         return notificationListener == null ? Collections.EMPTY_LIST
203                 : notificationListener.getNotificationsForKeys(notificationKeys);
204     }
205 
cancelNotification(String notificationKey)206     public void cancelNotification(String notificationKey) {
207         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
208         if (notificationListener == null) {
209             return;
210         }
211         notificationListener.cancelNotificationFromLauncher(notificationKey);
212     }
213 
setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets)214     public void setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets) {
215         mAllWidgets = allWidgets;
216         mChangeListener.onWidgetsBound();
217     }
218 
setChangeListener(PopupDataChangeListener listener)219     public void setChangeListener(PopupDataChangeListener listener) {
220         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
221     }
222 
getAllWidgets()223     public ArrayList<WidgetListRowEntry> getAllWidgets() {
224         return mAllWidgets;
225     }
226 
getWidgetsForPackageUser(PackageUserKey packageUserKey)227     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
228         for (WidgetListRowEntry entry : mAllWidgets) {
229             if (entry.pkgItem.packageName.equals(packageUserKey.mPackageName)) {
230                 ArrayList<WidgetItem> widgets = new ArrayList<>(entry.widgets);
231                 // Remove widgets not associated with the correct user.
232                 Iterator<WidgetItem> iterator = widgets.iterator();
233                 while (iterator.hasNext()) {
234                     if (!iterator.next().user.equals(packageUserKey.mUser)) {
235                         iterator.remove();
236                     }
237                 }
238                 return widgets.isEmpty() ? null : widgets;
239             }
240         }
241         return null;
242     }
243 
244     /**
245      * Returns a list of notifications that are relevant to given ItemInfo.
246      */
getNotificationsForItem( @onNull ItemInfo info, @NonNull List<NotificationKeyData> notifications)247     public static @NonNull List<NotificationKeyData> getNotificationsForItem(
248             @NonNull ItemInfo info, @NonNull List<NotificationKeyData> notifications) {
249         String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info);
250         if (shortcutId == null) {
251             return notifications;
252         }
253         String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info);
254         return notifications.stream().filter((NotificationKeyData notification) -> {
255                     if (notification.shortcutId != null) {
256                         return notification.shortcutId.equals(shortcutId);
257                     }
258                     if (notification.personKeysFromNotification.length != 0) {
259                         return Arrays.equals(notification.personKeysFromNotification, personKeys);
260                     }
261                     return false;
262                 }).collect(Collectors.toList());
263     }
264 
265     public interface PopupDataChangeListener {
266 
267         PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
268 
269         default void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) { }
270 
271         default void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { }
272 
273         default void onWidgetsBound() { }
274     }
275 }
276