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(); }