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/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 fdef5dd69aa0a..5317c8bd238ec 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3496,6 +3496,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 669a86b8a7429..da5c2968c6acb 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) {
@@ -809,37 +819,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();
+ }
}
}
@@ -987,14 +1003,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 998230f205ab0..85090dcbf748b 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 6fc1264d69e2a..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
@@ -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;
@@ -38,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;
@@ -51,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;
@@ -62,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;
@@ -203,7 +203,8 @@ public class NotificationConversationInfo extends LinearLayout implements
}
mShortcutInfo = entry.getRanking().getShortcutInfo();
- createConversationChannelIfNeeded();
+ mNotificationChannel = NotificationChannelHelper.createConversationChannelIfNeeded(
+ getContext(), mINotificationManager, entry, mNotificationChannel);
bindHeader();
bindActions();
@@ -212,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
@@ -316,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 {
@@ -598,6 +560,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/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index e472de3494664..2f5ef00056fbf 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 5f4f2ef04c1de..9da160c3820c0 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/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java
index 2fa80cd8e4e4f..27802ffc013d9 100644
--- a/services/core/java/com/android/server/notification/BubbleExtractor.java
+++ b/services/core/java/com/android/server/notification/BubbleExtractor.java
@@ -16,12 +16,18 @@
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;
+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 +38,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 +66,35 @@ 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.canBubble()
+ && record.isConversation()
+ && !mActivityManager.isLowRamDevice()
+ && (record.getNotification().flags & FLAG_FOREGROUND_SERVICE) == 0;
+ final boolean applyFlag = fulfillsPolicy && canPresentAsBubble(record);
if (applyFlag) {
record.getNotification().flags |= FLAG_BUBBLE;
} else {
@@ -95,165 +112,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/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 f9fc82bf05b13..54efe543a29f5 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;
@@ -1195,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) {
@@ -1219,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*/));
}
}
}
@@ -3079,39 +3078,36 @@ 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");
if (UserHandle.getCallingUserId() != UserHandle.getUserId(uid)) {
getContext().enforceCallingPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS,
- "canNotifyAsPackage for uid " + uid);
+ "getBubblePreferenceForPackage for uid " + uid);
}
- return mPreferencesHelper.areBubblesAllowed(pkg, uid);
+ return mPreferencesHelper.getBubblePreference(pkg, uid);
}
@Override
- public void setBubblesAllowed(String pkg, int uid, boolean allowed) {
- enforceSystemOrSystemUI("Caller not system or systemui");
- mPreferencesHelper.setBubblesAllowed(pkg, uid, allowed);
+ public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
+ checkCallerIsSystemOrSystemUiOrShell("Caller not system or sysui or shell");
+ 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);
@@ -3308,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);
@@ -3418,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);
}
@@ -5848,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(
@@ -7210,7 +7207,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);
@@ -8248,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) {
@@ -8264,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;
@@ -8271,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);
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/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/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..3cd0e92964ec1 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,
@@ -5528,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
@@ -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,
@@ -5565,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
@@ -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);
@@ -5594,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
@@ -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");
@@ -5619,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());
@@ -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");
@@ -5651,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());
@@ -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();
}