Notification Bubble Button

Button is visible on expanded notifications that are in the
conversation section.

Always visible if the notification has metadata & the user
hasn't turned off bubbles for the device.

Button is added to end of the template actions, making it
visible if there were no other actions.

Tapping it to make a button will change the app settings
to "selected" and enable bubbles for that channel, if the
bubble wasn't allowed otherwise.

Tapping it to stop the bubble will disallow that channel
from bubbling.

Test: manual:
- check bubble button on notif with long action text
- check button is hidden when inline reply is focused
- check no bubble button on non-conversation notifs
- check create bubble from HUN
- check create bubble from expanded notif
- check that dismissing a bubble updates state of the button
- check that getting new notif after dismissing bubble
  updates state of the button

Bug: 138116133
Change-Id: Ic12204bd191530e94ea0400e16a9315a0d149252
This commit is contained in:
Mady Mellor
2020-03-30 17:23:26 -07:00
parent a92268cd01
commit 9adfe6a20c
18 changed files with 329 additions and 126 deletions

View File

@@ -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);

View File

@@ -20,16 +20,34 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/notification_action_list_margin_top"
android:layout_gravity="bottom">
<com.android.internal.widget.NotificationActionListLayout
android:id="@+id/actions"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/notification_action_list_height"
android:paddingEnd="12dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="gone"
android:background="@color/notification_action_list_background_color"
android:paddingEnd="12dp"
>
<!-- actions will be added here -->
</com.android.internal.widget.NotificationActionListLayout>
<com.android.internal.widget.NotificationActionListLayout
android:id="@+id/actions"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_action_list_height"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="gone"
android:background="@color/notification_action_list_background_color"
>
<!-- actions will be added here -->
</com.android.internal.widget.NotificationActionListLayout>
<ImageView
android:id="@+id/bubble_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical|end"
android:visibility="gone"
android:scaleType="centerInside"
/>
</LinearLayout>
</FrameLayout>

View File

@@ -3495,6 +3495,7 @@
<java-symbol type="id" name="clip_children_set_tag" />
<java-symbol type="id" name="clip_to_padding_tag" />
<java-symbol type="id" name="clip_children_tag" />
<java-symbol type="id" name="bubble_button" />
<java-symbol type="dimen" name="messaging_avatar_size" />
<java-symbol type="dimen" name="messaging_group_sending_progress_size" />
<java-symbol type="dimen" name="messaging_image_rounding" />

View File

