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