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
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 974b0df..b6e5bb0 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -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" />
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index d9e9c7b..47a5b4f 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -39,14 +39,16 @@
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 @@
if (promiseStateChanged || info.isPromise()) {
applyPromiseState(promiseStateChanged);
}
+
+ applyBadgeState(info);
}
public void applyFromApplicationInfo(AppInfo info) {
@@ -178,6 +182,8 @@
// Verify high res immediately
verifyHighRes();
+
+ applyBadgeState(info);
}
public void applyFromPackageItemInfo(PackageItemInfo info) {
@@ -502,8 +508,9 @@
}
}
- 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 @@
* 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();
}
/**
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index b3e59f9..587d445 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -30,12 +30,13 @@
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 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 @@
}
private boolean hasBadge() {
- return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != null;
+ return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
}
@Override
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9245f18..aa5b8c8 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -84,6 +84,8 @@
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.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.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 @@
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 @@
mExtractedColors = new ExtractedColors();
loadExtractedColorsAndColorItems();
+ mPopupDataProvider = new PopupDataProvider(this);
+
((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE))
.addAccessibilityStateChangeListener(this);
@@ -652,6 +656,10 @@
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 @@
if (Utilities.ATLEAST_NOUGAT_MR1) {
mAppWidgetHost.stopListening();
}
+
+ NotificationListener.removeNotificationsChangedListener();
}
@Override
@@ -940,6 +950,10 @@
if (Utilities.ATLEAST_NOUGAT_MR1) {
mAppWidgetHost.startListening();
}
+
+ if (!isWorkspaceLoading()) {
+ NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+ }
}
@Override
@@ -1564,6 +1578,19 @@
}
};
+ 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 @@
InstallShortcutReceiver.disableAndFlushInstallQueue(this);
+ NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+
if (mLauncherCallbacks != null) {
mLauncherCallbacks.finishBindingItems(false);
}
@@ -3741,21 +3770,7 @@
*/
@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);
}
/**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6eb87f2..e646dd9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -78,6 +78,7 @@
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 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 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);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index a2266fe..ec1fa34 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -53,9 +53,11 @@
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 @@
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);
+ }
+ }
+ }
+ }
}
diff --git a/src/com/android/launcher3/badge/BadgeInfo.java b/src/com/android/launcher3/badge/BadgeInfo.java
index 0a9f87c..98d2277 100644
--- a/src/com/android/launcher3/badge/BadgeInfo.java
+++ b/src/com/android/launcher3/badge/BadgeInfo.java
@@ -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();
}
}
diff --git a/src/com/android/launcher3/badge/BadgeRenderer.java b/src/com/android/launcher3/badge/BadgeRenderer.java
index 238b918..787ee72 100644
--- a/src/com/android/launcher3/badge/BadgeRenderer.java
+++ b/src/com/android/launcher3/badge/BadgeRenderer.java
@@ -61,7 +61,7 @@
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,
diff --git a/src/com/android/launcher3/badging/NotificationInfo.java b/src/com/android/launcher3/badging/NotificationInfo.java
new file mode 100644
index 0000000..2590add
--- /dev/null
+++ b/src/com/android/launcher3/badging/NotificationInfo.java
@@ -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);
+ }
+}
diff --git a/src/com/android/launcher3/badging/NotificationListener.java b/src/com/android/launcher3/badging/NotificationListener.java
new file mode 100644
index 0000000..0a85d56
--- /dev/null
+++ b/src/com/android/launcher3/badging/NotificationListener.java
@@ -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);
+ }
+}
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
new file mode 100644
index 0000000..4ed32b5
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -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);
+ }
+}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
index db2654c..5e12a57 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -718,7 +718,8 @@
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(
diff --git a/src/com/android/launcher3/util/PackageUserKey.java b/src/com/android/launcher3/util/PackageUserKey.java
new file mode 100644
index 0000000..d08b0e9
--- /dev/null
+++ b/src/com/android/launcher3/util/PackageUserKey.java
@@ -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);
+ }
+}
diff --git a/src_config/com/android/launcher3/config/FeatureFlags.java b/src_config/com/android/launcher3/config/FeatureFlags.java
index 4cad836..ffb86e4 100644
--- a/src_config/com/android/launcher3/config/FeatureFlags.java
+++ b/src_config/com/android/launcher3/config/FeatureFlags.java
@@ -39,4 +39,6 @@
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;
}