@@ -14,16 +14,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9c0,1.39 0.32,2.69 0.88,3.86l1.53,-1.53C5.15,13.6 5,12.82 5,12c0,-3.86 3.14,-7 7,-7s7,3.14 7,7s-3.14,7 -7,7c-0.83,0 -1.62,-0.15 -2.35,-0.42l-1.53,1.53C9.3,20.67 10.61,21 12,21c4.97,0 9,-4.03 9,-9C21,7.03 16.97,3 12,3z"
android:fillColor="#000000"/>
<path
android:pathData="M12.99,15.99l2,0l0,-7l-7,0l0,2l3.59,0l-8.79,8.8l1.41,1.41l8.79,-8.79z"
android:fillColor="#000000"/>
</vector>
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M22,12C22,12 22,12 22,12C22,12 22,12 22,12c0,0.56 -0.06,1.1 -0.15,1.64l-1.97,-0.33c0.15,-0.91 0.15,-1.84 -0.02,-2.75c-0.01,-0.03 -0.01,-0.07 -0.02,-0.1c-0.03,-0.18 -0.08,-0.36 -0.13,-0.54c-0.02,-0.08 -0.04,-0.16 -0.06,-0.24c-0.04,-0.14 -0.09,-0.27 -0.14,-0.41c-0.04,-0.12 -0.08,-0.24 -0.13,-0.35c-0.04,-0.09 -0.08,-0.18 -0.13,-0.27c-0.07,-0.15 -0.14,-0.3 -0.22,-0.45c-0.03,-0.05 -0.06,-0.09 -0.08,-0.14c-0.72,-1.26 -1.77,-2.31 -3.03,-3.03c-0.05,-0.03 -0.09,-0.06 -0.14,-0.08c-0.15,-0.08 -0.3,-0.15 -0.45,-0.22c-0.09,-0.04 -0.18,-0.09 -0.27,-0.13c-0.11,-0.05 -0.23,-0.09 -0.35,-0.13c-0.14,-0.05 -0.27,-0.1 -0.41,-0.14c-0.08,-0.02 -0.16,-0.04 -0.23,-0.06c-0.18,-0.05 -0.36,-0.1 -0.54,-0.13c-0.03,-0.01 -0.07,-0.01 -0.1,-0.01c-0.95,-0.17 -1.93,-0.17 -2.88,0c-0.03,0.01 -0.07,0.01 -0.1,0.01c-0.18,0.04 -0.36,0.08 -0.54,0.13C9.85,4.3 9.77,4.32 9.69,4.34C9.55,4.38 9.42,4.44 9.28,4.49C9.17,4.53 9.05,4.57 8.93,4.61C8.84,4.65 8.75,4.7 8.66,4.74c-0.15,0.07 -0.3,0.14 -0.45,0.22C8.16,4.98 8.12,5.01 8.07,5.04C5.64,6.42 4,9.02 4,12c0,2.74 1.39,5.16 3.49,6.6c0.01,0.01 0.03,0.02 0.04,0.03c0.16,0.11 0.33,0.2 0.49,0.3c0.06,0.04 0.12,0.08 0.19,0.11c0.13,0.07 0.27,0.13 0.4,0.19c0.11,0.05 0.21,0.1 0.32,0.15c0.1,0.04 0.2,0.07 0.29,0.11c0.15,0.06 0.31,0.11 0.46,0.16c0.05,0.02 0.11,0.03 0.17,0.04c1.11,0.31 2.27,0.35 3.4,0.18l0.35,1.98c-0.54,0.09 -1.08,0.14 -1.62,0.14V22c-0.65,0 -1.28,-0.07 -1.9,-0.19c-0.01,0 -0.01,0 -0.02,0c-0.25,-0.05 -0.49,-0.11 -0.73,-0.18c-0.08,-0.02 -0.16,-0.04 -0.23,-0.06c-0.19,-0.06 -0.37,-0.13 -0.55,-0.19c-0.13,-0.05 -0.26,-0.09 -0.39,-0.14c-0.13,-0.05 -0.25,-0.12 -0.38,-0.18c-0.18,-0.08 -0.35,-0.16 -0.53,-0.25c-0.07,-0.04 -0.14,-0.08 -0.21,-0.13c-0.22,-0.12 -0.43,-0.25 -0.64,-0.39c-0.01,-0.01 -0.02,-0.02 -0.04,-0.03c-0.51,-0.35 -1,-0.74 -1.45,-1.2l0,0C3.12,17.26 2,14.76 2,12c0,-2.76 1.12,-5.26 2.93,-7.07l0,0c0.45,-0.45 0.93,-0.84 1.44,-1.19C6.39,3.73 6.4,3.72 6.42,3.71c0.2,-0.14 0.41,-0.26 0.62,-0.38c0.08,-0.05 0.15,-0.09 0.23,-0.14c0.17,-0.09 0.33,-0.16 0.5,-0.24c0.13,-0.06 0.27,-0.13 0.4,-0.19C8.3,2.71 8.42,2.67 8.55,2.63c0.19,-0.07 0.38,-0.14 0.58,-0.2c0.07,-0.02 0.14,-0.03 0.21,-0.05C10.18,2.14 11.07,2 12,2c0.65,0 1.29,0.07 1.91,0.19c0,0 0,0 0,0c0.25,0.05 0.5,0.11 0.75,0.18c0.07,0.02 0.14,0.03 0.22,0.06c0.19,0.06 0.38,0.13 0.57,0.2c0.12,0.05 0.25,0.09 0.37,0.14c0.14,0.06 0.27,0.12 0.4,0.18c0.17,0.08 0.34,0.16 0.51,0.25c0.08,0.04 0.15,0.09 0.23,0.14c0.21,0.12 0.42,0.24 0.62,0.38c0.01,0.01 0.03,0.02 0.04,0.03c0.51,0.35 0.99,0.74 1.45,1.19c0.24,0.24 0.47,0.49 0.68,0.75c0.04,0.04 0.06,0.09 0.1,0.13c0.17,0.22 0.34,0.45 0.5,0.68c0.01,0.01 0.02,0.03 0.03,0.04c0.69,1.05 1.17,2.21 1.42,3.44c0,0 0,0.01 0,0.01c0.06,0.29 0.1,0.58 0.13,0.87c0.01,0.04 0.01,0.09 0.02,0.13C21.98,11.32 22,11.66 22,12zM18.5,15c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S20.43,15 18.5,15z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M21.98,18.32l-3.3,-3.3C20.47,15.11 21.89,16.53 21.98,18.32zM8.66,4.74C8.75,4.7 8.84,4.65 8.93,4.61c0.11,-0.05 0.23,-0.09 0.35,-0.13c0.14,-0.05 0.27,-0.1 0.41,-0.14C9.77,4.32 9.85,4.3 9.92,4.28c0.18,-0.05 0.36,-0.1 0.54,-0.13c0.03,-0.01 0.07,-0.01 0.1,-0.01c0.95,-0.17 1.93,-0.17 2.88,0c0.03,0.01 0.07,0.01 0.1,0.01c0.18,0.04 0.36,0.08 0.54,0.13c0.08,0.02 0.16,0.04 0.23,0.06c0.14,0.04 0.27,0.09 0.41,0.14c0.12,0.04 0.23,0.08 0.35,0.13c0.09,0.04 0.18,0.09 0.27,0.13c0.15,0.07 0.3,0.14 0.45,0.22c0.05,0.03 0.09,0.06 0.14,0.08c1.26,0.72 2.31,1.77 3.03,3.03c0.03,0.05 0.06,0.09 0.08,0.14c0.08,0.15 0.15,0.3 0.22,0.45c0.04,0.09 0.09,0.18 0.13,0.27c0.05,0.11 0.09,0.23 0.13,0.35c0.05,0.13 0.1,0.27 0.14,0.41c0.02,0.08 0.04,0.16 0.06,0.24c0.05,0.18 0.1,0.36 0.13,0.54c0.01,0.03 0.01,0.07 0.02,0.1c0.16,0.91 0.17,1.84 0.02,2.75l1.97,0.33C21.94,13.1 22,12.56 22,12c0,0 0,0 0,0s0,0 0,0c0,-0.34 -0.02,-0.68 -0.05,-1.01c0,-0.04 -0.01,-0.09 -0.02,-0.13c-0.03,-0.29 -0.07,-0.58 -0.13,-0.87c0,0 0,-0.01 0,-0.01c-0.25,-1.23 -0.73,-2.39 -1.42,-3.44c-0.01,-0.01 -0.02,-0.03 -0.03,-0.04c-0.15,-0.23 -0.32,-0.46 -0.5,-0.68c-0.03,-0.04 -0.06,-0.09 -0.1,-0.13c-0.21,-0.26 -0.44,-0.51 -0.68,-0.75c-0.45,-0.45 -0.94,-0.84 -1.45,-1.19c-0.01,-0.01 -0.03,-0.02 -0.04,-0.03c-0.2,-0.14 -0.41,-0.26 -0.62,-0.38c-0.08,-0.04 -0.15,-0.09 -0.23,-0.14c-0.17,-0.09 -0.34,-0.17 -0.51,-0.25c-0.13,-0.06 -0.26,-0.13 -0.4,-0.18c-0.12,-0.05 -0.25,-0.09 -0.37,-0.14c-0.19,-0.07 -0.38,-0.14 -0.57,-0.2c-0.07,-0.02 -0.14,-0.04 -0.22,-0.06c-0.25,-0.07 -0.5,-0.13 -0.75,-0.18c0,0 0,0 0,0C13.29,2.07 12.65,2 12,2c-0.93,0 -1.82,0.14 -2.67,0.37C9.26,2.39 9.19,2.41 9.12,2.43c-0.2,0.06 -0.39,0.13 -0.58,0.2C8.42,2.67 8.3,2.71 8.18,2.76c-0.14,0.06 -0.27,0.12 -0.4,0.19C7.61,3.03 7.44,3.1 7.27,3.19C7.19,3.24 7.12,3.29 7.04,3.33C7.03,3.34 7.02,3.34 7.01,3.35l1.48,1.48C8.55,4.8 8.6,4.77 8.66,4.74zM2.71,1.29L1.29,2.71l2.97,2.97C2.85,7.4 2,9.6 2,12c0,2.76 1.12,5.26 2.93,7.07l0,0c0.45,0.45 0.94,0.85 1.45,1.2c0.01,0.01 0.02,0.02 0.04,0.03c0.21,0.14 0.42,0.27 0.64,0.39c0.07,0.04 0.14,0.09 0.21,0.13c0.17,0.09 0.35,0.17 0.53,0.25c0.13,0.06 0.25,0.12 0.38,0.18c0.13,0.05 0.26,0.1 0.39,0.14c0.18,0.07 0.36,0.14 0.55,0.19c0.08,0.02 0.16,0.04 0.23,0.06c0.24,0.07 0.48,0.13 0.73,0.18c0.01,0 0.01,0 0.02,0C10.72,21.93 11.35,22 12,22v-0.01c0.54,0 1.08,-0.05 1.62,-0.14l-0.35,-1.98c-1.13,0.18 -2.29,0.13 -3.4,-0.18c-0.06,-0.02 -0.11,-0.03 -0.17,-0.04c-0.16,-0.05 -0.31,-0.11 -0.46,-0.16c-0.1,-0.04 -0.2,-0.07 -0.29,-0.11c-0.11,-0.05 -0.22,-0.1 -0.32,-0.15c-0.13,-0.06 -0.27,-0.12 -0.4,-0.19c-0.06,-0.03 -0.13,-0.08 -0.19,-0.11c-0.17,-0.1 -0.33,-0.19 -0.49,-0.3c-0.01,-0.01 -0.03,-0.02 -0.04,-0.03C5.39,17.16 4,14.74 4,12c0,-1.85 0.64,-3.54 1.7,-4.89l9.73,9.73C15.16,17.33 15,17.9 15,18.5c0,1.93 1.57,3.5 3.5,3.5c0.6,0 1.17,-0.16 1.66,-0.43l1.13,1.13l1.41,-1.41L2.71,1.29z"/>
</vector>

