Add NotificationListener to launcher.

- NotificationListener extends NotificationListenerService, and is
  added to the manifest.
- Added PopupDataProvider, which contains logic for storing and
  interacting with data that goes into the long-press popup menu
  (shortcuts and notifications). A follow-up CL will rename
  DeepShortcutsContainer to a generic PopupContainerWithArrow.
- If Launcher has notification access, NotificationListener will
  get callbacks when notifications are posted and removed; upon
  receiving these callbacks, NotificationListener passes them to
  PopupDataProvider via a NotificationsChangedListener interface.
- Upon receiving the changed notifications, PopupDataProvider maps
  them to the corresponding package/user and tells launcher to
  update relevant icons on the workspace and all apps.

This is guarded by FeatureFlags.BADGE_ICONS.

Bug: 32410600
Change-Id: I59aeb31a7f92399c9c4b831ab551e51e13f44f5c
This commit is contained in:
Tony Wickham 2017-01-20 08:15:28 -08:00
parent c711e6006f
commit 010d255018
14 changed files with 655 additions and 35 deletions

View File

@ -76,6 +76,13 @@
android:process=":wallpaper_chooser">
</service>
<service android:name="com.android.launcher3.badging.NotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<meta-data android:name="android.nfc.disable_beam_default"
android:value="true" />

View File

