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