View File

@@ -17,6 +17,8 @@
package com.android.systemui.bubbles;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
@@ -30,7 +32,6 @@ import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_EXPERIMENTS;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -43,6 +44,9 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.UserIdInt;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -83,6 +87,7 @@ import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.notification.NotificationChannelHelper;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
@@ -169,6 +174,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final ZenModeController mZenModeController;
private StatusBarStateListener mStatusBarStateListener;
private INotificationManager mINotificationManager;
// Callback that updates BubbleOverflowActivity on data change.
@Nullable private Runnable mOverflowCallback = null;
@@ -293,11 +299,13 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
FeatureFlags featureFlags,
DumpManager dumpManager,
FloatingContentCoordinator floatingContentCoordinator,
SysUiState sysUiState) {
SysUiState sysUiState,
INotificationManager notificationManager) {
this(context, notificationShadeWindowController, statusBarStateController, shadeController,
data, null /* synchronizer */, configurationController, interruptionStateProvider,
zenModeController, notifUserManager, groupManager, entryManager,
notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState);
notifPipeline, featureFlags, dumpManager, floatingContentCoordinator, sysUiState,
notificationManager);
}
/**
@@ -319,7 +327,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
FeatureFlags featureFlags,
DumpManager dumpManager,
FloatingContentCoordinator floatingContentCoordinator,
SysUiState sysUiState) {
SysUiState sysUiState,
INotificationManager notificationManager) {
dumpManager.registerDumpable(TAG, this);
mContext = context;
mShadeController = shadeController;
@@ -327,6 +336,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
mNotifUserManager = notifUserManager;
mZenModeController = zenModeController;
mFloatingContentCoordinator = floatingContentCoordinator;
mINotificationManager = notificationManager;
mZenModeController.addCallback(new ZenModeController.Callback() {
@Override
public void onZenChanged(int zen) {
@@ -829,37 +839,43 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
* This method will collapse the shade, create the bubble without a flyout or dot, and suppress
* the notification from appearing in the shade.
*
* @param entry the notification to show as a bubble.
* @param entry the notification to change bubble state for.
* @param shouldBubble whether the notification should show as a bubble or not.
*/
public void onUserCreatedBubbleFromNotification(NotificationEntry entry) {
if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) {
Log.d(TAG, "onUserCreatedBubble: " + entry.getKey());
public void onUserChangedBubble(NotificationEntry entry, boolean shouldBubble) {
NotificationChannel channel = entry.getChannel();
final String appPkg = entry.getSbn().getPackageName();
final int appUid = entry.getSbn().getUid();
if (channel == null || appPkg == null) {
return;
}
mShadeController.collapsePanel(true);
entry.setFlagBubble(true);
updateBubble(entry, true /* suppressFlyout */, false /* showInShade */);
mUserCreatedBubbles.add(entry.getKey());
mUserBlockedBubbles.remove(entry.getKey());
}
/**
* Called when a user has indicated that an active notification appearing as a bubble should
* no longer be shown as a bubble.
*
* @param entry the notification to no longer show as a bubble.
*/
public void onUserDemotedBubbleFromNotification(NotificationEntry entry) {
if (DEBUG_EXPERIMENTS || DEBUG_BUBBLE_CONTROLLER) {
Log.d(TAG, "onUserDemotedBubble: " + entry.getKey());
// Update the state in NotificationManagerService
try {
int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags);
} catch (RemoteException e) {
}
entry.setFlagBubble(false);
removeBubble(entry, DISMISS_BLOCKED);
mUserCreatedBubbles.remove(entry.getKey());
if (BubbleExperimentConfig.isPackageWhitelistedToAutoBubble(
mContext, entry.getSbn().getPackageName())) {
// This package is whitelist but user demoted the bubble, let's save it so we don't
// auto-bubble for the whitelist again.
mUserBlockedBubbles.add(entry.getKey());
// Change the settings
channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext,
mINotificationManager, entry, channel);
channel.setAllowBubbles(shouldBubble);
try {
int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid);
if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) {
mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED);
}
mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel);
} catch (RemoteException e) {
Log.e(TAG, e.getMessage());
}
if (shouldBubble) {
mShadeController.collapsePanel(true);
if (entry.getRow() != null) {
entry.getRow().updateBubbleButton();
}
}
}
@@ -1007,14 +1023,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
} else {
// Update the flag for SysUI
bubble.getEntry().getSbn().getNotification().flags &= ~FLAG_BUBBLE;
if (bubble.getEntry().getRow() != null) {
bubble.getEntry().getRow().updateBubbleButton();
}
// Make sure NoMan knows it's not a bubble anymore so anyone querying it
// will get right result back
// Update the state in NotificationManagerService
try {
mBarService.onNotificationBubbleChanged(bubble.getKey(),
false /* isBubble */);
false /* isBubble */, 0 /* flags */);
} catch (RemoteException e) {
// Bad things have happened
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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() {

View File

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

View File

@@ -40,11 +40,9 @@ import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.transition.ChangeBounds;
@@ -53,7 +51,6 @@ import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Slog;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
@@ -64,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.notification.ConversationIconFactory;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.NotificationChannelHelper;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -205,7 +203,8 @@ public class NotificationConversationInfo extends LinearLayout implements
}
mShortcutInfo = entry.getRanking().getShortcutInfo();
createConversationChannelIfNeeded();
mNotificationChannel = NotificationChannelHelper.createConversationChannelIfNeeded(
getContext(), mINotificationManager, entry, mNotificationChannel);
bindHeader();
bindActions();
@@ -214,27 +213,6 @@ public class NotificationConversationInfo extends LinearLayout implements
done.setOnClickListener(mOnDone);
}
void createConversationChannelIfNeeded() {
// If this channel is not already a customized conversation channel, create
// a custom channel
if (TextUtils.isEmpty(mNotificationChannel.getConversationId())) {
try {
// TODO: remove
mNotificationChannel.setName(mContext.getString(
R.string.notification_summary_message_format,
getName(), mNotificationChannel.getName()));
mINotificationManager.createConversationNotificationChannelForPackage(
mPackageName, mAppUid, mSbn.getKey(), mNotificationChannel,
mConversationId);
mNotificationChannel = mINotificationManager.getConversationNotificationChannel(
mContext.getOpPackageName(), UserHandle.getUserId(mAppUid), mPackageName,
mNotificationChannel.getId(), false, mConversationId);
} catch (RemoteException e) {
Slog.e(TAG, "Could not create conversation channel", e);
}
}
}
private void bindActions() {
// TODO: b/152050825
@@ -318,24 +296,6 @@ public class NotificationConversationInfo extends LinearLayout implements
}
}
private void bindName() {
TextView name = findViewById(R.id.name);
name.setText(getName());
}
private String getName() {
if (mShortcutInfo != null) {
return mShortcutInfo.getShortLabel().toString();
} else {
Bundle extras = mSbn.getNotification().extras;
String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
if (TextUtils.isEmpty(nameString)) {
nameString = extras.getString(Notification.EXTRA_TITLE);
}
return nameString;
}
}
private void bindPackage() {
ApplicationInfo info;
try {

View File

@@ -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);

View File

@@ -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);

