/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.popup; import android.content.ComponentName; import android.service.notification.StatusBarNotification; import android.support.annotation.NonNull; import android.util.Log; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; import com.android.launcher3.badge.BadgeInfo; import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.notification.NotificationKeyData; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageUserKey; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Provides data for the popup menu that appears after long-clicking on apps. */ public class PopupDataProvider implements NotificationListener.NotificationsChangedListener { private static final boolean LOGD = false; private static final String TAG = "PopupDataProvider"; /** Note that these are in order of priority. */ private static final SystemShortcut[] SYSTEM_SHORTCUTS = new SystemShortcut[] { new SystemShortcut.AppInfo(), new SystemShortcut.Widgets(), }; private final Launcher mLauncher; /** Maps launcher activity components to their list of shortcut ids. */ private MultiHashMap mDeepShortcutMap = new MultiHashMap<>(); /** Maps packages to their BadgeInfo's . */ private Map mPackageUserToBadgeInfos = new HashMap<>(); public PopupDataProvider(Launcher launcher) { mLauncher = launcher; } @Override public void onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut) { BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(postedPackageUserKey); boolean badgeShouldBeRefreshed; if (badgeInfo == null) { if (!shouldBeFilteredOut) { BadgeInfo newBadgeInfo = new BadgeInfo(postedPackageUserKey); newBadgeInfo.addOrUpdateNotificationKey(notificationKey); mPackageUserToBadgeInfos.put(postedPackageUserKey, newBadgeInfo); badgeShouldBeRefreshed = true; } else { badgeShouldBeRefreshed = false; } } else { badgeShouldBeRefreshed = shouldBeFilteredOut ? badgeInfo.removeNotificationKey(notificationKey) : badgeInfo.addOrUpdateNotificationKey(notificationKey); if (badgeInfo.getNotificationKeys().size() == 0) { mPackageUserToBadgeInfos.remove(postedPackageUserKey); } } updateLauncherIconBadges(Utilities.singletonHashSet(postedPackageUserKey), badgeShouldBeRefreshed); } @Override public void onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey) { BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(removedPackageUserKey); if (oldBadgeInfo != null && oldBadgeInfo.removeNotificationKey(notificationKey)) { if (oldBadgeInfo.getNotificationKeys().size() == 0) { mPackageUserToBadgeInfos.remove(removedPackageUserKey); } updateLauncherIconBadges(Utilities.singletonHashSet(removedPackageUserKey)); PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher); if (openContainer != null) { openContainer.trimNotifications(mPackageUserToBadgeInfos); } } } @Override public void onNotificationFullRefresh(List activeNotifications) { if (activeNotifications == null) return; // This will contain the PackageUserKeys which have updated badges. HashMap updatedBadges = new HashMap<>(mPackageUserToBadgeInfos); mPackageUserToBadgeInfos.clear(); for (StatusBarNotification notification : activeNotifications) { PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification); BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(packageUserKey); if (badgeInfo == null) { badgeInfo = new BadgeInfo(packageUserKey); mPackageUserToBadgeInfos.put(packageUserKey, badgeInfo); } badgeInfo.addOrUpdateNotificationKey(NotificationKeyData .fromNotification(notification)); } // Add and remove from updatedBadges so it contains the PackageUserKeys of updated badges. for (PackageUserKey packageUserKey : mPackageUserToBadgeInfos.keySet()) { BadgeInfo prevBadge = updatedBadges.get(packageUserKey); BadgeInfo newBadge = mPackageUserToBadgeInfos.get(packageUserKey); if (prevBadge == null) { updatedBadges.put(packageUserKey, newBadge); } else { if (!prevBadge.shouldBeInvalidated(newBadge)) { updatedBadges.remove(packageUserKey); } } } if (!updatedBadges.isEmpty()) { updateLauncherIconBadges(updatedBadges.keySet()); } PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher); if (openContainer != null) { openContainer.trimNotifications(updatedBadges); } } private void updateLauncherIconBadges(Set updatedBadges) { updateLauncherIconBadges(updatedBadges, true); } /** * Updates the icons on launcher (workspace, folders, all apps) to refresh their badges. * @param updatedBadges The packages whose badges should be refreshed (either a notification was * added or removed, or the badge should show the notification icon). * @param shouldRefresh An optional parameter that will allow us to only refresh badges that * have actually changed. If a notification updated its content but not * its count or icon, then the badge doesn't change. */ private void updateLauncherIconBadges(Set updatedBadges, boolean shouldRefresh) { Iterator iterator = updatedBadges.iterator(); while (iterator.hasNext()) { BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(iterator.next()); if (badgeInfo != null && !updateBadgeIcon(badgeInfo) && !shouldRefresh) { // The notification icon isn't used, and the badge hasn't changed // so there is no update to be made. iterator.remove(); } } if (!updatedBadges.isEmpty()) { mLauncher.updateIconBadges(updatedBadges); } } /** * Determines whether the badge should show a notification icon rather than a number, * and sets that icon on the BadgeInfo if so. * @param badgeInfo The badge to update with an icon (null if it shouldn't show one). * @return Whether the badge icon potentially changed (true unless it stayed null). */ private boolean updateBadgeIcon(BadgeInfo badgeInfo) { boolean hadNotificationToShow = badgeInfo.hasNotificationToShow(); NotificationInfo notificationInfo = null; NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); if (notificationListener != null && badgeInfo.getNotificationKeys().size() >= 1) { // Look for the most recent notification that has an icon that should be shown in badge. for (NotificationKeyData notificationKeyData : badgeInfo.getNotificationKeys()) { String notificationKey = notificationKeyData.notificationKey; StatusBarNotification[] activeNotifications = notificationListener .getActiveNotifications(new String[]{notificationKey}); if (activeNotifications.length == 1) { notificationInfo = new NotificationInfo(mLauncher, activeNotifications[0]); if (notificationInfo.shouldShowIconInBadge()) { // Found an appropriate icon. break; } else { // Keep looking. notificationInfo = null; } } } } badgeInfo.setNotificationToShow(notificationInfo); return hadNotificationToShow || badgeInfo.hasNotificationToShow(); } public void setDeepShortcutMap(MultiHashMap deepShortcutMapCopy) { mDeepShortcutMap = deepShortcutMapCopy; if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap); } public List getShortcutIdsForItem(ItemInfo info) { if (!DeepShortcutManager.supportsShortcuts(info)) { return Collections.EMPTY_LIST; } ComponentName component = info.getTargetComponent(); if (component == null) { return Collections.EMPTY_LIST; } List ids = mDeepShortcutMap.get(new ComponentKey(component, info.user)); return ids == null ? Collections.EMPTY_LIST : ids; } public BadgeInfo getBadgeInfoForItem(ItemInfo info) { if (!DeepShortcutManager.supportsShortcuts(info)) { return null; } return mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info)); } public @NonNull List getNotificationKeysForItem(ItemInfo info) { BadgeInfo badgeInfo = getBadgeInfoForItem(info); return badgeInfo == null ? Collections.EMPTY_LIST : badgeInfo.getNotificationKeys(); } /** This makes a potentially expensive binder call and should be run on a background thread. */ public @NonNull List getStatusBarNotificationsForKeys( List notificationKeys) { NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); return notificationListener == null ? Collections.EMPTY_LIST : notificationListener.getNotificationsForKeys(notificationKeys); } public @NonNull List getEnabledSystemShortcutsForItem(ItemInfo info) { List systemShortcuts = new ArrayList<>(); for (SystemShortcut systemShortcut : SYSTEM_SHORTCUTS) { if (systemShortcut.getOnClickListener(mLauncher, info) != null) { systemShortcuts.add(systemShortcut); } } return systemShortcuts; } public void cancelNotification(String notificationKey) { NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); if (notificationListener == null) { return; } notificationListener.cancelNotification(notificationKey); } }