diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 974b0df455..b6e5bb0db9 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -76,6 +76,13 @@ android:process=":wallpaper_chooser"> + + + + + + diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index d9e9c7bb02..47a5b4fbdc 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -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(); } /** diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index b3e59f99bf..587d44552e 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -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 diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 9245f187c0..aa5b8c833b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -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 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 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 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; + mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy); } /** diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 6eb87f2db2..e646dd916b 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -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 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 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 a2266fef13..ec1fa34d0f 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.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 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 0a9f87c6e5..98d2277d0d 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 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 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 238b9188fb..787ee724e1 100644 --- a/src/com/android/launcher3/badge/BadgeRenderer.java +++ b/src/com/android/launcher3/badge/BadgeRenderer.java @@ -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, diff --git a/src/com/android/launcher3/badging/NotificationInfo.java b/src/com/android/launcher3/badging/NotificationInfo.java new file mode 100644 index 0000000000..2590add531 --- /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 0000000000..0a85d566b0 --- /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 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 pair + = (Pair) message.obj; + sNotificationsChangedListener.onNotificationPosted(pair.first, pair.second); + } + break; + case MSG_NOTIFICATION_REMOVED: + if (sNotificationsChangedListener != null) { + Pair pair + = (Pair) message.obj; + sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second); + } + break; + case MSG_NOTIFICATION_FULL_REFRESH: + if (sNotificationsChangedListener != null) { + sNotificationsChangedListener.onNotificationFullRefresh( + (List) 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 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 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 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 filterNotifications( + StatusBarNotification[] notifications) { + if (notifications == null) return null; + Set removedNotifications = new HashSet<>(); + for (int i = 0; i < notifications.length; i++) { + if (shouldBeFilteredOut(notifications[i].getNotification())) { + removedNotifications.add(i); + } + } + List 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 activeNotifications); + } +} diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java new file mode 100644 index 0000000000..4ed32b5431 --- /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 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, 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 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.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 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 String[] getNotificationKeysForItem(ItemInfo info) { + BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info)); + Set 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 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 db2654c576..5e12a57c7f 100644 --- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java +++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java @@ -718,7 +718,8 @@ public class DeepShortcutsContainer extends AbstractFloatingView icon.clearFocus(); return null; } - List ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag()); + List 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 0000000000..d08b0e9314 --- /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 4cad836c35..ffb86e4cb9 100644 --- a/src_config/com/android/launcher3/config/FeatureFlags.java +++ b/src_config/com/android/launcher3/config/FeatureFlags.java @@ -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; }