View File

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

View File

@@ -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.

View File

@@ -1196,14 +1196,7 @@ public class NotificationManagerService extends SystemService {
}
@Override
public void onNotificationBubbleChanged(String key, boolean isBubble) {
String pkg;
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
pkg = r != null && r.getSbn() != null ? r.getSbn().getPackageName() : null;
}
boolean isAppForeground = pkg != null
&& mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
public void onNotificationBubbleChanged(String key, boolean isBubble, int flags) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r != null) {
@@ -1220,8 +1213,13 @@ public class NotificationManagerService extends SystemService {
// be applied there.
r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
r.setFlagBubbleRemoved(false);
if (r.getNotification().getBubbleMetadata() != null) {
r.getNotification().getBubbleMetadata().setFlags(flags);
}
// Force isAppForeground true here, because for sysui's purposes we
// want to adjust the flag behaviour.
mHandler.post(new EnqueueNotificationRunnable(r.getUser().getIdentifier(),
r, isAppForeground));
r, true /* isAppForeground*/));
}
}
}

View File

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

View File

@@ -5548,7 +5548,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
assertTrue((notifsBefore[0].getNotification().flags & FLAG_BUBBLE) != 0);
// Notify we're not a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0);
waitForIdle();
// Make sure we are not a bubble
@@ -5587,7 +5587,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
reset(mListeners);
// Notify we are now a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0);
waitForIdle();
// Make sure we are a bubble
@@ -5618,7 +5618,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
assertEquals((notifsBefore[0].getNotification().flags & FLAG_BUBBLE), 0);
// Notify we are now a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0);
waitForIdle();
// We still wouldn't be a bubble because the notification didn't meet requirements
@@ -5646,7 +5646,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
assertFalse(recordToCheck.isFlagBubbleRemoved());
// Notify we're not a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0);
waitForIdle();
// Flag should be modified
recordToCheck = mService.getNotificationRecord(nr.getSbn().getKey());
@@ -5681,14 +5681,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
assertFalse(recordToCheck.isFlagBubbleRemoved());
// Notify we're not a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), false, 0);
waitForIdle();
// Flag should be modified
recordToCheck = mService.getNotificationRecord(nr.getSbn().getKey());
assertTrue(recordToCheck.isFlagBubbleRemoved());
// Notify we are a bubble
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true);
mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(), true, 0);
waitForIdle();
// And the flag is reset
assertFalse(recordToCheck.isFlagBubbleRemoved());