@ -39,14 +39,16 @@ import android.widget.TextView;
import com.android.launcher3.IconCache.IconLoadRequest;
import com.android.launcher3.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.badge.BadgeRenderer;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.badge.BadgeRenderer;
import com.android.launcher3.badging.NotificationInfo;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.DrawableFactory;
import com.android.launcher3.graphics.HolographicOutlineHelper;
import com.android.launcher3.model.PackageItemInfo;
import java.text.NumberFormat;
import java.util.List;
/**
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
@ -168,6 +170,8 @@ public class BubbleTextView extends TextView
if (promiseStateChanged || info.isPromise()) {
applyPromiseState(promiseStateChanged);
}
applyBadgeState(info);
}
public void applyFromApplicationInfo(AppInfo info) {
@ -178,6 +182,8 @@ public class BubbleTextView extends TextView
// Verify high res immediately
verifyHighRes();
applyBadgeState(info);
}
public void applyFromPackageItemInfo(PackageItemInfo info) {
@ -502,8 +508,9 @@ public class BubbleTextView extends TextView
}
}
public void applyBadgeState(BadgeInfo badgeInfo) {
public void applyBadgeState(ItemInfo itemInfo) {
if (mIcon instanceof FastBitmapDrawable) {
BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
BadgeRenderer badgeRenderer = mLauncher.getDeviceProfile().mBadgeRenderer;
((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer);
}
@ -634,7 +641,8 @@ public class BubbleTextView extends TextView
* Returns true if the view can show custom shortcuts.
*/
public boolean hasDeepShortcuts() {
return !mLauncher.getShortcutIdsForItem((ItemInfo) getTag()).isEmpty();
return !mLauncher.getPopupDataProvider().getShortcutIdsForItem((ItemInfo) getTag())
.isEmpty();
}
/**

View File

@ -30,12 +30,13 @@ import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseArray;
import android.view.animation.DecelerateInterpolator;
import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.badge.BadgeRenderer;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.badge.BadgeRenderer;
import com.android.launcher3.graphics.IconPalette;
public class FastBitmapDrawable extends Drawable {
private static final float DISABLED_DESATURATION = 1f;
@ -123,13 +124,17 @@ public class FastBitmapDrawable extends Drawable {
}
public void applyIconBadge(BadgeInfo badgeInfo, BadgeRenderer badgeRenderer) {
boolean wasBadged = mBadgeInfo != null;
boolean isBadged = badgeInfo != null;
mBadgeInfo = badgeInfo;
mBadgeRenderer = badgeRenderer;
if (mIconPalette == null) {
mIconPalette = IconPalette.fromDominantColor(Utilities
.findDominantColorByHue(mBitmap, 20));
if (wasBadged || isBadged) {
if (mBadgeInfo != null && mIconPalette == null) {
mIconPalette = IconPalette.fromDominantColor(Utilities
.findDominantColorByHue(mBitmap, 20));
}
invalidateSelf();
}
invalidateSelf();
}
@Override
@ -157,7 +162,7 @@ public class FastBitmapDrawable extends Drawable {
}
private boolean hasBadge() {
return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != null;
return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
}
@Override

View File

@ -84,6 +84,8 @@ import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.DefaultAppSearchController;
import com.android.launcher3.anim.AnimationLayerSet;
import com.android.launcher3.badging.NotificationListener;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.LauncherAppsCompat;
import com.android.launcher3.compat.PinItemRequestCompat;
@ -117,6 +119,7 @@ import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.PendingRequestArgs;
import com.android.launcher3.util.TestingUtils;
import com.android.launcher3.util.Thunk;
@ -130,10 +133,10 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Default launcher application.
@ -260,8 +263,7 @@ public class Launcher extends BaseActivity
private boolean mHasFocus = false;
private boolean mAttached = false;
/** Maps launcher activity components to their list of shortcut ids. */
private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
private PopupDataProvider mPopupDataProvider;
private View.OnTouchListener mHapticFeedbackTouchListener;
@ -394,6 +396,8 @@ public class Launcher extends BaseActivity
mExtractedColors = new ExtractedColors();
loadExtractedColorsAndColorItems();
mPopupDataProvider = new PopupDataProvider(this);
((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE))
.addAccessibilityStateChangeListener(this);
@ -652,6 +656,10 @@ public class Launcher extends BaseActivity
return (int) info.id;
}
public PopupDataProvider getPopupDataProvider() {
return mPopupDataProvider;
}
/**
* Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
* a configuration step, this allows the proper animations to run after other transitions.
@ -926,6 +934,8 @@ public class Launcher extends BaseActivity
if (Utilities.ATLEAST_NOUGAT_MR1) {
mAppWidgetHost.stopListening();
}
NotificationListener.removeNotificationsChangedListener();
}
@Override
@ -940,6 +950,10 @@ public class Launcher extends BaseActivity
if (Utilities.ATLEAST_NOUGAT_MR1) {
mAppWidgetHost.startListening();
}
if (!isWorkspaceLoading()) {
NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
}
}
@Override
@ -1564,6 +1578,19 @@ public class Launcher extends BaseActivity
}
};
public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
Runnable r = new Runnable() {
@Override
public void run() {
mWorkspace.updateIconBadges(updatedBadges);
mAppsView.updateIconBadges(updatedBadges);
}
};
if (!waitUntilResume(r)) {
r.run();
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
@ -3672,6 +3699,8 @@ public class Launcher extends BaseActivity
InstallShortcutReceiver.disableAndFlushInstallQueue(this);
NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
if (mLauncherCallbacks != null) {
mLauncherCallbacks.finishBindingItems(false);
}
@ -3741,21 +3770,7 @@ public class Launcher extends BaseActivity
*/
@Override
public void bindDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
mDeepShortcutMap = deepShortcutMapCopy;
if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
}
public List<String> getShortcutIdsForItem(ItemInfo info) {
if (!DeepShortcutManager.supportsShortcuts(info)) {
return Collections.EMPTY_LIST;
}
ComponentName component = info.getTargetComponent();
if (component == null) {
return Collections.EMPTY_LIST;
}
List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
return ids == null ? Collections.EMPTY_LIST : ids;
mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
}
/**

View File

@ -78,6 +78,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LongArrayMap;
import com.android.launcher3.util.MultiStateAlphaController;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.VerticalFlingDetector;
import com.android.launcher3.util.WallpaperOffsetInterpolator;
@ -86,6 +87,7 @@ import com.android.launcher3.widget.PendingAddWidgetInfo;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* The workspace is a wide area with a wallpaper and a finite number of pages.
@ -3957,6 +3959,23 @@ public class Workspace extends PagedView
});
}
public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
mapOverItems(MAP_RECURSE, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View v) {
if (info instanceof ShortcutInfo && v instanceof BubbleTextView) {
packageUserKey.updateFromItemInfo(info);
if (updatedBadges.contains(packageUserKey)) {
((BubbleTextView) v).applyBadgeState(info);
}
}
// process all the shortcuts
return false;
}
});
}
public void removeAbandonedPromise(String packageName, UserHandle user) {
HashSet<String> packages = new HashSet<>(1);
packages.add(packageName);

View File

@ -53,9 +53,11 @@ import com.android.launcher3.graphics.TintedDrawableSpan;
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* The all apps view container.
@ -472,4 +474,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
navBarBg.setVisibility(View.VISIBLE);
}
}
public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
for (AlphabeticalAppsList.AdapterItem app : mApps.getAdapterItems()) {
if (app.appInfo != null) {
packageUserKey.updateFromItemInfo(app.appInfo);
if (updatedBadges.contains(packageUserKey)) {
mAdapter.notifyItemChanged(app.position);
}
}
}
}
}

View File

@ -16,18 +16,60 @@
package com.android.launcher3.badge;
import com.android.launcher3.util.PackageUserKey;
import java.util.HashSet;
import java.util.Set;
/**
* Contains data to be used in an icon badge.
*/
public class BadgeInfo {
private int mNotificationCount;
/** Used to link this BadgeInfo to icons on the workspace and all apps */
private PackageUserKey mPackageUserKey;
/**
* The keys of the notifications that this badge represents. These keys can later be
* used to retrieve {@link com.android.launcher3.badging.NotificationInfo}'s.
*/
private Set<String> mNotificationKeys;
public void setNotificationCount(int count) {
mNotificationCount = count;
public BadgeInfo(PackageUserKey packageUserKey) {
mPackageUserKey = packageUserKey;
mNotificationKeys = new HashSet<>();
}
public String getNotificationCount() {
return mNotificationCount == 0 ? null : String.valueOf(mNotificationCount);
/**
* Returns whether the notification was added (false if it already existed).
*/
public boolean addNotificationKey(String notificationKey) {
return mNotificationKeys.add(notificationKey);
}
/**
* Returns whether the notification was removed (false if it didn't exist).
*/
public boolean removeNotificationKey(String notificationKey) {
return mNotificationKeys.remove(notificationKey);
}
public Set<String> getNotificationKeys() {
return mNotificationKeys;
}
public int getNotificationCount() {
return mNotificationKeys.size();
}
/**
* Whether newBadge represents the same PackageUserKey as this badge, and icons with
* this badge should be invalidated. So, for instance, if a badge has 3 notifications
* and one of those notifications is updated, this method should return false because
* the badge still says "3" and the contents of those notifications are only retrieved
* upon long-click. This method always returns true when adding or removing notifications.
*/
public boolean shouldBeInvalidated(BadgeInfo newBadge) {
return mPackageUserKey.equals(newBadge.mPackageUserKey)
&& getNotificationCount() != newBadge.getNotificationCount();
}
}

View File

@ -61,7 +61,7 @@ public class BadgeRenderer {
mBackgroundRect.set(iconBounds.right - size, iconBounds.top, iconBounds.right,
iconBounds.top + size);
canvas.drawOval(mBackgroundRect, mBackgroundPaint);
String notificationCount = badgeInfo.getNotificationCount();
String notificationCount = String.valueOf(badgeInfo.getNotificationCount());
canvas.drawText(notificationCount,
mBackgroundRect.centerX(),
mBackgroundRect.centerY() + mTextHeight / 2,

View File

@ -0,0 +1,82 @@
/*
* 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.badging;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.service.notification.StatusBarNotification;
import android.view.View;
import com.android.launcher3.Launcher;
import com.android.launcher3.shortcuts.DeepShortcutsContainer;
import com.android.launcher3.util.PackageUserKey;
/**
* An object that contains relevant information from a {@link StatusBarNotification}. This should
* only be created when we need to show the notification contents on the UI; until then, a
* {@link com.android.launcher3.badge.BadgeInfo} with only the notification key should
* be passed around, and then this can be constructed using the StatusBarNotification from
* {@link NotificationListener#getNotificationsForKeys(String[])}.
*/
public class NotificationInfo implements View.OnClickListener {
public final PackageUserKey packageUserKey;
public final String notificationKey;
public final CharSequence title;
public final CharSequence text;
public final Drawable iconDrawable;
public final PendingIntent intent;
public final boolean autoCancel;
/**
* Extracts the data that we need from the StatusBarNotification.
*/
public NotificationInfo(Context context, StatusBarNotification notification) {
packageUserKey = PackageUserKey.fromNotification(notification);
notificationKey = notification.getKey();
title = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
text = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TEXT);
Icon icon = notification.getNotification().getLargeIcon();
if (icon == null) {
icon = notification.getNotification().getSmallIcon();
iconDrawable = icon.loadDrawable(context);
iconDrawable.setTint(notification.getNotification().color);
} else {
iconDrawable = icon.loadDrawable(context);
}
intent = notification.getNotification().contentIntent;
autoCancel = (notification.getNotification().flags
& Notification.FLAG_AUTO_CANCEL) != 0;
}
@Override
public void onClick(View view) {
final Launcher launcher = Launcher.getLauncher(view.getContext());
try {
intent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
if (autoCancel) {
launcher.getPopupDataProvider().cancelNotification(notificationKey);
}
DeepShortcutsContainer.getOpen(launcher).close(true);
}
}

View File

@ -0,0 +1,213 @@
/*
* 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.badging;
import android.app.Notification;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.annotation.Nullable;
import android.support.v4.util.Pair;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A {@link NotificationListenerService} that sends updates to its
* {@link NotificationsChangedListener} when notifications are posted or canceled,
* as well and when this service first connects. An instance of NotificationListener,
* and its methods for getting notifications, can be obtained via {@link #getInstance()}.
*/
public class NotificationListener extends NotificationListenerService {
private static final int MSG_NOTIFICATION_POSTED = 1;
private static final int MSG_NOTIFICATION_REMOVED = 2;
private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
private static NotificationListener sNotificationListenerInstance = null;
private static NotificationsChangedListener sNotificationsChangedListener;
private final Handler mWorkerHandler;
private final Handler mUiHandler;
private Handler.Callback mWorkerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_NOTIFICATION_POSTED:
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
break;
case MSG_NOTIFICATION_REMOVED:
mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
break;
case MSG_NOTIFICATION_FULL_REFRESH:
final List<StatusBarNotification> activeNotifications
= filterNotifications(getActiveNotifications());
mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
break;
}
return true;
}
};
private Handler.Callback mUiCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_NOTIFICATION_POSTED:
if (sNotificationsChangedListener != null) {
Pair<PackageUserKey, String> pair
= (Pair<PackageUserKey, String>) message.obj;
sNotificationsChangedListener.onNotificationPosted(pair.first, pair.second);
}
break;
case MSG_NOTIFICATION_REMOVED:
if (sNotificationsChangedListener != null) {
Pair<PackageUserKey, String> pair
= (Pair<PackageUserKey, String>) message.obj;
sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
}
break;
case MSG_NOTIFICATION_FULL_REFRESH:
if (sNotificationsChangedListener != null) {
sNotificationsChangedListener.onNotificationFullRefresh(
(List<StatusBarNotification>) message.obj);
}
break;
}
return true;
}
};
public NotificationListener() {
super();
mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
}
public static @Nullable NotificationListener getInstance() {
return sNotificationListenerInstance;
}
public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
if (!FeatureFlags.BADGE_ICONS) {
return;
}
sNotificationsChangedListener = listener;
NotificationListener notificationListener = getInstance();
if (notificationListener != null) {
notificationListener.onNotificationFullRefresh();
}
}
public static void removeNotificationsChangedListener() {
sNotificationsChangedListener = null;
}
@Override
public void onListenerConnected() {
super.onListenerConnected();
sNotificationListenerInstance = this;
onNotificationFullRefresh();
}
private void onNotificationFullRefresh() {
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
}
@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
sNotificationListenerInstance = null;
}
@Override
public void onNotificationPosted(final StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
if (!shouldBeFilteredOut(sbn.getNotification())) {
Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
= new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, packageUserKeyAndNotificationKey)
.sendToTarget();
}
}
@Override
public void onNotificationRemoved(final StatusBarNotification sbn) {
super.onNotificationRemoved(sbn);
if (!shouldBeFilteredOut(sbn.getNotification())) {
Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
= new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
.sendToTarget();
}
}
/** This makes a potentially expensive binder call and should be run on a background thread. */
public List<StatusBarNotification> getNotificationsForKeys(String[] keys) {
StatusBarNotification[] notifications = NotificationListener.this
.getActiveNotifications(keys);
return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications);
}
/**
* Filter out notifications that don't have an intent
* or are headers for grouped notifications.
*
* TODO: use the system concept of a badged notification instead
*/
private List<StatusBarNotification> filterNotifications(
StatusBarNotification[] notifications) {
if (notifications == null) return null;
Set<Integer> removedNotifications = new HashSet<>();
for (int i = 0; i < notifications.length; i++) {
if (shouldBeFilteredOut(notifications[i].getNotification())) {
removedNotifications.add(i);
}
}
List<StatusBarNotification> filteredNotifications = new ArrayList<>(
notifications.length - removedNotifications.size());
for (int i = 0; i < notifications.length; i++) {
if (!removedNotifications.contains(i)) {
filteredNotifications.add(notifications[i]);
}
}
return filteredNotifications;
}
private boolean shouldBeFilteredOut(Notification notification) {
boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
return (notification.contentIntent == null || isGroupHeader);
}
public interface NotificationsChangedListener {
void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey);
void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey);
void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
}
}

