From a92268cd01aeaf66c4126f9197fed423fdbe9202 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Mon, 9 Mar 2020 17:25:08 -0700 Subject: [PATCH 1/3] Make bubble settings a pref with an int rather than a bool This will allow us to do all/selected/none preferences for bubbles in settings. - Feature is on by default - App is none by default - Channel is off by default Test: atest NotificationManagerServiceTest BubbleExtractorTest Bug: 138116133 Change-Id: Ifad1c22525123354f76959c2d44392a25d56347d --- .../android/app/INotificationManager.aidl | 5 +- .../java/android/app/NotificationChannel.java | 21 +- .../java/android/app/NotificationManager.java | 15 +- .../row/NotificationConversationInfo.java | 9 + .../server/notification/BubbleExtractor.java | 261 +++++-------- .../NotificationManagerService.java | 27 +- .../notification/PreferencesHelper.java | 52 +-- .../server/notification/RankingConfig.java | 2 +- .../notification/BubbleCheckerTest.java | 261 ------------- .../notification/BubbleExtractorTest.java | 353 +++++++++++++++--- .../NotificationManagerServiceTest.java | 224 ++++++----- .../notification/PreferencesHelperTest.java | 50 ++- 12 files changed, 655 insertions(+), 625 deletions(-) delete mode 100644 services/tests/uiservicestests/src/com/android/server/notification/BubbleCheckerTest.java diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 78d3581a70129..9d0364eba39f5 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -78,10 +78,9 @@ interface INotificationManager boolean shouldHideSilentStatusIcons(String callingPkg); void setHideSilentStatusIcons(boolean hide); - void setBubblesAllowed(String pkg, int uid, boolean allowed); + void setBubblesAllowed(String pkg, int uid, int bubblePreference); boolean areBubblesAllowed(String pkg); - boolean areBubblesAllowedForPackage(String pkg, int uid); - boolean hasUserApprovedBubblesForPackage(String pkg, int uid); + int getBubblePreferenceForPackage(String pkg, int uid); void createNotificationChannelGroups(String pkg, in ParceledListSlice channelGroupList); void createNotificationChannels(String pkg, in ParceledListSlice channelsList); diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index d1d67f0d81d87..2feb9277775c5 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -102,7 +102,7 @@ public final class NotificationChannel implements Parcelable { private static final String ATT_FG_SERVICE_SHOWN = "fgservice"; private static final String ATT_GROUP = "group"; private static final String ATT_BLOCKABLE_SYSTEM = "blockable_system"; - private static final String ATT_ALLOW_BUBBLE = "can_bubble"; + private static final String ATT_ALLOW_BUBBLE = "allow_bubble"; private static final String ATT_ORIG_IMP = "orig_imp"; private static final String ATT_PARENT_CHANNEL = "parent"; private static final String ATT_CONVERSATION_ID = "conv_id"; @@ -168,7 +168,7 @@ public final class NotificationChannel implements Parcelable { NotificationManager.IMPORTANCE_UNSPECIFIED; private static final boolean DEFAULT_DELETED = false; private static final boolean DEFAULT_SHOW_BADGE = true; - private static final boolean DEFAULT_ALLOW_BUBBLE = true; + private static final boolean DEFAULT_ALLOW_BUBBLE = false; @UnsupportedAppUsage private String mId; @@ -545,15 +545,8 @@ public final class NotificationChannel implements Parcelable { } /** - * Sets whether notifications posted to this channel can appear outside of the notification - * shade, floating over other apps' content as a bubble. - * - *

This value will be ignored for channels that aren't allowed to pop on screen (that is, - * channels whose {@link #getImportance() importance} is < - * {@link NotificationManager#IMPORTANCE_HIGH}.

- * - *

Only modifiable before the channel is submitted to - * * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.

+ * As of Android 11 this value is no longer respected. + * @see #canBubble() * @see Notification#getBubbleMetadata() */ public void setAllowBubbles(boolean allowBubbles) { @@ -702,8 +695,10 @@ public final class NotificationChannel implements Parcelable { } /** - * Returns whether notifications posted to this channel can display outside of the notification - * shade, in a floating window on top of other apps. + * Returns whether notifications posted to this channel are allowed to display outside of the + * notification shade, in a floating window on top of other apps. + * + * @see Notification#getBubbleMetadata() */ public boolean canBubble() { return mAllowBubbles; diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 0e97e3fe06cee..d6df400f86b62 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -452,6 +452,19 @@ public class NotificationManager { */ public static final int IMPORTANCE_MAX = 5; + /** + * @hide + */ + public static final int BUBBLE_PREFERENCE_NONE = 0; + /** + * @hide + */ + public static final int BUBBLE_PREFERENCE_ALL = 1; + /** + * @hide + */ + public static final int BUBBLE_PREFERENCE_SELECTED = 2; + @UnsupportedAppUsage private static INotificationManager sService; @@ -1213,7 +1226,7 @@ public class NotificationManager { /** - * Sets whether notifications posted by this app can appear outside of the + * Gets whether all notifications posted by this app can appear outside of the * notification shade, floating over other apps' content. * *

This value will be ignored for notifications that are posted to channels that do not diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 6fc1264d69e2a..290784ddbffec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; @@ -598,6 +600,13 @@ public class NotificationConversationInfo extends LinearLayout implements !mChannelToUpdate.isImportantConversation()); if (mChannelToUpdate.isImportantConversation()) { mChannelToUpdate.setAllowBubbles(true); + int currentPref = + mINotificationManager.getBubblePreferenceForPackage( + mAppPkg, mAppUid); + if (currentPref == BUBBLE_PREFERENCE_NONE) { + mINotificationManager.setBubblesAllowed(mAppPkg, mAppUid, + BUBBLE_PREFERENCE_SELECTED); + } } mChannelToUpdate.setImportance(Math.max( mChannelToUpdate.getOriginalImportance(), IMPORTANCE_DEFAULT)); diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java index 2fa80cd8e4e4f..536ea30d6a947 100644 --- a/services/core/java/com/android/server/notification/BubbleExtractor.java +++ b/services/core/java/com/android/server/notification/BubbleExtractor.java @@ -16,12 +16,17 @@ package com.android.server.notification; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static com.android.internal.util.FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING; import static com.android.internal.util.FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE; import android.app.ActivityManager; import android.app.Notification; +import android.app.NotificationChannel; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -32,13 +37,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; /** - * Determines whether a bubble can be shown for this notification + * Determines whether a bubble can be shown for this notification. */ public class BubbleExtractor implements NotificationSignalExtractor { private static final String TAG = "BubbleExtractor"; private static final boolean DBG = false; - private BubbleChecker mBubbleChecker; + private ShortcutHelper mShortcutHelper; private RankingConfig mConfig; private ActivityManager mActivityManager; private Context mContext; @@ -60,24 +65,34 @@ public class BubbleExtractor implements NotificationSignalExtractor { return null; } - if (mBubbleChecker == null) { - if (DBG) Slog.d(TAG, "missing bubble checker"); + if (mShortcutHelper == null) { + if (DBG) Slog.d(TAG, "missing shortcut helper"); return null; } - boolean appCanShowBubble = - mConfig.areBubblesAllowed(record.getSbn().getPackageName(), record.getSbn().getUid()); - if (!mConfig.bubblesEnabled() || !appCanShowBubble) { + int bubblePreference = + mConfig.getBubblePreference( + record.getSbn().getPackageName(), record.getSbn().getUid()); + NotificationChannel recordChannel = record.getChannel(); + + if (!mConfig.bubblesEnabled() || bubblePreference == BUBBLE_PREFERENCE_NONE) { record.setAllowBubble(false); - } else { - if (record.getChannel() != null) { - record.setAllowBubble(record.getChannel().canBubble() && appCanShowBubble); - } else { - record.setAllowBubble(appCanShowBubble); - } + } else if (recordChannel == null) { + // the app is allowed but there's no channel to check + record.setAllowBubble(true); + } else if (bubblePreference == BUBBLE_PREFERENCE_ALL) { + // by default the channel is not allowed, only don't bubble if the user specified + boolean userLockedNoBubbles = !recordChannel.canBubble() + && (recordChannel.getUserLockedFields() & USER_LOCKED_ALLOW_BUBBLE) != 0; + record.setAllowBubble(!userLockedNoBubbles); + } else if (bubblePreference == BUBBLE_PREFERENCE_SELECTED) { + record.setAllowBubble(recordChannel.canBubble()); } - final boolean applyFlag = mBubbleChecker.isNotificationAppropriateToBubble(record) - && !record.isFlagBubbleRemoved(); + + final boolean fulfillsPolicy = record.isConversation() + && !mActivityManager.isLowRamDevice() + && record.canBubble(); + final boolean applyFlag = fulfillsPolicy && canPresentAsBubble(record); if (applyFlag) { record.getNotification().flags |= FLAG_BUBBLE; } else { @@ -95,165 +110,95 @@ public class BubbleExtractor implements NotificationSignalExtractor { public void setZenHelper(ZenModeHelper helper) { } - /** - * Expected to be called after {@link #setConfig(RankingConfig)} has occurred. - */ - void setShortcutHelper(ShortcutHelper helper) { - if (mConfig == null) { - if (DBG) Slog.d(TAG, "setting shortcut helper prior to setConfig"); - return; - } - mBubbleChecker = new BubbleChecker(mContext, helper, mConfig, mActivityManager); + public void setShortcutHelper(ShortcutHelper helper) { + mShortcutHelper = helper; } @VisibleForTesting - void setBubbleChecker(BubbleChecker checker) { - mBubbleChecker = checker; + public void setActivityManager(ActivityManager manager) { + mActivityManager = manager; } /** - * Encapsulates special checks to see if a notification can be flagged as a bubble. This - * makes testing a bit easier. + * @return whether there is valid information for the notification to bubble. */ - public static class BubbleChecker { - - private ActivityManager mActivityManager; - private RankingConfig mRankingConfig; - private Context mContext; - private ShortcutHelper mShortcutHelper; - - BubbleChecker(Context context, ShortcutHelper helper, RankingConfig config, - ActivityManager activityManager) { - mContext = context; - mActivityManager = activityManager; - mShortcutHelper = helper; - mRankingConfig = config; + @VisibleForTesting + boolean canPresentAsBubble(NotificationRecord r) { + Notification notification = r.getNotification(); + Notification.BubbleMetadata metadata = notification.getBubbleMetadata(); + String pkg = r.getSbn().getPackageName(); + if (metadata == null) { + return false; } - /** - * @return whether the provided notification record is allowed to be represented as a - * bubble, accounting for user choice & policy. - */ - public boolean isNotificationAppropriateToBubble(NotificationRecord r) { - final String pkg = r.getSbn().getPackageName(); - final int userId = r.getSbn().getUser().getIdentifier(); - Notification notification = r.getNotification(); - if (!canBubble(r, pkg, userId)) { - // no log: canBubble has its own - return false; - } - - if (mActivityManager.isLowRamDevice()) { - logBubbleError(r.getKey(), "low ram device"); - return false; - } - - boolean isMessageStyle = Notification.MessagingStyle.class.equals( - notification.getNotificationStyle()); - if (!isMessageStyle) { - logBubbleError(r.getKey(), "must be Notification.MessageStyle"); - return false; - } + String shortcutId = metadata.getShortcutId(); + String notificationShortcutId = r.getShortcutInfo() != null + ? r.getShortcutInfo().getId() + : null; + boolean shortcutValid = false; + if (notificationShortcutId != null && shortcutId != null) { + // NoMan already checks validity of shortcut, just check if they match. + shortcutValid = shortcutId.equals(notificationShortcutId); + } else if (shortcutId != null) { + shortcutValid = + mShortcutHelper.getValidShortcutInfo(shortcutId, pkg, r.getUser()) != null; + } + if (metadata.getIntent() == null && !shortcutValid) { + // Should have a shortcut if intent is null + logBubbleError(r.getKey(), + "couldn't find valid shortcut for bubble with shortcutId: " + shortcutId); + return false; + } + if (shortcutValid) { + // TODO: check the shortcut intent / ensure it can show in activity view return true; } + return canLaunchInActivityView(mContext, metadata.getIntent(), pkg); + } - /** - * @return whether the user has enabled the provided notification to bubble, and if the - * developer has provided valid information for the notification to bubble. - */ - @VisibleForTesting - boolean canBubble(NotificationRecord r, String pkg, int userId) { - Notification notification = r.getNotification(); - Notification.BubbleMetadata metadata = notification.getBubbleMetadata(); - if (metadata == null) { - // no log: no need to inform dev if they didn't attach bubble metadata - return false; - } - if (!mRankingConfig.bubblesEnabled()) { - logBubbleError(r.getKey(), "bubbles disabled for user: " + userId); - return false; - } - if (!mRankingConfig.areBubblesAllowed(pkg, userId)) { - logBubbleError(r.getKey(), - "bubbles for package: " + pkg + " disabled for user: " + userId); - return false; - } - if (!r.getChannel().canBubble()) { - logBubbleError(r.getKey(), - "bubbles for channel " + r.getChannel().getId() + " disabled"); - return false; - } - - String shortcutId = metadata.getShortcutId(); - String notificationShortcutId = r.getShortcutInfo() != null - ? r.getShortcutInfo().getId() - : null; - boolean shortcutValid = false; - if (notificationShortcutId != null && shortcutId != null) { - // NoMan already checks validity of shortcut, just check if they match. - shortcutValid = shortcutId.equals(notificationShortcutId); - } else if (shortcutId != null) { - shortcutValid = - mShortcutHelper.getValidShortcutInfo(shortcutId, pkg, r.getUser()) != null; - } - if (metadata.getIntent() == null && !shortcutValid) { - // Should have a shortcut if intent is null - logBubbleError(r.getKey(), - "couldn't find valid shortcut for bubble with shortcutId: " + shortcutId); - return false; - } - if (shortcutValid) { - return true; - } - // no log: canLaunch method has the failure log - return canLaunchInActivityView(mContext, metadata.getIntent(), pkg); + /** + * Whether an intent is properly configured to display in an {@link + * android.app.ActivityView} for bubbling. + * + * @param context the context to use. + * @param pendingIntent the pending intent of the bubble. + * @param packageName the notification package name for this bubble. + */ + // Keep checks in sync with BubbleController#canLaunchInActivityView. + @VisibleForTesting + protected boolean canLaunchInActivityView(Context context, PendingIntent pendingIntent, + String packageName) { + if (pendingIntent == null) { + Slog.w(TAG, "Unable to create bubble -- no intent"); + return false; } - /** - * Whether an intent is properly configured to display in an {@link - * android.app.ActivityView}. - * - * @param context the context to use. - * @param pendingIntent the pending intent of the bubble. - * @param packageName the notification package name for this bubble. - */ - // Keep checks in sync with BubbleController#canLaunchInActivityView. - @VisibleForTesting - protected boolean canLaunchInActivityView(Context context, PendingIntent pendingIntent, - String packageName) { - if (pendingIntent == null) { - Slog.w(TAG, "Unable to create bubble -- no intent"); - return false; - } - - Intent intent = pendingIntent.getIntent(); - - ActivityInfo info = intent != null - ? intent.resolveActivityInfo(context.getPackageManager(), 0) - : null; - if (info == null) { - FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, - packageName, - BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING); - Slog.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: " - + intent); - return false; - } - if (!ActivityInfo.isResizeableMode(info.resizeMode)) { - FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, - packageName, - BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE); - Slog.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: " - + intent); - return false; - } - return true; + Intent intent = pendingIntent.getIntent(); + ActivityInfo info = intent != null + ? intent.resolveActivityInfo(context.getPackageManager(), 0) + : null; + if (info == null) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, + packageName, + BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING); + Slog.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: " + + intent); + return false; } + if (!ActivityInfo.isResizeableMode(info.resizeMode)) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, + packageName, + BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE); + Slog.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: " + + intent); + return false; + } + return true; + } - private void logBubbleError(String key, String failureMessage) { - if (DBG) { - Slog.w(TAG, "Bubble notification: " + key + " failed: " + failureMessage); - } + private void logBubbleError(String key, String failureMessage) { + if (DBG) { + Slog.w(TAG, "Bubble notification: " + key + " failed: " + failureMessage); } } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index f9fc82bf05b13..2a81e37b8fa59 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -32,6 +32,7 @@ import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED; import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED; import static android.app.NotificationManager.ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; import static android.app.NotificationManager.EXTRA_AUTOMATIC_ZEN_RULE_ID; import static android.app.NotificationManager.EXTRA_AUTOMATIC_ZEN_RULE_STATUS; import static android.app.NotificationManager.IMPORTANCE_LOW; @@ -3079,13 +3080,17 @@ public class NotificationManagerService extends SystemService { return mPreferencesHelper.getImportance(pkg, uid) != IMPORTANCE_NONE; } + /** + * @return true if and only if "all" bubbles are allowed from the provided package. + */ @Override public boolean areBubblesAllowed(String pkg) { - return areBubblesAllowedForPackage(pkg, Binder.getCallingUid()); + return getBubblePreferenceForPackage(pkg, Binder.getCallingUid()) + == BUBBLE_PREFERENCE_ALL; } @Override - public boolean areBubblesAllowedForPackage(String pkg, int uid) { + public int getBubblePreferenceForPackage(String pkg, int uid) { enforceSystemOrSystemUIOrSamePackage(pkg, "Caller not system or systemui or same package"); @@ -3095,23 +3100,16 @@ public class NotificationManagerService extends SystemService { "canNotifyAsPackage for uid " + uid); } - return mPreferencesHelper.areBubblesAllowed(pkg, uid); + return mPreferencesHelper.getBubblePreference(pkg, uid); } @Override - public void setBubblesAllowed(String pkg, int uid, boolean allowed) { + public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { enforceSystemOrSystemUI("Caller not system or systemui"); - mPreferencesHelper.setBubblesAllowed(pkg, uid, allowed); + mPreferencesHelper.setBubblesAllowed(pkg, uid, bubblePreference); handleSavePolicyFile(); } - @Override - public boolean hasUserApprovedBubblesForPackage(String pkg, int uid) { - enforceSystemOrSystemUI("Caller not system or systemui"); - int lockedFields = mPreferencesHelper.getAppLockedFields(pkg, uid); - return (lockedFields & PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE) != 0; - } - @Override public boolean shouldHideSilentStatusIcons(String callingPkg) { checkCallerIsSameApp(callingPkg); @@ -7210,7 +7208,10 @@ public class NotificationManagerService extends SystemService { boolean interruptiveChanged = record.canBubble() && (interruptiveBefore != record.isInterruptive()); - changed = indexChanged || interceptChanged || visibilityChanged || interruptiveChanged; + changed = indexChanged + || interceptChanged + || visibilityChanged + || interruptiveChanged; if (interceptBefore && !record.isIntercepted() && record.isNewEnoughForAlerting(System.currentTimeMillis())) { buzzBeepBlinkLocked(record); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 8154988a4917a..b3d373ffab3ab 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -17,6 +17,7 @@ package com.android.server.notification; import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; @@ -117,10 +118,14 @@ public class PreferencesHelper implements RankingConfig { @VisibleForTesting static final boolean DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS = false; private static final boolean DEFAULT_SHOW_BADGE = true; - static final boolean DEFAULT_ALLOW_BUBBLE = true; + private static final boolean DEFAULT_OEM_LOCKED_IMPORTANCE = false; private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE = false; + static final boolean DEFAULT_GLOBAL_ALLOW_BUBBLE = true; + @VisibleForTesting + static final int DEFAULT_BUBBLE_PREFERENCE = BUBBLE_PREFERENCE_NONE; + /** * Default value for what fields are user locked. See {@link LockableAppFields} for all lockable * fields. @@ -148,7 +153,7 @@ public class PreferencesHelper implements RankingConfig { private final NotificationChannelLogger mNotificationChannelLogger; private SparseBooleanArray mBadgingEnabled; - private boolean mBubblesEnabled = DEFAULT_ALLOW_BUBBLE; + private boolean mBubblesEnabledGlobally = DEFAULT_GLOBAL_ALLOW_BUBBLE; private boolean mAreChannelsBypassingDnd; private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS; @@ -226,8 +231,8 @@ public class PreferencesHelper implements RankingConfig { parser, ATT_VISIBILITY, DEFAULT_VISIBILITY), XmlUtils.readBooleanAttribute( parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE), - XmlUtils.readBooleanAttribute( - parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE)); + XmlUtils.readIntAttribute( + parser, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE)); r.importance = XmlUtils.readIntAttribute( parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); r.priority = XmlUtils.readIntAttribute( @@ -339,19 +344,19 @@ public class PreferencesHelper implements RankingConfig { int uid) { return getOrCreatePackagePreferencesLocked(pkg, UserHandle.getUserId(uid), uid, DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE, - DEFAULT_ALLOW_BUBBLE); + DEFAULT_BUBBLE_PREFERENCE); } private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid) { return getOrCreatePackagePreferencesLocked(pkg, userId, uid, DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE, - DEFAULT_ALLOW_BUBBLE); + DEFAULT_BUBBLE_PREFERENCE); } private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid, int importance, int priority, int visibility, - boolean showBadge, boolean allowBubble) { + boolean showBadge, int bubblePreference) { final String key = packagePreferencesKey(pkg, uid); PackagePreferences r = (uid == UNKNOWN_UID) @@ -365,7 +370,7 @@ public class PreferencesHelper implements RankingConfig { r.priority = priority; r.visibility = visibility; r.showBadge = showBadge; - r.allowBubble = allowBubble; + r.bubblePreference = bubblePreference; try { createDefaultChannelIfNeededLocked(r); @@ -479,7 +484,7 @@ public class PreferencesHelper implements RankingConfig { || r.channels.size() > 0 || r.groups.size() > 0 || r.delegate != null - || r.allowBubble != DEFAULT_ALLOW_BUBBLE; + || r.bubblePreference != DEFAULT_BUBBLE_PREFERENCE; if (hasNonDefaultSettings) { out.startTag(null, TAG_PACKAGE); out.attribute(null, ATT_NAME, r.pkg); @@ -492,8 +497,8 @@ public class PreferencesHelper implements RankingConfig { if (r.visibility != DEFAULT_VISIBILITY) { out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); } - if (r.allowBubble != DEFAULT_ALLOW_BUBBLE) { - out.attribute(null, ATT_ALLOW_BUBBLE, Boolean.toString(r.allowBubble)); + if (r.bubblePreference != DEFAULT_BUBBLE_PREFERENCE) { + out.attribute(null, ATT_ALLOW_BUBBLE, Integer.toString(r.bubblePreference)); } out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge)); out.attribute(null, ATT_APP_USER_LOCKED_FIELDS, @@ -544,14 +549,14 @@ public class PreferencesHelper implements RankingConfig { * * @param pkg the package to allow or not allow bubbles for. * @param uid the uid to allow or not allow bubbles for. - * @param allowed whether bubbles are allowed. + * @param bubblePreference whether bubbles are allowed. */ - public void setBubblesAllowed(String pkg, int uid, boolean allowed) { + public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { boolean changed = false; synchronized (mPackagePreferences) { PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid); - changed = p.allowBubble != allowed; - p.allowBubble = allowed; + changed = p.bubblePreference != bubblePreference; + p.bubblePreference = bubblePreference; p.lockedAppFields = p.lockedAppFields | LockableAppFields.USER_LOCKED_BUBBLE; } if (changed) { @@ -567,9 +572,9 @@ public class PreferencesHelper implements RankingConfig { * @return whether bubbles are allowed. */ @Override - public boolean areBubblesAllowed(String pkg, int uid) { + public int getBubblePreference(String pkg, int uid) { synchronized (mPackagePreferences) { - return getOrCreatePackagePreferencesLocked(pkg, uid).allowBubble; + return getOrCreatePackagePreferencesLocked(pkg, uid).bubblePreference; } } @@ -788,6 +793,7 @@ public class PreferencesHelper implements RankingConfig { } if (fromTargetApp) { channel.setLockscreenVisibility(r.visibility); + channel.setAllowBubbles(existing != null && existing.canBubble()); } clearLockedFieldsLocked(channel); channel.setImportanceLockedByOEM(r.oemLockedImportance); @@ -2125,7 +2131,7 @@ public class PreferencesHelper implements RankingConfig { p.groups = new ArrayMap<>(); p.delegate = null; p.lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; - p.allowBubble = DEFAULT_ALLOW_BUBBLE; + p.bubblePreference = DEFAULT_BUBBLE_PREFERENCE; p.importance = DEFAULT_IMPORTANCE; p.priority = DEFAULT_PRIORITY; p.visibility = DEFAULT_VISIBILITY; @@ -2165,15 +2171,15 @@ public class PreferencesHelper implements RankingConfig { public void updateBubblesEnabled() { final boolean newValue = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.NOTIFICATION_BUBBLES, - DEFAULT_ALLOW_BUBBLE ? 1 : 0) == 1; - if (newValue != mBubblesEnabled) { - mBubblesEnabled = newValue; + DEFAULT_GLOBAL_ALLOW_BUBBLE ? 1 : 0) == 1; + if (newValue != mBubblesEnabledGlobally) { + mBubblesEnabledGlobally = newValue; updateConfig(); } } public boolean bubblesEnabled() { - return mBubblesEnabled; + return mBubblesEnabledGlobally; } public void updateBadgingEnabled() { @@ -2229,7 +2235,7 @@ public class PreferencesHelper implements RankingConfig { int priority = DEFAULT_PRIORITY; int visibility = DEFAULT_VISIBILITY; boolean showBadge = DEFAULT_SHOW_BADGE; - boolean allowBubble = DEFAULT_ALLOW_BUBBLE; + int bubblePreference = DEFAULT_BUBBLE_PREFERENCE; int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; // these fields are loaded on boot from a different source of truth and so are not // written to notification policy xml diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java index 7e98be7fe065a..7fc79e6a9bf75 100644 --- a/services/core/java/com/android/server/notification/RankingConfig.java +++ b/services/core/java/com/android/server/notification/RankingConfig.java @@ -29,7 +29,7 @@ public interface RankingConfig { void setShowBadge(String packageName, int uid, boolean showBadge); boolean canShowBadge(String packageName, int uid); boolean badgingEnabled(UserHandle userHandle); - boolean areBubblesAllowed(String packageName, int uid); + int getBubblePreference(String packageName, int uid); boolean bubblesEnabled(); boolean isGroupBlocked(String packageName, int uid, String groupId); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BubbleCheckerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BubbleCheckerTest.java deleted file mode 100644 index 2578ca8925204..0000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/BubbleCheckerTest.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2020 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.server.notification; - -import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; -import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - -import static junit.framework.Assert.assertTrue; - -import static org.junit.Assert.assertFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ShortcutInfo; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; -import android.test.suitebuilder.annotation.SmallTest; - -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.UiServiceTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class BubbleCheckerTest extends UiServiceTestCase { - - private static final String SHORTCUT_ID = "shortcut"; - private static final String PKG = "pkg"; - private static final String KEY = "key"; - private static final int USER_ID = 1; - - @Mock - ActivityManager mActivityManager; - @Mock - RankingConfig mRankingConfig; - @Mock - ShortcutHelper mShortcutHelper; - - @Mock - NotificationRecord mNr; - @Mock - UserHandle mUserHandle; - @Mock - Notification mNotif; - @Mock - StatusBarNotification mSbn; - @Mock - NotificationChannel mChannel; - @Mock - Notification.BubbleMetadata mBubbleMetadata; - @Mock - PendingIntent mPendingIntent; - @Mock - Intent mIntent; - - BubbleExtractor.BubbleChecker mBubbleChecker; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - when(mNr.getKey()).thenReturn(KEY); - when(mNr.getSbn()).thenReturn(mSbn); - when(mNr.getUser()).thenReturn(mUserHandle); - when(mUserHandle.getIdentifier()).thenReturn(USER_ID); - when(mNr.getChannel()).thenReturn(mChannel); - when(mSbn.getPackageName()).thenReturn(PKG); - when(mSbn.getUser()).thenReturn(mUserHandle); - when(mNr.getNotification()).thenReturn(mNotif); - when(mNotif.getBubbleMetadata()).thenReturn(mBubbleMetadata); - - mBubbleChecker = new BubbleExtractor.BubbleChecker(mContext, - mShortcutHelper, - mRankingConfig, - mActivityManager); - } - - void setUpIntentBubble() { - when(mPendingIntent.getIntent()).thenReturn(mIntent); - when(mBubbleMetadata.getIntent()).thenReturn(mPendingIntent); - when(mBubbleMetadata.getShortcutId()).thenReturn(null); - } - - void setUpShortcutBubble(boolean isValid) { - when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID); - ShortcutInfo info = mock(ShortcutInfo.class); - when(info.getId()).thenReturn(SHORTCUT_ID); - when(mShortcutHelper.getValidShortcutInfo(SHORTCUT_ID, PKG, mUserHandle)) - .thenReturn(isValid ? info : null); - when(mBubbleMetadata.getIntent()).thenReturn(null); - } - - void setUpBubblesEnabled(boolean feature, boolean app, boolean channel) { - when(mRankingConfig.bubblesEnabled()).thenReturn(feature); - when(mRankingConfig.areBubblesAllowed(PKG, USER_ID)).thenReturn(app); - when(mChannel.canBubble()).thenReturn(channel); - } - - void setUpActivityIntent(boolean isResizable) { - when(mPendingIntent.getIntent()).thenReturn(mIntent); - ActivityInfo info = new ActivityInfo(); - info.resizeMode = isResizable - ? RESIZE_MODE_RESIZEABLE - : RESIZE_MODE_UNRESIZEABLE; - when(mIntent.resolveActivityInfo(any(), anyInt())).thenReturn(info); - } - - // - // canBubble - // - - @Test - public void testCanBubble_true_intentBubble() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - setUpIntentBubble(); - setUpActivityIntent(true /* isResizable */); - when(mActivityManager.isLowRamDevice()).thenReturn(false); - assertTrue(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_true_shortcutBubble() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - setUpShortcutBubble(true /* isValid */); - assertTrue(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_false_noIntentInvalidShortcut() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - setUpShortcutBubble(false /* isValid */); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_false_noIntentNoShortcut() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - when(mBubbleMetadata.getIntent()).thenReturn(null); - when(mBubbleMetadata.getShortcutId()).thenReturn(null); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubbble_false_noMetadata() { - setUpBubblesEnabled(true/* feature */, true /* app */, true /* channel */); - when(mNotif.getBubbleMetadata()).thenReturn(null); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_false_bubblesNotEnabled() { - setUpBubblesEnabled(false /* feature */, true /* app */, true /* channel */); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_false_packageNotAllowed() { - setUpBubblesEnabled(true /* feature */, false /* app */, true /* channel */); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - @Test - public void testCanBubble_false_channelNotAllowed() { - setUpBubblesEnabled(true /* feature */, true /* app */, false /* channel */); - assertFalse(mBubbleChecker.canBubble(mNr, PKG, USER_ID)); - } - - // - // canLaunchInActivityView - // - - @Test - public void testCanLaunchInActivityView_true() { - setUpActivityIntent(true /* resizable */); - assertTrue(mBubbleChecker.canLaunchInActivityView(mContext, mPendingIntent, PKG)); - } - - @Test - public void testCanLaunchInActivityView_false_noIntent() { - when(mPendingIntent.getIntent()).thenReturn(null); - assertFalse(mBubbleChecker.canLaunchInActivityView(mContext, mPendingIntent, PKG)); - } - - @Test - public void testCanLaunchInActivityView_false_noInfo() { - when(mPendingIntent.getIntent()).thenReturn(mIntent); - when(mIntent.resolveActivityInfo(any(), anyInt())).thenReturn(null); - assertFalse(mBubbleChecker.canLaunchInActivityView(mContext, mPendingIntent, PKG)); - } - - @Test - public void testCanLaunchInActivityView_false_notResizable() { - setUpActivityIntent(false /* resizable */); - assertFalse(mBubbleChecker.canLaunchInActivityView(mContext, mPendingIntent, PKG)); - } - - // - // isNotificationAppropriateToBubble - // - - @Test - public void testIsNotifAppropriateToBubble_true() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - setUpIntentBubble(); - when(mActivityManager.isLowRamDevice()).thenReturn(false); - setUpActivityIntent(true /* resizable */); - doReturn(Notification.MessagingStyle.class).when(mNotif).getNotificationStyle(); - - assertTrue(mBubbleChecker.isNotificationAppropriateToBubble(mNr)); - } - - @Test - public void testIsNotifAppropriateToBubble_false_lowRam() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - when(mActivityManager.isLowRamDevice()).thenReturn(true); - setUpActivityIntent(true /* resizable */); - doReturn(Notification.MessagingStyle.class).when(mNotif).getNotificationStyle(); - - assertFalse(mBubbleChecker.isNotificationAppropriateToBubble(mNr)); - } - - @Test - public void testIsNotifAppropriateToBubble_false_notMessageStyle() { - setUpBubblesEnabled(true /* feature */, true /* app */, true /* channel */); - when(mActivityManager.isLowRamDevice()).thenReturn(false); - setUpActivityIntent(true /* resizable */); - doReturn(Notification.BigPictureStyle.class).when(mNotif).getNotificationStyle(); - - assertFalse(mBubbleChecker.isNotificationAppropriateToBubble(mNr)); - } - -} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java index 0dbbbaa9cdd69..3c376c9972ac3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BubbleExtractorTest.java @@ -15,12 +15,19 @@ */ package com.android.server.notification; -import static android.app.NotificationManager.IMPORTANCE_HIGH; -import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; +import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -28,6 +35,12 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.Notification.Builder; import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.app.Person; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ShortcutInfo; +import android.os.SystemClock; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.test.suitebuilder.annotation.SmallTest; @@ -46,16 +59,32 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class BubbleExtractorTest extends UiServiceTestCase { - @Mock RankingConfig mConfig; - @Mock BubbleExtractor.BubbleChecker mBubbleChecker; + private static final String SHORTCUT_ID = "shortcut"; + private static final String PKG = "com.android.server.notification"; + private static final String TAG = null; + private static final int ID = 1001; + private static final int UID = 1000; + private static final int PID = 2000; + UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); + BubbleExtractor mBubbleExtractor; - private String mPkg = "com.android.server.notification"; - private int mId = 1001; - private String mTag = null; - private int mUid = 1000; - private int mPid = 2000; - private UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); + @Mock + RankingConfig mConfig; + @Mock + NotificationChannel mChannel; + @Mock + Notification.BubbleMetadata mBubbleMetadata; + @Mock + PendingIntent mPendingIntent; + @Mock + Intent mIntent; + @Mock + ShortcutInfo mShortcutInfo; + @Mock + ShortcutHelper mShortcutHelper; + @Mock + ActivityManager mActivityManager; @Before public void setUp() { @@ -63,58 +92,103 @@ public class BubbleExtractorTest extends UiServiceTestCase { mBubbleExtractor = new BubbleExtractor(); mBubbleExtractor.initialize(mContext, mock(NotificationUsageStats.class)); mBubbleExtractor.setConfig(mConfig); - mBubbleExtractor.setShortcutHelper(mock(ShortcutHelper.class)); + mBubbleExtractor.setShortcutHelper(mShortcutHelper); + mBubbleExtractor.setActivityManager(mActivityManager); + + when(mConfig.getNotificationChannel(PKG, UID, "a", false)).thenReturn(mChannel); + when(mShortcutInfo.getId()).thenReturn(SHORTCUT_ID); } - private NotificationRecord getNotificationRecord(boolean allow, int importanceHigh) { - NotificationChannel channel = new NotificationChannel("a", "a", importanceHigh); - channel.setAllowBubbles(allow); - when(mConfig.getNotificationChannel(mPkg, mUid, "a", false)).thenReturn(channel); - + /* NotificationRecord that fulfills conversation requirements (message style + shortcut) */ + private NotificationRecord getNotificationRecord(boolean addBubble) { final Builder builder = new Builder(getContext()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) .setPriority(Notification.PRIORITY_HIGH) .setDefaults(Notification.DEFAULT_SOUND); - + Person person = new Person.Builder() + .setName("bubblebot") + .build(); + builder.setShortcutId(SHORTCUT_ID); + builder.setStyle(new Notification.MessagingStyle(person) + .setConversationTitle("Bubble Chat") + .addMessage("Hello?", + SystemClock.currentThreadTimeMillis() - 300000, person) + .addMessage("Is it me you're looking for?", + SystemClock.currentThreadTimeMillis(), person)); Notification n = builder.build(); - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, mId, mTag, mUid, - mPid, n, mUser, null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, channel); + if (addBubble) { + n.setBubbleMetadata(mBubbleMetadata); + } + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, ID, TAG, UID, + PID, n, mUser, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(getContext(), sbn, mChannel); + r.setShortcutInfo(mShortcutInfo); return r; } + void setUpIntentBubble(boolean isValid) { + when(mPendingIntent.getIntent()).thenReturn(mIntent); + when(mBubbleMetadata.getIntent()).thenReturn(mPendingIntent); + when(mBubbleMetadata.getShortcutId()).thenReturn(null); + + when(mPendingIntent.getIntent()).thenReturn(mIntent); + ActivityInfo info = new ActivityInfo(); + info.resizeMode = isValid + ? RESIZE_MODE_RESIZEABLE + : RESIZE_MODE_UNRESIZEABLE; + when(mIntent.resolveActivityInfo(any(), anyInt())).thenReturn(info); + } + + void setUpShortcutBubble(boolean isValid) { + when(mBubbleMetadata.getShortcutId()).thenReturn(SHORTCUT_ID); + when(mBubbleMetadata.getIntent()).thenReturn(null); + ShortcutInfo answer = isValid ? mShortcutInfo : null; + when(mShortcutHelper.getValidShortcutInfo(SHORTCUT_ID, PKG, mUser)).thenReturn(answer); + } + + void setUpBubblesEnabled(boolean feature, int app, boolean channel) { + when(mConfig.bubblesEnabled()).thenReturn(feature); + when(mConfig.getBubblePreference(anyString(), anyInt())).thenReturn(app); + when(mChannel.canBubble()).thenReturn(channel); + } + // - // Tests + // Tests for the record being allowed to bubble. // @Test public void testAppYesChannelNo() { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); - NotificationRecord r = getNotificationRecord(false, IMPORTANCE_UNSPECIFIED); - + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + false /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); + when(mChannel.getUserLockedFields()).thenReturn(USER_LOCKED_ALLOW_BUBBLE); mBubbleExtractor.process(r); assertFalse(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); } @Test public void testAppNoChannelYes() throws Exception { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(false); - NotificationRecord r = getNotificationRecord(true, IMPORTANCE_HIGH); + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_NONE /* app */, + true /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); assertFalse(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); } @Test public void testAppYesChannelYes() { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); - NotificationRecord r = getNotificationRecord(true, IMPORTANCE_UNSPECIFIED); + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); @@ -123,34 +197,85 @@ public class BubbleExtractorTest extends UiServiceTestCase { @Test public void testAppNoChannelNo() { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(false); - NotificationRecord r = getNotificationRecord(false, IMPORTANCE_UNSPECIFIED); + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_NONE /* app */, + false /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); assertFalse(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); } @Test public void testAppYesChannelYesUserNo() { - when(mConfig.bubblesEnabled()).thenReturn(false); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); - NotificationRecord r = getNotificationRecord(true, IMPORTANCE_HIGH); + setUpBubblesEnabled(false /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); assertFalse(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); } @Test - public void testFlagBubble_true() { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); - NotificationRecord r = getNotificationRecord(true, IMPORTANCE_UNSPECIFIED); + public void testAppSelectedChannelNo() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_SELECTED /* app */, + false /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); - mBubbleExtractor.setBubbleChecker(mBubbleChecker); - when(mBubbleChecker.isNotificationAppropriateToBubble(r)).thenReturn(true); + mBubbleExtractor.process(r); + + assertFalse(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testAppSeletedChannelYes() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_SELECTED /* app */, + true /* channel */); + NotificationRecord r = getNotificationRecord(true /* bubble */); + when(mChannel.getUserLockedFields()).thenReturn(USER_LOCKED_ALLOW_BUBBLE); + + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + } + + // + // Tests for flagging it as a bubble. + // + + @Test + public void testFlagBubble_false_previouslyRemoved() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + r.setFlagBubbleRemoved(true); + + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_true_shortcutBubble() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + setUpShortcutBubble(true /* isValid */); + + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); assertTrue(r.canBubble()); @@ -158,14 +283,142 @@ public class BubbleExtractorTest extends UiServiceTestCase { } @Test - public void testFlagBubble_noFlag_previouslyRemoved() { - when(mConfig.bubblesEnabled()).thenReturn(true); - when(mConfig.areBubblesAllowed(mPkg, mUid)).thenReturn(true); - NotificationRecord r = getNotificationRecord(true, IMPORTANCE_UNSPECIFIED); - r.setFlagBubbleRemoved(true); + public void testFlagBubble_true_intentBubble() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + setUpIntentBubble(true /* isValid */); - mBubbleExtractor.setBubbleChecker(mBubbleChecker); - when(mBubbleChecker.isNotificationAppropriateToBubble(r)).thenReturn(true); + NotificationRecord r = getNotificationRecord(true /* bubble */); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertTrue(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_noIntentInvalidShortcut() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + setUpShortcutBubble(false /* isValid */); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + r.setShortcutInfo(null); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_invalidIntentNoShortcut() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + setUpIntentBubble(false /* isValid */); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + r.setShortcutInfo(null); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_noIntentNoShortcut() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + + // Shortcut here is for the notification not the bubble + NotificationRecord r = getNotificationRecord(true /* bubble */); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_noMetadata() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + + NotificationRecord r = getNotificationRecord(false /* bubble */); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_notConversation() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(false); + setUpIntentBubble(true /* isValid */); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + // No longer a conversation: + r.setShortcutInfo(null); + r.getNotification().extras.putString(Notification.EXTRA_TEMPLATE, null); + + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_lowRamDevice() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(true); + setUpIntentBubble(true /* isValid */); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_noIntent() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(true); + setUpIntentBubble(true /* isValid */); + when(mPendingIntent.getIntent()).thenReturn(null); + + NotificationRecord r = getNotificationRecord(true /* bubble */); + mBubbleExtractor.process(r); + + assertTrue(r.canBubble()); + assertFalse(r.getNotification().isBubbleNotification()); + } + + @Test + public void testFlagBubble_false_noActivityInfo() { + setUpBubblesEnabled(true /* feature */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + when(mActivityManager.isLowRamDevice()).thenReturn(true); + setUpIntentBubble(true /* isValid */); + when(mPendingIntent.getIntent()).thenReturn(mIntent); + when(mIntent.resolveActivityInfo(any(), anyInt())).thenReturn(null); + + NotificationRecord r = getNotificationRecord(true /* bubble */); mBubbleExtractor.process(r); assertTrue(r.canBubble()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 15220e1ff54ae..e6e84eeab4ed2 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -21,6 +21,10 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIB import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; +import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.app.NotificationManager.EXTRA_BLOCKED_STATE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; @@ -39,6 +43,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.PackageManager.FEATURE_WATCH; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -101,6 +106,7 @@ import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.LauncherApps; @@ -204,7 +210,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private TestableNotificationManagerService mService; private INotificationManager mBinderService; private NotificationManagerInternal mInternalService; - private TestableBubbleChecker mTestableBubbleChecker; private ShortcutHelper mShortcutHelper; @Mock private IPackageManager mPackageManager; @@ -347,21 +352,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } } - private class TestableBubbleChecker extends BubbleExtractor.BubbleChecker { - - TestableBubbleChecker(Context context, ShortcutHelper helper, RankingConfig config, - ActivityManager manager) { - super(context, helper, config, manager); - } - - @Override - protected boolean canLaunchInActivityView(Context context, PendingIntent pendingIntent, - String packageName) { - // Tests for this not being true are in CTS NotificationManagerTest - return true; - } - } - private class TestableToastCallback extends ITransientNotification.Stub { @Override public void show(IBinder windowToken) { @@ -480,9 +470,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Set the testable bubble extractor RankingHelper rankingHelper = mService.getRankingHelper(); BubbleExtractor extractor = rankingHelper.findExtractor(BubbleExtractor.class); - mTestableBubbleChecker = new TestableBubbleChecker(mContext, mShortcutHelper, - mService.mPreferencesHelper, mActivityManager); - extractor.setBubbleChecker(mTestableBubbleChecker); + extractor.setActivityManager(mActivityManager); // Tests call directly into the Binder. mBinderService = mService.getBinderService(); @@ -544,13 +532,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } private void setUpPrefsForBubbles(String pkg, int uid, boolean globalEnabled, - boolean pkgEnabled, boolean channelEnabled) { + int pkgPref, boolean channelEnabled) { Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.NOTIFICATION_BUBBLES, globalEnabled ? 1 : 0); mService.mPreferencesHelper.updateBubblesEnabled(); assertEquals(globalEnabled, mService.mPreferencesHelper.bubblesEnabled()); try { - mBinderService.setBubblesAllowed(pkg, uid, pkgEnabled); + mBinderService.setBubblesAllowed(pkg, uid, pkgPref); } catch (RemoteException e) { e.printStackTrace(); } @@ -687,19 +675,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { false); } - private Notification.BubbleMetadata.Builder getBubbleMetadataBuilder() { - PendingIntent pi = PendingIntent.getActivity(mContext, 0, new Intent(), 0); - return new Notification.BubbleMetadata.Builder(pi, - Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)); - } - private Notification.Builder getMessageStyleNotifBuilder(boolean addBubbleMetadata, String groupKey, boolean isSummary) { // Give it a person Person person = new Person.Builder() .setName("bubblebot") .build(); - // It needs remote input to be bubble-able RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); @@ -724,11 +705,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { nb.setGroup(groupKey); } if (addBubbleMetadata) { - nb.setBubbleMetadata(getBubbleMetadataBuilder().build()); + nb.setBubbleMetadata(getBubbleMetadata()); } return nb; } + private Notification.BubbleMetadata getBubbleMetadata() { + PendingIntent pendingIntent = mock(PendingIntent.class); + Intent intent = mock(Intent.class); + when(pendingIntent.getIntent()).thenReturn(intent); + + ActivityInfo info = new ActivityInfo(); + info.resizeMode = RESIZE_MODE_RESIZEABLE; + when(intent.resolveActivityInfo(any(), anyInt())).thenReturn(info); + + return new Notification.BubbleMetadata.Builder( + pendingIntent, + Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)) + .build(); + } + private NotificationRecord addGroupWithBubblesAndValidateAdded(boolean summaryAutoCancel) throws RemoteException { @@ -4483,24 +4479,31 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testBubble() throws Exception { - mBinderService.setBubblesAllowed(PKG, mUid, false); - assertFalse(mBinderService.areBubblesAllowedForPackage(PKG, mUid)); + mBinderService.setBubblesAllowed(PKG, mUid, BUBBLE_PREFERENCE_NONE); + assertFalse(mBinderService.areBubblesAllowed(PKG)); + assertEquals(mBinderService.getBubblePreferenceForPackage(PKG, mUid), + BUBBLE_PREFERENCE_NONE); } @Test - public void testUserApprovedBubblesForPackage() throws Exception { - assertFalse(mBinderService.hasUserApprovedBubblesForPackage(PKG, mUid)); - mBinderService.setBubblesAllowed(PKG, mUid, true); - assertTrue(mBinderService.hasUserApprovedBubblesForPackage(PKG, mUid)); - assertTrue(mBinderService.areBubblesAllowedForPackage(PKG, mUid)); + public void testUserApprovedBubblesForPackageSelected() throws Exception { + mBinderService.setBubblesAllowed(PKG, mUid, BUBBLE_PREFERENCE_SELECTED); + assertEquals(mBinderService.getBubblePreferenceForPackage(PKG, mUid), + BUBBLE_PREFERENCE_SELECTED); + } + + @Test + public void testUserApprovedBubblesForPackageAll() throws Exception { + mBinderService.setBubblesAllowed(PKG, mUid, BUBBLE_PREFERENCE_ALL); + assertTrue(mBinderService.areBubblesAllowed(PKG)); + assertEquals(mBinderService.getBubblePreferenceForPackage(PKG, mUid), + BUBBLE_PREFERENCE_ALL); } @Test public void testUserRejectsBubblesForPackage() throws Exception { - assertFalse(mBinderService.hasUserApprovedBubblesForPackage(PKG, mUid)); - mBinderService.setBubblesAllowed(PKG, mUid, false); - assertTrue(mBinderService.hasUserApprovedBubblesForPackage(PKG, mUid)); - assertFalse(mBinderService.areBubblesAllowedForPackage(PKG, mUid)); + mBinderService.setBubblesAllowed(PKG, mUid, BUBBLE_PREFERENCE_NONE); + assertFalse(mBinderService.areBubblesAllowed(PKG)); } @Test @@ -5166,8 +5169,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubble() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubble"); @@ -5185,8 +5190,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubble_noFlag_appNotAllowed() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, false /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_NONE /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubble_noFlag_appNotAllowed"); @@ -5204,15 +5211,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubbleNotifs_noFlag_whenAppForeground() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Notif with bubble metadata but not our other misc requirements Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setBubbleMetadata(getBubbleMetadataBuilder().build()); + .setBubbleMetadata(getBubbleMetadata()); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); @@ -5232,8 +5241,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubbleNotifs_flag_messaging() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubbleNotifs_flag_messaging"); @@ -5249,8 +5260,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubbleNotifs_noFlag_messaging_appNotAllowed() throws RemoteException { - // Bubbles are NOT allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, false /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_NONE /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubbleNotifs_noFlag_messaging_appNotAllowed"); @@ -5267,8 +5280,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubbleNotifs_noFlag_notBubble() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Messaging notif WITHOUT bubble metadata Notification.Builder nb = getMessageStyleNotifBuilder(false /* addBubbleMetadata */, @@ -5291,11 +5306,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testFlagBubbleNotifs_noFlag_messaging_channelNotAllowed() throws RemoteException { - // Bubbles are allowed except on this channel - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, false /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + false /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testFlagBubbleNotifs_noFlag_messaging_channelNotAllowed"); + nr.getChannel().lockFields(USER_LOCKED_ALLOW_BUBBLE); // Post the notification mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), @@ -5488,7 +5506,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testAreBubblesAllowedForPackage_crossUser() throws Exception { try { - mBinderService.areBubblesAllowedForPackage(mContext.getPackageName(), + mBinderService.getBubblePreferenceForPackage(mContext.getPackageName(), mUid + UserHandle.PER_USER_RANGE); fail("Cannot call cross user without permission"); } catch (SecurityException e) { @@ -5497,7 +5515,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // cross user, with permission, no problem enableInteractAcrossUsers(); - mBinderService.areBubblesAllowedForPackage(mContext.getPackageName(), + mBinderService.getBubblePreferenceForPackage(mContext.getPackageName(), mUid + UserHandle.PER_USER_RANGE); } @@ -5508,8 +5526,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbleChanged_false() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Notif with bubble metadata NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, @@ -5539,8 +5559,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbleChanged_true() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Notif that is not a bubble NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, @@ -5576,8 +5598,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbleChanged_true_notAllowed() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Notif that is not a bubble NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); @@ -5605,8 +5629,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbleIsFlagRemoved_resetOnUpdate() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + // Notif with bubble metadata NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testNotificationBubbleIsFlagRemoved_resetOnUpdate"); @@ -5637,8 +5664,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbleIsFlagRemoved_resetOnBubbleChangedTrue() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + // Notif with bubble metadata NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testNotificationBubbleIsFlagRemoved_trueOnBubbleChangedTrue"); @@ -5666,16 +5696,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testOnBubbleNotificationSuppressionChanged() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // Bubble notification NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag"); - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, nr.getSbn().getUserId(), true /* global */, - true /* app */, true /* channel */); - mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); waitForIdle(); @@ -5888,8 +5916,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_disabled_lowRamDevice() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); // And we are low ram when(mActivityManager.isLowRamDevice()).thenReturn(true); @@ -5972,8 +6002,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_flagAutoExpandForeground_fails_notForeground() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testNotificationBubbles_flagAutoExpandForeground_fails_notForeground"); @@ -6002,8 +6034,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_flagAutoExpandForeground_succeeds_foreground() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testNotificationBubbles_flagAutoExpandForeground_succeeds_foreground"); @@ -6032,8 +6066,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_flagRemoved_whenShortcutRemoved() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); ArgumentCaptor launcherAppsCallback = ArgumentCaptor.forClass(LauncherApps.Callback.class); @@ -6090,8 +6126,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_shortcut_stopListeningWhenNotifRemoved() throws RemoteException { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); ArgumentCaptor launcherAppsCallback = ArgumentCaptor.forClass(LauncherApps.Callback.class); @@ -6141,8 +6179,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); @@ -6165,8 +6205,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked() throws Exception { - // Bubbles are allowed! - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded( true /* summaryAutoCancel */); @@ -6197,8 +6239,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_bubbleStays_whenClicked() throws Exception { + setUpPrefsForBubbles(PKG, mUid, + true /* global */, + BUBBLE_PREFERENCE_ALL /* app */, + true /* channel */); + // GIVEN a notification that has the auto cancels flag (cancel on click) and is a bubble - setUpPrefsForBubbles(PKG, mUid, true /* global */, true /* app */, true /* channel */); final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel); nr.getSbn().getNotification().flags |= FLAG_BUBBLE | FLAG_AUTO_CANCEL; mService.addNotification(nr); @@ -6332,7 +6378,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertEquals("friend", friendChannel.getConversationId()); assertEquals(null, original.getConversationId()); assertEquals(original.canShowBadge(), friendChannel.canShowBadge()); - assertEquals(original.canBubble(), friendChannel.canBubble()); + assertFalse(friendChannel.canBubble()); // can't be modified by app assertFalse(original.getId().equals(friendChannel.getId())); assertNotNull(friendChannel.getId()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index ed5ec6ac785b3..427237c4be0fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -16,6 +16,9 @@ package com.android.server.notification; import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; @@ -23,6 +26,7 @@ import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; +import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT; import static com.google.common.truth.Truth.assertThat; @@ -1094,7 +1098,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { .getUserLockedFields()); final NotificationChannel update = getChannel(); - update.setAllowBubbles(false); + update.setAllowBubbles(true); mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true); assertEquals(NotificationChannel.USER_LOCKED_ALLOW_BUBBLE, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update.getId(), false) @@ -1734,14 +1738,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.updateDefaultApps(UserHandle.getUserId(UID_O), null, pkgPair); mHelper.setNotificationDelegate(PKG_O, UID_O, "", 1); mHelper.setImportance(PKG_O, UID_O, IMPORTANCE_NONE); - mHelper.setBubblesAllowed(PKG_O, UID_O, false); + mHelper.setBubblesAllowed(PKG_O, UID_O, DEFAULT_BUBBLE_PREFERENCE); mHelper.setShowBadge(PKG_O, UID_O, false); mHelper.setAppImportanceLocked(PKG_O, UID_O); mHelper.clearData(PKG_O, UID_O); assertEquals(IMPORTANCE_UNSPECIFIED, mHelper.getImportance(PKG_O, UID_O)); - assertTrue(mHelper.areBubblesAllowed(PKG_O, UID_O)); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), DEFAULT_BUBBLE_PREFERENCE); assertTrue(mHelper.canShowBadge(PKG_O, UID_O)); assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O)); @@ -2412,21 +2416,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testAllowBubbles_defaults() throws Exception { - assertTrue(mHelper.areBubblesAllowed(PKG_O, UID_O)); + public void testBubblePreference_defaults() throws Exception { + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL); mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger); loadStreamXml(baos, false, UserHandle.USER_ALL); - assertTrue(mHelper.areBubblesAllowed(PKG_O, UID_O)); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE); assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O)); } @Test - public void testAllowBubbles_xml() throws Exception { - mHelper.setBubblesAllowed(PKG_O, UID_O, false); - assertFalse(mHelper.areBubblesAllowed(PKG_O, UID_O)); + public void testBubblePreference_xml() throws Exception { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE); assertEquals(PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE, mHelper.getAppLockedFields(PKG_O, UID_O)); @@ -2434,7 +2438,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mLogger); loadStreamXml(baos, false, UserHandle.USER_ALL); - assertFalse(mHelper.areBubblesAllowed(PKG_O, UID_O)); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE); assertEquals(PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE, mHelper.getAppLockedFields(PKG_O, UID_O)); } @@ -2766,9 +2770,29 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testSetBubblesAllowed_false() { - mHelper.setBubblesAllowed(PKG_O, UID_O, false); - assertFalse(mHelper.areBubblesAllowed(PKG_O, UID_O)); + public void testSetBubblesAllowed_none() { + // Change it to non-default first + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_ALL); + verify(mHandler, times(1)).requestSort(); + reset(mHandler); + // Now test + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_NONE); + verify(mHandler, times(1)).requestSort(); + } + + @Test + public void testSetBubblesAllowed_all() { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_ALL); + verify(mHandler, times(1)).requestSort(); + } + + @Test + public void testSetBubblesAllowed_selected() { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_SELECTED); + assertEquals(mHelper.getBubblePreference(PKG_O, UID_O), BUBBLE_PREFERENCE_SELECTED); verify(mHandler, times(1)).requestSort(); } From 9adfe6a20cebfe6aa6f414e61c3412775602b905 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Mon, 30 Mar 2020 17:23:26 -0700 Subject: [PATCH 2/3] Notification Bubble Button Button is visible on expanded notifications that are in the conversation section. Always visible if the notification has metadata & the user hasn't turned off bubbles for the device. Button is added to end of the template actions, making it visible if there were no other actions. Tapping it to make a button will change the app settings to "selected" and enable bubbles for that channel, if the bubble wasn't allowed otherwise. Tapping it to stop the bubble will disallow that channel from bubbling. Test: manual: - check bubble button on notif with long action text - check button is hidden when inline reply is focused - check no bubble button on non-conversation notifs - check create bubble from HUN - check create bubble from expanded notif - check that dismissing a bubble updates state of the button - check that getting new notif after dismissing bubble updates state of the button Bug: 138116133 Change-Id: Ic12204bd191530e94ea0400e16a9315a0d149252 --- .../internal/statusbar/IStatusBarService.aidl | 2 +- .../notification_material_action_list.xml | 36 ++++++-- core/res/res/values/symbols.xml | 1 + .../res/drawable/ic_create_bubble.xml | 20 ++--- .../SystemUI/res/drawable/ic_stop_bubble.xml | 25 ++++++ .../systemui/bubbles/BubbleController.java | 87 +++++++++++-------- .../systemui/bubbles/dagger/BubbleModule.java | 9 +- .../NotificationChannelHelper.java | 84 ++++++++++++++++++ .../row/ExpandableNotificationRow.java | 25 +++++- .../row/NotificationContentView.java | 71 +++++++++++++++ .../row/NotificationConversationInfo.java | 46 +--------- .../bubbles/BubbleControllerTest.java | 4 +- .../NewNotifPipelineBubbleControllerTest.java | 4 +- .../bubbles/TestableBubbleController.java | 7 +- .../notification/NotificationDelegate.java | 2 +- .../NotificationManagerService.java | 16 ++-- .../statusbar/StatusBarManagerService.java | 4 +- .../NotificationManagerServiceTest.java | 12 +-- 18 files changed, 329 insertions(+), 126 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_stop_bubble.xml create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 1dbd69c67831c..24fe0638b0910 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -77,7 +77,7 @@ interface IStatusBarService void onNotificationSmartReplySent(in String key, in int replyIndex, in CharSequence reply, in int notificationLocation, boolean modifiedBeforeSending); void onNotificationSettingsViewed(String key); - void onNotificationBubbleChanged(String key, boolean isBubble); + void onNotificationBubbleChanged(String key, boolean isBubble, int flags); void onBubbleNotificationSuppressionChanged(String key, boolean isSuppressed); void grantInlineReplyUriPermission(String key, in Uri uri, in UserHandle user, String packageName); void clearInlineReplyUriPermissions(String key); diff --git a/core/res/res/layout/notification_material_action_list.xml b/core/res/res/layout/notification_material_action_list.xml index 425801991927e..ec54091e5a20b 100644 --- a/core/res/res/layout/notification_material_action_list.xml +++ b/core/res/res/layout/notification_material_action_list.xml @@ -20,16 +20,34 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/notification_action_list_margin_top" android:layout_gravity="bottom"> - - - + + + + + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef1e8b74b05fb..69862b2e2820e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3495,6 +3495,7 @@ + diff --git a/packages/SystemUI/res/drawable/ic_create_bubble.xml b/packages/SystemUI/res/drawable/ic_create_bubble.xml index 1947f58f8f5e6..d58e9a347a2ff 100644 --- a/packages/SystemUI/res/drawable/ic_create_bubble.xml +++ b/packages/SystemUI/res/drawable/ic_create_bubble.xml @@ -14,16 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_stop_bubble.xml b/packages/SystemUI/res/drawable/ic_stop_bubble.xml new file mode 100644 index 0000000000000..11bc741a4f178 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_stop_bubble.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 9d885fd3c207e..878a88452f438 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -17,6 +17,8 @@ package com.android.systemui.bubbles; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; +import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; @@ -30,7 +32,6 @@ import static android.view.View.VISIBLE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -43,6 +44,9 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.UserIdInt; import android.app.ActivityManager.RunningTaskInfo; +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -83,6 +87,7 @@ import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; +import com.android.systemui.statusbar.notification.NotificationChannelHelper; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotifCollection; @@ -169,6 +174,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private final NotificationShadeWindowController mNotificationShadeWindowController; private final ZenModeController mZenModeController; private StatusBarStateListener mStatusBarStateListener; + private INotificationManager mINotificationManager; // Callback that updates BubbleOverflowActivity on data change. @Nullable private Runnable mOverflowCallback = null; @@ -293,11 +299,13 @@ public class BubbleController implements ConfigurationController.ConfigurationLi FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notificationManager) { this(context, notificationShadeWindowController, statusBarStateController, shadeController, data, null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, notifUserManager, groupManager, entryManager, - notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState); + notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState, + notificationManager); } /** @@ -319,7 +327,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notificationManager) { dumpManager.registerDumpable(TAG, this); mContext = context; mShadeController = shadeController; @@ -327,6 +336,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mNotifUserManager = notifUserManager; mZenModeController = zenModeController; mFloatingContentCoordinator = floatingContentCoordinator; + mINotificationManager = notificationManager; mZenModeController.addCallback(new ZenModeController.Callback() { @Override public void onZenChanged(int zen) { @@ -829,37 +839,43 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * This method will collapse the shade, create the bubble without a flyout or dot, and suppress * the notification from appearing in the shade. * - * @param entry the notification to show as a bubble. + * @param entry the notification to change bubble state for. + * @param shouldBubble whether the notification should show as a bubble or not. */ - public void onUserCreatedBubbleFromNotification(NotificationEntry entry) { - if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) { - Log.d(TAG, "onUserCreatedBubble: " + entry.getKey()); + public void onUserChangedBubble(NotificationEntry entry, boolean shouldBubble) { + NotificationChannel channel = entry.getChannel(); + final String appPkg = entry.getSbn().getPackageName(); + final int appUid = entry.getSbn().getUid(); + if (channel == null || appPkg == null) { + return; } - mShadeController.collapsePanel(true); - entry.setFlagBubble(true); - updateBubble(entry, true /* suppressFlyout */, false /* showInShade */); - mUserCreatedBubbles.add(entry.getKey()); - mUserBlockedBubbles.remove(entry.getKey()); - } - /** - * Called when a user has indicated that an active notification appearing as a bubble should - * no longer be shown as a bubble. - * - * @param entry the notification to no longer show as a bubble. - */ - public void onUserDemotedBubbleFromNotification(NotificationEntry entry) { - if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) { - Log.d(TAG, "onUserDemotedBubble: " + entry.getKey()); + // Update the state in NotificationManagerService + try { + int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; + mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); + } catch (RemoteException e) { } - entry.setFlagBubble(false); - removeBubble(entry, DISMISS_BLOCKED); - mUserCreatedBubbles.remove(entry.getKey()); - if (BubbleExperimentConfig.isPackageWhitelistedToAutoBubble( - mContext, entry.getSbn().getPackageName())) { - // This package is whitelist but user demoted the bubble, let's save it so we don't - // auto-bubble for the whitelist again. - mUserBlockedBubbles.add(entry.getKey()); + + // Change the settings + channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, + mINotificationManager, entry, channel); + channel.setAllowBubbles(shouldBubble); + try { + int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid); + if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { + mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); + } + mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); + } catch (RemoteException e) { + Log.e(TAG, e.getMessage()); + } + + if (shouldBubble) { + mShadeController.collapsePanel(true); + if (entry.getRow() != null) { + entry.getRow().updateBubbleButton(); + } } } @@ -1007,14 +1023,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } else { // Update the flag for SysUI bubble.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE; + if (bubble.getEntry().getRow() != null) { + bubble.getEntry().getRow().updateBubbleButton(); + } - // Make sure NoMan knows it's not a bubble anymore so anyone querying it - // will get right result back + // Update the state in NotificationManagerService try { mBarService.onNotificationBubbleChanged(bubble.getKey(), - false /* isBubble */); + false /* isBubble */, 0 /* flags */); } catch (RemoteException e) { - // Bad things have happened } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java index e84e932c9e61d..72d646e0554d5 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java @@ -16,6 +16,7 @@ package com.android.systemui.bubbles.dagger; +import android.app.INotificationManager; import android.content.Context; import com.android.systemui.bubbles.BubbleController; @@ -64,14 +65,15 @@ public interface BubbleModule { FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notifManager) { return new BubbleController( context, notificationShadeWindowController, statusBarStateController, shadeController, data, - /* synchronizer */null, + null /* synchronizer */, configurationController, interruptionStateProvider, zenModeController, @@ -82,6 +84,7 @@ public interface BubbleModule { featureFlags, dumpManager, floatingContentCoordinator, - sysUiState); + sysUiState, + notifManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java new file mode 100644 index 0000000000000..ff945d15a4ed6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationChannelHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 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.systemui.statusbar.notification; + +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.systemui.R; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * Helps SystemUI create notification channels. + */ +public class NotificationChannelHelper { + private static final String TAG = "NotificationChannelHelper"; + + /** Creates a conversation channel based on the shortcut info or notification title. */ + public static NotificationChannel createConversationChannelIfNeeded( + Context context, + INotificationManager notificationManager, + NotificationEntry entry, + NotificationChannel channel) { + if (!TextUtils.isEmpty(channel.getConversationId())) { + return channel; + } + final String conversationId = entry.getSbn().getShortcutId(context); + final String pkg = entry.getSbn().getPackageName(); + final int appUid = entry.getSbn().getUid(); + if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(pkg)) { + return channel; + } + + String name; + if (entry.getRanking().getShortcutInfo() != null) { + name = entry.getRanking().getShortcutInfo().getShortLabel().toString(); + } else { + Bundle extras = entry.getSbn().getNotification().extras; + String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE); + if (TextUtils.isEmpty(nameString)) { + nameString = extras.getString(Notification.EXTRA_TITLE); + } + name = nameString; + } + + // If this channel is not already a customized conversation channel, create + // a custom channel + try { + // TODO: When shortcuts are enforced remove this and use the shortcut label for naming + channel.setName(context.getString( + R.string.notification_summary_message_format, + name, channel.getName())); + notificationManager.createConversationNotificationChannelForPackage( + pkg, appUid, entry.getSbn().getKey(), channel, + conversationId); + channel = notificationManager.getConversationNotificationChannel( + context.getOpPackageName(), UserHandle.getUserId(appUid), pkg, + channel.getId(), false, conversationId); + } catch (RemoteException e) { + Slog.e(TAG, "Could not create conversation channel", e); + } + return channel; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 5c578dfc57441..ba060636500a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -70,6 +70,7 @@ import com.android.internal.widget.CachingIconView; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; +import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -579,6 +580,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** Call when bubble state has changed and the button on the notification should be updated. */ + public void updateBubbleButton() { + for (NotificationContentView l : mLayouts) { + l.updateBubbleButton(mEntry); + } + } + @VisibleForTesting void updateShelfIconColor() { StatusBarIconView expandedIcon = mEntry.getIcons().getShelfIcon(); @@ -1086,6 +1094,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateClickAndFocus(); } + /** The click listener for the bubble button. */ + public View.OnClickListener getBubbleClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + Dependency.get(BubbleController.class) + .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); + mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); + } + }; + } + private void updateClickAndFocus() { boolean normalChild = !isChildInGroup() || isGroupExpanded(); boolean clickable = mOnClickListener != null && normalChild; @@ -1267,7 +1287,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mNotificationColor; } - private void updateNotificationColor() { + public void updateNotificationColor() { Configuration currentConfig = getResources().getConfiguration(); boolean nightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; @@ -1613,6 +1633,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mFalsingManager = falsingManager; mStatusbarStateController = statusBarStateController; mPeopleNotificationIdentifier = peopleNotificationIdentifier; + for (NotificationContentView l : mLayouts) { + l.setPeopleNotificationIdentifier(mPeopleNotificationIdentifier); + } } private void initDimens() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 3c3f1b21fb3c0..bd1745eaa0284 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.notification.row; + +import static android.provider.Settings.Global.NOTIFICATION_BUBBLES; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.util.ArraySet; @@ -47,6 +52,7 @@ import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -118,6 +124,7 @@ public class NotificationContentView extends FrameLayout { private NotificationGroupManager mGroupManager; private RemoteInputController mRemoteInputController; private Runnable mExpandedVisibleListener; + private PeopleNotificationIdentifier mPeopleIdentifier; /** * List of listeners for when content views become inactive (i.e. not the showing view). */ @@ -454,6 +461,9 @@ public class NotificationContentView extends FrameLayout { mExpandedChild = child; mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); + if (mContainingNotification != null) { + applyBubbleAction(mExpandedChild, mContainingNotification.getEntry()); + } } /** @@ -493,6 +503,9 @@ public class NotificationContentView extends FrameLayout { mHeadsUpChild = child; mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); + if (mContainingNotification != null) { + applyBubbleAction(mHeadsUpChild, mContainingNotification.getEntry()); + } } @Override @@ -1138,6 +1151,8 @@ public class NotificationContentView extends FrameLayout { mForceSelectNextLayout = true; mPreviousExpandedRemoteInputIntent = null; mPreviousHeadsUpRemoteInputIntent = null; + applyBubbleAction(mExpandedChild, entry); + applyBubbleAction(mHeadsUpChild, entry); } private void updateAllSingleLineViews() { @@ -1308,6 +1323,58 @@ public class NotificationContentView extends FrameLayout { return null; } + /** + * Call to update state of the bubble button (i.e. does it show bubble or unbubble or no + * icon at all). + * + * @param entry the new entry to use. + */ + public void updateBubbleButton(NotificationEntry entry) { + applyBubbleAction(mExpandedChild, entry); + } + + private boolean isBubblesEnabled() { + return Settings.Global.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, 0) == 1; + } + + private void applyBubbleAction(View layout, NotificationEntry entry) { + if (layout == null || mContainingNotification == null || mPeopleIdentifier == null) { + return; + } + ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button); + View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container); + if (bubbleButton == null || actionContainer == null) { + return; + } + boolean isPerson = + mPeopleIdentifier.getPeopleNotificationType(entry.getSbn(), entry.getRanking()) + != PeopleNotificationIdentifier.TYPE_NON_PERSON; + boolean showButton = isBubblesEnabled() + && isPerson + && entry.getBubbleMetadata() != null; + if (showButton) { + Drawable d = mContext.getResources().getDrawable(entry.isBubble() + ? R.drawable.ic_stop_bubble + : R.drawable.ic_create_bubble); + mContainingNotification.updateNotificationColor(); + final int tint = mContainingNotification.getNotificationColor(); + d.setTint(tint); + + String contentDescription = mContext.getResources().getString(entry.isBubble() + ? R.string.notification_conversation_unbubble + : R.string.notification_conversation_bubble); + + bubbleButton.setContentDescription(contentDescription); + bubbleButton.setImageDrawable(d); + bubbleButton.setOnClickListener(mContainingNotification.getBubbleClickListener()); + bubbleButton.setVisibility(VISIBLE); + actionContainer.setVisibility(VISIBLE); + } else { + bubbleButton.setVisibility(GONE); + } + } + private void applySmartReplyView( SmartRepliesAndActions smartRepliesAndActions, NotificationEntry entry) { @@ -1512,6 +1579,10 @@ public class NotificationContentView extends FrameLayout { mContainingNotification = containingNotification; } + public void setPeopleNotificationIdentifier(PeopleNotificationIdentifier peopleIdentifier) { + mPeopleIdentifier = peopleIdentifier; + } + public void requestSelectLayout(boolean needsAnimation) { selectLayout(needsAnimation, false); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 290784ddbffec..a27199370b163 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -40,11 +40,9 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.graphics.drawable.Icon; -import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.os.RemoteException; -import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.transition.ChangeBounds; @@ -53,7 +51,6 @@ import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; @@ -64,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.statusbar.notification.NotificationChannelHelper; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -205,7 +203,8 @@ public class NotificationConversationInfo extends LinearLayout implements } mShortcutInfo = entry.getRanking().getShortcutInfo(); - createConversationChannelIfNeeded(); + mNotificationChannel = NotificationChannelHelper.createConversationChannelIfNeeded( + getContext(), mINotificationManager, entry, mNotificationChannel); bindHeader(); bindActions(); @@ -214,27 +213,6 @@ public class NotificationConversationInfo extends LinearLayout implements done.setOnClickListener(mOnDone); } - void createConversationChannelIfNeeded() { - // If this channel is not already a customized conversation channel, create - // a custom channel - if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) { - try { - // TODO: remove - mNotificationChannel.setName(mContext.getString( - R.string.notification_summary_message_format, - getName(), mNotificationChannel.getName())); - mINotificationManager.createConversationNotificationChannelForPackage( - mPackageName, mAppUid, mSbn.getKey(), mNotificationChannel, - mConversationId); - mNotificationChannel = mINotificationManager.getConversationNotificationChannel( - mContext.getOpPackageName(), UserHandle.getUserId(mAppUid), mPackageName, - mNotificationChannel.getId(), false, mConversationId); - } catch (RemoteException e) { - Slog.e(TAG, "Could not create conversation channel", e); - } - } - } - private void bindActions() { // TODO: b/152050825 @@ -318,24 +296,6 @@ public class NotificationConversationInfo extends LinearLayout implements } } - private void bindName() { - TextView name = findViewById(R.id.name); - name.setText(getName()); - } - - private String getName() { - if (mShortcutInfo != null) { - return mShortcutInfo.getShortLabel().toString(); - } else { - Bundle extras = mSbn.getNotification().extras; - String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE); - if (TextUtils.isEmpty(nameString)) { - nameString = extras.getString(Notification.EXTRA_TITLE); - } - return nameString; - } - } - private void bindPackage() { ApplicationInfo info; try { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 037f04ec1d7cd..95f0bd52f4ee7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.IActivityManager; +import android.app.INotificationManager; import android.app.Notification; import android.app.PendingIntent; import android.content.res.Resources; @@ -273,7 +274,8 @@ public class BubbleControllerTest extends SysuiTestCase { mFeatureFlagsOldPipeline, mDumpManager, mFloatingContentCoordinator, - mSysUiState); + mSysUiState, + mock(INotificationManager.class)); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java index 545de210d5b50..e8721a89c3b4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.IActivityManager; +import android.app.INotificationManager; import android.app.Notification; import android.app.PendingIntent; import android.content.res.Resources; @@ -250,7 +251,8 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase { mFeatureFlagsNewPipeline, mDumpManager, mFloatingContentCoordinator, - mSysUiState); + mSysUiState, + mock(INotificationManager.class)); mBubbleController.addNotifCallback(mNotifCallback); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java index f4861028e81ae..7815ae78823ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java @@ -16,6 +16,7 @@ package com.android.systemui.bubbles; +import android.app.INotificationManager; import android.content.Context; import com.android.systemui.dump.DumpManager; @@ -54,12 +55,14 @@ public class TestableBubbleController extends BubbleController { FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState) { + SysUiState sysUiState, + INotificationManager notificationManager) { super(context, notificationShadeWindowController, statusBarStateController, shadeController, data, Runnable::run, configurationController, interruptionStateProvider, zenModeController, lockscreenUserManager, groupManager, entryManager, - notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState); + notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState, + notificationManager); setInflateSynchronously(true); } } diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index b8140be2e2663..1051423ea17fb 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -51,7 +51,7 @@ public interface NotificationDelegate { /** * Called when the state of {@link Notification#FLAG_BUBBLE} is changed. */ - void onNotificationBubbleChanged(String key, boolean isBubble); + void onNotificationBubbleChanged(String key, boolean isBubble, int flags); /** * Called when the state of {@link Notification.BubbleMetadata#FLAG_SUPPRESS_NOTIFICATION} * changes. diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 2a81e37b8fa59..c11a80ddf352f 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1196,14 +1196,7 @@ public class NotificationManagerService extends SystemService { } @Override - public void onNotificationBubbleChanged(String key, boolean isBubble) { - String pkg; - synchronized (mNotificationLock) { - NotificationRecord r = mNotificationsByKey.get(key); - pkg = r != null && r.getSbn() != null ? r.getSbn().getPackageName() : null; - } - boolean isAppForeground = pkg != null - && mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND; + public void onNotificationBubbleChanged(String key, boolean isBubble, int flags) { synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { @@ -1220,8 +1213,13 @@ public class NotificationManagerService extends SystemService { // be applied there. r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; r.setFlagBubbleRemoved(false); + if (r.getNotification().getBubbleMetadata() != null) { + r.getNotification().getBubbleMetadata().setFlags(flags); + } + // Force isAppForeground true here, because for sysui's purposes we + // want to adjust the flag behaviour. mHandler.post(new EnqueueNotificationRunnable(r.getUser().getIdentifier(), - r, isAppForeground)); + r, true /* isAppForeground*/)); } } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index d7a0c9871b483..289bf66e1add1 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -1380,11 +1380,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onNotificationBubbleChanged(String key, boolean isBubble) { + public void onNotificationBubbleChanged(String key, boolean isBubble, int flags) { enforceStatusBarService(); long identity = Binder.clearCallingIdentity(); try { - mNotificationDelegate.onNotificationBubbleChanged(key, isBubble); + mNotificationDelegate.onNotificationBubbleChanged(key, isBubble, flags); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index e6e84eeab4ed2..3cd0e92964ec1 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -5548,7 +5548,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertTrue((notifsBefore[0].getNotification().flags & FLAG_BUBBLE) != 0); // Notify we're not a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0); waitForIdle(); // Make sure we are not a bubble @@ -5587,7 +5587,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { reset(mListeners); // Notify we are now a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0); waitForIdle(); // Make sure we are a bubble @@ -5618,7 +5618,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertEquals((notifsBefore[0].getNotification().flags & FLAG_BUBBLE), 0); // Notify we are now a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0); waitForIdle(); // We still wouldn't be a bubble because the notification didn't meet requirements @@ -5646,7 +5646,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertFalse(recordToCheck.isFlagBubbleRemoved()); // Notify we're not a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0); waitForIdle(); // Flag should be modified recordToCheck = mService.getNotificationRecord(nr.getSbn().getKey()); @@ -5681,14 +5681,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertFalse(recordToCheck.isFlagBubbleRemoved()); // Notify we're not a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0); waitForIdle(); // Flag should be modified recordToCheck = mService.getNotificationRecord(nr.getSbn().getKey()); assertTrue(recordToCheck.isFlagBubbleRemoved()); // Notify we are a bubble - mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true); + mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0); waitForIdle(); // And the flag is reset assertFalse(recordToCheck.isFlagBubbleRemoved()); From 0f99c3c2891bde7da871151a6b25585d15e7b49b Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Tue, 7 Apr 2020 19:38:19 -0700 Subject: [PATCH 3/3] Changes to enable bubble settings CTS * NotificationShellCmds for setting bubble app and channel user preference * Fix missing check for foreground service * Fix removing shortcut info when shortcut is removed Test: see CTS cl Bug: 138116133 Change-Id: I25ba0ab74c5c27cc768a72f0ece451800a6053ea --- .../server/notification/BubbleExtractor.java | 6 ++- .../NotificationManagerService.java | 24 +++++++--- .../notification/NotificationShellCmd.java | 44 +++++++++++++++++-- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java index 536ea30d6a947..27802ffc013d9 100644 --- a/services/core/java/com/android/server/notification/BubbleExtractor.java +++ b/services/core/java/com/android/server/notification/BubbleExtractor.java @@ -16,6 +16,7 @@ package com.android.server.notification; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; @@ -89,9 +90,10 @@ public class BubbleExtractor implements NotificationSignalExtractor { record.setAllowBubble(recordChannel.canBubble()); } - final boolean fulfillsPolicy = record.isConversation() + final boolean fulfillsPolicy = record.canBubble() + && record.isConversation() && !mActivityManager.isLowRamDevice() - && record.canBubble(); + && (record.getNotification().flags & FLAG_FOREGROUND_SERVICE) == 0; final boolean applyFlag = fulfillsPolicy && canPresentAsBubble(record); if (applyFlag) { record.getNotification().flags |= FLAG_BUBBLE; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c11a80ddf352f..54efe543a29f5 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3095,7 +3095,7 @@ public class NotificationManagerService extends SystemService { if (UserHandle.getCallingUserId() != UserHandle.getUserId(uid)) { getContext().enforceCallingPermission( android.Manifest.permission.INTERACT_ACROSS_USERS, - "canNotifyAsPackage for uid " + uid); + "getBubblePreferenceForPackage for uid " + uid); } return mPreferencesHelper.getBubblePreference(pkg, uid); @@ -3103,7 +3103,7 @@ public class NotificationManagerService extends SystemService { @Override public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { - enforceSystemOrSystemUI("Caller not system or systemui"); + checkCallerIsSystemOrSystemUiOrShell("Caller not system or sysui or shell"); mPreferencesHelper.setBubblesAllowed(pkg, uid, bubblePreference); handleSavePolicyFile(); } @@ -3304,7 +3304,7 @@ public class NotificationManagerService extends SystemService { String targetPkg, String channelId, boolean returnParentIfNoConversationChannel, String conversationId) { if (canNotifyAsPackage(callingPkg, targetPkg, userId) - || isCallerIsSystemOrSystemUi()) { + || isCallerIsSystemOrSysemUiOrShell()) { int targetUid = -1; try { targetUid = mPackageManagerClient.getPackageUidAsUser(targetPkg, userId); @@ -3414,7 +3414,7 @@ public class NotificationManagerService extends SystemService { @Override public void updateNotificationChannelForPackage(String pkg, int uid, NotificationChannel channel) { - enforceSystemOrSystemUI("Caller not system or systemui"); + checkCallerIsSystemOrSystemUiOrShell("Caller not system or sysui or shell"); Objects.requireNonNull(channel); updateNotificationChannelInt(pkg, uid, channel, false); } @@ -5844,6 +5844,7 @@ public class NotificationManagerService extends SystemService { synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { + r.setShortcutInfo(null); // Enqueue will trigger resort & flag is updated that way. r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; mHandler.post( @@ -8247,6 +8248,14 @@ public class NotificationManagerService extends SystemService { == PERMISSION_GRANTED; } + private boolean isCallerIsSystemOrSysemUiOrShell() { + int callingUid = Binder.getCallingUid(); + if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) { + return true; + } + return isCallerIsSystemOrSystemUi(); + } + private void checkCallerIsSystemOrShell() { int callingUid = Binder.getCallingUid(); if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) { @@ -8263,6 +8272,10 @@ public class NotificationManagerService extends SystemService { } private void checkCallerIsSystemOrSystemUiOrShell() { + checkCallerIsSystemOrSystemUiOrShell(null); + } + + private void checkCallerIsSystemOrSystemUiOrShell(String message) { int callingUid = Binder.getCallingUid(); if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) { return; @@ -8270,7 +8283,8 @@ public class NotificationManagerService extends SystemService { if (isCallerSystemOrPhone()) { return; } - getContext().enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE, null); + getContext().enforceCallingPermission(android.Manifest.permission.STATUS_BAR_SERVICE, + message); } private void checkCallerIsSystemOrSameApp(String pkg) { diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java index 2b5ba25284296..e4a17740b0b7c 100644 --- a/services/core/java/com/android/server/notification/NotificationShellCmd.java +++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java @@ -38,7 +38,6 @@ import android.content.res.Resources; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; -import android.media.IRingtonePlayer; import android.net.Uri; import android.os.Binder; import android.os.Process; @@ -48,8 +47,6 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; -import com.android.internal.util.FunctionalUtils; - import java.io.PrintWriter; import java.net.URISyntaxException; import java.util.Collections; @@ -72,7 +69,11 @@ public class NotificationShellCmd extends ShellCommand { + " unsuspend_package PACKAGE\n" + " reset_assistant_user_set [user_id (current user if not specified)]\n" + " get_approved_assistant [user_id (current user if not specified)]\n" - + " post [--help | flags] TAG TEXT"; + + " post [--help | flags] TAG TEXT\n" + + " set_bubbles PACKAGE PREFERENCE (0=none 1=all 2=selected) " + + "[user_id (current user if not specified)]\n" + + " set_bubbles_channel PACKAGE CHANNEL_ID ALLOW " + + "[user_id (current user if not specified)]\n"; private static final String NOTIFY_USAGE = "usage: cmd notification post [flags] \n\n" @@ -109,6 +110,7 @@ public class NotificationShellCmd extends ShellCommand { private final NotificationManagerService mDirectService; private final INotificationManager mBinderService; private final PackageManager mPm; + private NotificationChannel mChannel; public NotificationShellCmd(NotificationManagerService service) { mDirectService = service; @@ -276,6 +278,40 @@ public class NotificationShellCmd extends ShellCommand { } break; } + case "set_bubbles": { + // only use for testing + String packageName = getNextArgRequired(); + int preference = Integer.parseInt(getNextArgRequired()); + if (preference > 3 || preference < 0) { + pw.println("Invalid preference - must be between 0-3 " + + "(0=none 1=all 2=selected)"); + return -1; + } + int userId = ActivityManager.getCurrentUser(); + if (peekNextArg() != null) { + userId = Integer.parseInt(getNextArgRequired()); + } + int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0)); + mBinderService.setBubblesAllowed(packageName, appUid, preference); + break; + } + case "set_bubbles_channel": { + // only use for testing + String packageName = getNextArgRequired(); + String channelId = getNextArgRequired(); + boolean allow = Boolean.parseBoolean(getNextArgRequired()); + int userId = ActivityManager.getCurrentUser(); + if (peekNextArg() != null) { + userId = Integer.parseInt(getNextArgRequired()); + } + NotificationChannel channel = mBinderService.getNotificationChannel( + callingPackage, userId, packageName, channelId); + channel.setAllowBubbles(allow); + int appUid = UserHandle.getUid(userId, mPm.getPackageUid(packageName, 0)); + mBinderService.updateNotificationChannelForPackage(packageName, appUid, + channel); + break; + } case "post": case "notify": doNotify(pw, callingPackage, callingUid);