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