View File

@ -0,0 +1,161 @@
/*
* 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 com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.badging.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.Collections;
import java.util.HashMap;
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";
private final Launcher mLauncher;
/** Maps launcher activity components to their list of shortcut ids. */
private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
/** Maps packages to their BadgeInfo's . */
private Map<PackageUserKey, BadgeInfo> mPackageUserToBadgeInfos = new HashMap<>();
public PopupDataProvider(Launcher launcher) {
mLauncher = launcher;
}
@Override
public void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey) {
BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(postedPackageUserKey);
if (oldBadgeInfo == null) {
BadgeInfo newBadgeInfo = new BadgeInfo(postedPackageUserKey);
newBadgeInfo.addNotificationKey(notificationKey);
mPackageUserToBadgeInfos.put(postedPackageUserKey, newBadgeInfo);
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
} else if (oldBadgeInfo.addNotificationKey(notificationKey)) {
mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
}
}
@Override
public void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey) {
BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(removedPackageUserKey);
if (oldBadgeInfo != null && oldBadgeInfo.removeNotificationKey(notificationKey)) {
if (oldBadgeInfo.getNotificationCount() == 0) {
mPackageUserToBadgeInfos.remove(removedPackageUserKey);
}
mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey));
}
}
@Override
public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
if (activeNotifications == null) return;
// This will contain the PackageUserKeys which have updated badges.
HashMap<PackageUserKey, BadgeInfo> 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.addNotificationKey(notification.getKey());
}
// 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()) {
mLauncher.updateIconBadges(updatedBadges.keySet());
}
}
public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
mDeepShortcutMap = deepShortcutMapCopy;
if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
}
public List<String> getShortcutIdsForItem(ItemInfo info) {
if (!DeepShortcutManager.supportsShortcuts(info)) {
return Collections.EMPTY_LIST;
}
ComponentName component = info.getTargetComponent();
if (component == null) {
return Collections.EMPTY_LIST;
}
List<String> 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 String[] getNotificationKeysForItem(ItemInfo info) {
BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
Set<String> notificationKeys = badgeInfo.getNotificationKeys();
return notificationKeys.toArray(new String[notificationKeys.size()]);
}
/** This makes a potentially expensive binder call and should be run on a background thread. */
public List<StatusBarNotification> getStatusBarNotificationsForKeys(String[] notificationKeys) {
NotificationListener notificationListener = NotificationListener.getInstance();
return notificationListener == null ? Collections.EMPTY_LIST
: notificationListener.getNotificationsForKeys(notificationKeys);
}
public void cancelNotification(String notificationKey) {
NotificationListener notificationListener = NotificationListener.getInstance();
if (notificationListener == null) {
return;
}
notificationListener.cancelNotification(notificationKey);
}
}

