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