/* * 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.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.notification.NotificationKeyData; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import java.io.PrintWriter; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; /** * 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"; private final Consumer> mNotificationDotsChangeListener; /** Maps launcher activity components to a count of how many shortcuts they have. */ private HashMap mDeepShortcutMap = new HashMap<>(); /** Maps packages to their DotInfo's . */ private Map mPackageUserToDotInfos = new HashMap<>(); /** All installed widgets. */ private List mAllWidgets = List.of(); /** Widgets that can be recommended to the users. */ private List mRecommendedWidgets = List.of(); private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE; public PopupDataProvider(Consumer> notificationDotsChangeListener) { mNotificationDotsChangeListener = notificationDotsChangeListener; } private void updateNotificationDots(Predicate updatedDots) { mNotificationDotsChangeListener.accept(updatedDots); mChangeListener.onNotificationDotsUpdated(updatedDots); } @Override public void onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey) { DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey); if (dotInfo == null) { dotInfo = new DotInfo(); mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo); } if (dotInfo.addOrUpdateNotificationKey(notificationKey)) { updateNotificationDots(postedPackageUserKey::equals); } } @Override public void onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey) { DotInfo oldDotInfo = mPackageUserToDotInfos.get(removedPackageUserKey); if (oldDotInfo != null && oldDotInfo.removeNotificationKey(notificationKey)) { if (oldDotInfo.getNotificationKeys().size() == 0) { mPackageUserToDotInfos.remove(removedPackageUserKey); } updateNotificationDots(removedPackageUserKey::equals); trimNotifications(mPackageUserToDotInfos); } } @Override public void onNotificationFullRefresh(List activeNotifications) { if (activeNotifications == null) return; // This will contain the PackageUserKeys which have updated dots. HashMap updatedDots = new HashMap<>(mPackageUserToDotInfos); mPackageUserToDotInfos.clear(); for (StatusBarNotification notification : activeNotifications) { PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification); DotInfo dotInfo = mPackageUserToDotInfos.get(packageUserKey); if (dotInfo == null) { dotInfo = new DotInfo(); mPackageUserToDotInfos.put(packageUserKey, dotInfo); } dotInfo.addOrUpdateNotificationKey(NotificationKeyData.fromNotification(notification)); } // Add and remove from updatedDots so it contains the PackageUserKeys of updated dots. for (PackageUserKey packageUserKey : mPackageUserToDotInfos.keySet()) { DotInfo prevDot = updatedDots.get(packageUserKey); DotInfo newDot = mPackageUserToDotInfos.get(packageUserKey); if (prevDot == null || prevDot.getNotificationCount() != newDot.getNotificationCount()) { updatedDots.put(packageUserKey, newDot); } else { // No need to update the dot if it already existed (no visual change). // Note that if the dot was removed entirely, we wouldn't reach this point because // this loop only includes active notifications added above. updatedDots.remove(packageUserKey); } } if (!updatedDots.isEmpty()) { updateNotificationDots(updatedDots::containsKey); } trimNotifications(updatedDots); } private void trimNotifications(Map updatedDots) { mChangeListener.trimNotifications(updatedDots); } public void setDeepShortcutMap(HashMap deepShortcutMapCopy) { mDeepShortcutMap = deepShortcutMapCopy; if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap); } public int getShortcutCountForItem(ItemInfo info) { if (!ShortcutUtil.supportsDeepShortcuts(info)) { return 0; } ComponentName component = info.getTargetComponent(); if (component == null) { return 0; } Integer count = mDeepShortcutMap.get(new ComponentKey(component, info.user)); return count == null ? 0 : count; } public @Nullable DotInfo getDotInfoForItem(@NonNull ItemInfo info) { if (!ShortcutUtil.supportsShortcuts(info)) { return null; } DotInfo dotInfo = mPackageUserToDotInfos.get(PackageUserKey.fromItemInfo(info)); if (dotInfo == null) { return null; } List notifications = getNotificationsForItem( info, dotInfo.getNotificationKeys()); if (notifications.isEmpty()) { return null; } return dotInfo; } public @NonNull List getNotificationKeysForItem(ItemInfo info) { DotInfo dotInfo = getDotInfoForItem(info); return dotInfo == null ? Collections.EMPTY_LIST : getNotificationsForItem(info, dotInfo.getNotificationKeys()); } public void cancelNotification(String notificationKey) { NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); if (notificationListener == null) { return; } notificationListener.cancelNotificationFromLauncher(notificationKey); } /** * Sets a list of recommended widgets ordered by their order of appearance in the widgets * recommendation UI. */ public void setRecommendedWidgets(List recommendedWidgets) { mRecommendedWidgets = recommendedWidgets; mChangeListener.onRecommendedWidgetsBound(); } public void setAllWidgets(List allWidgets) { mAllWidgets = allWidgets; mChangeListener.onWidgetsBound(); } public void setChangeListener(PopupDataChangeListener listener) { mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener; } public List getAllWidgets() { return mAllWidgets; } /** Returns a list of recommended widgets. */ public List getRecommendedWidgets() { HashMap allWidgetItems = new HashMap<>(); mAllWidgets.stream() .filter(entry -> entry instanceof WidgetsListContentEntry) .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets .forEach(widget -> allWidgetItems.put( new ComponentKey(widget.componentName, widget.user), widget))); return mRecommendedWidgets.stream() .map(recommendedWidget -> allWidgetItems.get( new ComponentKey(recommendedWidget.getTargetComponent(), recommendedWidget.user))) .filter(Objects::nonNull) .collect(Collectors.toList()); } public List getWidgetsForPackageUser(PackageUserKey packageUserKey) { return mAllWidgets.stream() .filter(row -> row instanceof WidgetsListContentEntry && row.mPkgItem.packageName.equals(packageUserKey.mPackageName)) .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream()) .filter(widget -> packageUserKey.mUser.equals(widget.user)) .collect(Collectors.toList()); } /** * Returns a list of notifications that are relevant to given ItemInfo. */ public static @NonNull List getNotificationsForItem( @NonNull ItemInfo info, @NonNull List notifications) { String shortcutId = ShortcutUtil.getShortcutIdIfPinnedShortcut(info); if (shortcutId == null) { return notifications; } String[] personKeys = ShortcutUtil.getPersonKeysIfPinnedShortcut(info); return notifications.stream().filter((NotificationKeyData notification) -> { if (notification.shortcutId != null) { return notification.shortcutId.equals(shortcutId); } if (notification.personKeysFromNotification.length != 0) { return Arrays.equals(notification.personKeysFromNotification, personKeys); } return false; }).collect(Collectors.toList()); } public void dump(String prefix, PrintWriter writer) { writer.println(prefix + "PopupDataProvider:"); writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos); } public interface PopupDataChangeListener { PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { }; default void onNotificationDotsUpdated(Predicate updatedDots) { } default void trimNotifications(Map updatedDots) { } default void onWidgetsBound() { } /** A callback to get notified when recommended widgets are bound. */ default void onRecommendedWidgetsBound() { } } }