View File

@ -718,7 +718,8 @@ public class DeepShortcutsContainer extends AbstractFloatingView
icon.clearFocus();
return null;
}
List<String> ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag());
List<String> ids = launcher.getPopupDataProvider().getShortcutIdsForItem(
(ItemInfo) icon.getTag());
if (!ids.isEmpty()) {
final DeepShortcutsContainer container =
(DeepShortcutsContainer) launcher.getLayoutInflater().inflate(

View File

@ -0,0 +1,51 @@
package com.android.launcher3.util;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import com.android.launcher3.ItemInfo;
import java.util.Arrays;
/** Creates a hash key based on package name and user. */
public class PackageUserKey {
private String mPackageName;
private UserHandle mUser;
private int mHashCode;
public static PackageUserKey fromItemInfo(ItemInfo info) {
return new PackageUserKey(info.getTargetComponent().getPackageName(), info.user);
}
public static PackageUserKey fromNotification(StatusBarNotification notification) {
return new PackageUserKey(notification.getPackageName(), notification.getUser());
}
public PackageUserKey(String packageName, UserHandle user) {
update(packageName, user);
}
private void update(String packageName, UserHandle user) {
mPackageName = packageName;
mUser = user;
mHashCode = Arrays.hashCode(new Object[] {packageName, user});
}
/** This should only be called to avoid new object creations in a loop. */
public void updateFromItemInfo(ItemInfo info) {
update(info.getTargetComponent().getPackageName(), info.user);
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PackageUserKey)) return false;
PackageUserKey otherKey = (PackageUserKey) obj;
return mPackageName.equals(otherKey.mPackageName) && mUser.equals(otherKey.mUser);
}
}

View File

@ -39,4 +39,6 @@ public final class FeatureFlags {
public static final boolean LIGHT_STATUS_BAR = false;
// When enabled allows to use any point on the fast scrollbar to start dragging.
public static final boolean LAUNCHER3_DIRECT_SCROLL = true;
// When enabled icons are badged with the number of notifications associated with that app.
public static final boolean BADGE_ICONS = true;
}