Merge "When a Bubble notification is clicked, show the bubble"

This commit is contained in:
Mark Renouf
2019-04-01 18:48:51 +00:00
committed by Android (Google) Code Review
8 changed files with 229 additions and 59 deletions

View File

@@ -292,6 +292,17 @@ public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListe
}
}
/**
* Request the stack expand if needed, then select the specified Bubble as current.
*
* @param notificationKey the notification key for the bubble to be selected
*/
public void expandStackAndSelectBubble(String notificationKey) {
if (mStackView != null && mBubbleData.getBubble(notificationKey) != null) {
mStackView.setExpandedBubble(notificationKey);
}
}
/**
* Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
*/

View File

@@ -78,8 +78,7 @@ public final class NotificationClicker implements View.OnClickListener {
row.setJustClicked(true);
DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));
// If it was a bubble we should close it
if (row.getEntry().isBubble()) {
if (!row.getEntry().isBubble()) {
mBubbleController.collapseStack();
}
@@ -95,7 +94,8 @@ public final class NotificationClicker implements View.OnClickListener {
*/
public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
Notification notification = sbn.getNotification();
if (notification.contentIntent != null || notification.fullScreenIntent != null) {
if (notification.contentIntent != null || notification.fullScreenIntent != null
|| row.getEntry().isBubble()) {
row.setOnClickListener(this);
} else {
row.setOnClickListener(null);

View File

@@ -220,8 +220,6 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
StatusBarNotification sbn,
ExpandableNotificationRow row) {
row.setIsLowPriority(entry.ambient);
// bind the click event to the content area
checkNotNull(mNotificationClicker).register(row, sbn);
// Extract target SDK version.
try {
@@ -257,6 +255,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
row.setNeedsRedaction(
Dependency.get(NotificationLockscreenUserManager.class).needsRedaction(entry));
row.inflateViews();
// bind the click event to the content area
checkNotNull(mNotificationClicker).register(row, sbn);
}
private void logNotificationExpansion(String key, boolean userAction, boolean expanded) {

View File

@@ -25,6 +25,7 @@ import static android.app.StatusBarManager.WindowVisibleState;
import static android.app.StatusBarManager.windowStateToString;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
import static com.android.systemui.Dependency.BG_HANDLER;
import static com.android.systemui.Dependency.MAIN_HANDLER;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
@@ -1076,7 +1077,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mLockscreenUserManager, shadeController, mKeyguardMonitor,
mNotificationInterruptionStateProvider, mMetricsLogger,
new LockPatternUtils(mContext), Dependency.get(MAIN_HANDLER),
mActivityIntentHelper);
Dependency.get(BG_HANDLER), mActivityIntentHelper, mBubbleController);
mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);

View File

@@ -48,6 +48,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
@@ -98,7 +99,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
private final CommandQueue mCommandQueue;
private final IDreamManager mDreamManager;
private final Handler mMainThreadHandler;
private final Handler mBackgroundHandler;
private final ActivityIntentHelper mActivityIntentHelper;
private final BubbleController mBubbleController;
private boolean mIsCollapsingToShowActivityOverLockscreen;
@@ -125,7 +128,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
MetricsLogger metricsLogger,
LockPatternUtils lockPatternUtils,
Handler mainThreadHandler,
ActivityIntentHelper activityIntentHelper) {
Handler backgroundHandler,
ActivityIntentHelper activityIntentHelper,
BubbleController bubbleController) {
mContext = context;
mNotificationPanel = panel;
mPresenter = presenter;
@@ -147,6 +152,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mAssistManager = assistManager;
mGroupManager = groupManager;
mLockPatternUtils = lockPatternUtils;
mBackgroundHandler = backgroundHandler;
mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@Override
public void onPendingEntryAdded(NotificationEntry entry) {
@@ -156,6 +162,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mStatusBarRemoteInputCallback = remoteInputCallback;
mMainThreadHandler = mainThreadHandler;
mActivityIntentHelper = activityIntentHelper;
mBubbleController = bubbleController;
}
/**
@@ -178,14 +185,24 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
final PendingIntent intent = notification.contentIntent != null
? notification.contentIntent
: notification.fullScreenIntent;
final boolean isBubble = row.getEntry().isBubble();
// This code path is now executed for notification without a contentIntent.
// The only valid case is Bubble notifications. Guard against other cases
// entering here.
if (intent == null && !isBubble) {
Log.e(TAG, "onNotificationClicked called for non-clickable notification!");
return;
}
final String notificationKey = sbn.getKey();
boolean isActivityIntent = intent.isActivity();
boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
final boolean afterKeyguardGone = isActivityIntent
&& mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
mLockscreenUserManager.getCurrentUserId());
final boolean wasOccluded = mShadeController.isOccluded();
boolean showOverLockscreen = mKeyguardMonitor.isShowing()
boolean showOverLockscreen = mKeyguardMonitor.isShowing() && intent != null
&& mActivityIntentHelper.wouldShowOverLockscreen(intent.getIntent(),
mLockscreenUserManager.getCurrentUserId());
ActivityStarter.OnDismissAction postKeyguardAction =
@@ -244,9 +261,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mShadeController.addAfterKeyguardGoneRunnable(runnable);
mShadeController.collapsePanel();
} else {
new Thread(runnable).start();
mBackgroundHandler.postAtFrontOfQueue(runnable);
}
return !mNotificationPanel.isFullyCollapsed();
}
@@ -287,6 +303,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
}
Intent fillInIntent = null;
NotificationEntry entry = row.getEntry();
final boolean isBubble = entry.isBubble();
CharSequence remoteInputText = null;
if (!TextUtils.isEmpty(entry.remoteInputText)) {
remoteInputText = entry.remoteInputText;
@@ -295,8 +312,12 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT,
remoteInputText.toString());
}
startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent);
if (isActivityIntent) {
if (isBubble) {
expandBubbleStackOnMainThread(notificationKey);
} else {
startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent);
}
if (isActivityIntent || isBubble) {
mAssistManager.hideAssist();
}
if (shouldCollapse()) {
@@ -316,18 +337,29 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
} catch (RemoteException ex) {
// system process is dead if we're here.
}
if (parentToCancelFinal != null) {
removeNotification(parentToCancelFinal);
}
if (shouldAutoCancel(sbn)
|| mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
notificationKey)) {
// Automatically remove all notifications that we may have kept around longer
removeNotification(sbn);
if (!isBubble) {
if (parentToCancelFinal != null) {
removeNotification(parentToCancelFinal);
}
if (shouldAutoCancel(sbn)
|| mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
notificationKey)) {
// Automatically remove all notifications that we may have kept around longer
removeNotification(sbn);
}
}
mIsCollapsingToShowActivityOverLockscreen = false;
}
private void expandBubbleStackOnMainThread(String notificationKey) {
if (Looper.getMainLooper().isCurrentThread()) {
mBubbleController.expandStackAndSelectBubble(notificationKey);
} else {
mMainThreadHandler.post(
() -> mBubbleController.expandStackAndSelectBubble(notificationKey));
}
}
private void startNotificationIntent(PendingIntent intent, Intent fillInIntent,
ExpandableNotificationRow row, boolean wasOccluded, boolean isActivityIntent) {
RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row,

View File

@@ -437,6 +437,22 @@ public class BubbleControllerTest extends SysuiTestCase {
mContext.unregisterReceiver(receiver);
}
@Test
public void testExpandStackAndSelectBubble_removedFirst() {
final String key = mRow.getEntry().key;
mEntryListener.onPendingEntryAdded(mRow.getEntry());
mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
assertTrue(mRow.getEntry().isBubble());
// Simulate notification cancellation.
mEntryListener.onEntryRemoved(mRow.getEntry(), null /* notificationVisibility (unused) */,
false /* removedbyUser */);
mBubbleController.expandStackAndSelectBubble(key);
}
@Test
public void testMarkNewNotificationAsBubble() {
mEntryListener.onPendingEntryAdded(mRow.getEntry());

View File

@@ -149,6 +149,16 @@ public class NotificationTestHelper {
return createRow(PKG, UID, USER_HANDLE, false /* isGroupSummary */, groupkey);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*/
public ExpandableNotificationRow createBubble()
throws Exception {
Notification n = createNotification(false /* isGroupSummary */,
null /* groupKey */, makeBubbleMetadata(null));
return generateRow(n, PKG, UID, USER_HANDLE, 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
}
/**
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
*

View File

@@ -24,7 +24,10 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.app.KeyguardManager;
@@ -49,6 +52,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
@@ -101,22 +105,23 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
private KeyguardMonitor mKeyguardMonitor;
@Mock
private Handler mHandler;
@Mock
private BubbleController mBubbleController;
@Mock
private ActivityIntentHelper mActivityIntentHelper;
@Mock
private PendingIntent mContentIntent;
@Mock
private Intent mContentIntentInner;
@Mock
private NotificationData mNotificationData;
@Mock
private NotificationEntry mNotificationEntry;
@Mock
private NotificationEntry mBubbleEntry;
private NotificationActivityStarter mNotificationActivityStarter;
private NotificationTestHelper mNotificationTestHelper;
ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
private final Answer<Void> mCallOnDismiss = answerVoid(
(ActivityStarter.OnDismissAction dismissAction, Runnable cancel,
@@ -129,14 +134,32 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController);
when(mEntryManager.getNotificationData()).thenReturn(mNotificationData);
mActiveNotifications = new ArrayList<>();
mActiveNotifications.add(mNotificationEntry);
mActiveNotifications.add(mBubbleEntry);
when(mNotificationData.getActiveNotifications()).thenReturn(mActiveNotifications);
when(mNotificationEntry.getRow()).thenReturn(mNotificationRow);
when(mContentIntent.isActivity()).thenReturn(true);
when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
when(mContentIntent.getIntent()).thenReturn(mContentIntentInner);
mNotificationTestHelper = new NotificationTestHelper(mContext);
// Create standard notification with contentIntent
mNotificationRow = mNotificationTestHelper.createRow();
StatusBarNotification sbn = mNotificationRow.getStatusBarNotification();
sbn.getNotification().contentIntent = mContentIntent;
sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
// Create bubble notification row with contentIntent
mBubbleNotificationRow = mNotificationTestHelper.createBubble();
StatusBarNotification bubbleSbn = mBubbleNotificationRow.getStatusBarNotification();
bubbleSbn.getNotification().contentIntent = mContentIntent;
bubbleSbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
// Do what BubbleController's NotificationEntryListener#onPendingEntryAdded does:
mBubbleNotificationRow.getEntry().setIsBubble(true);
mBubbleNotificationRow.getEntry().setShowInShadeWhenBubble(true);
mActiveNotifications = new ArrayList<>();
mActiveNotifications.add(mNotificationRow.getEntry());
mActiveNotifications.add(mBubbleNotificationRow.getEntry());
when(mNotificationData.getActiveNotifications()).thenReturn(mActiveNotifications);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
mNotificationActivityStarter = new StatusBarNotificationActivityStarter(getContext(),
mock(CommandQueue.class), mAssistManager, mock(NotificationPanelView.class),
@@ -147,16 +170,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
mock(StatusBarRemoteInputCallback.class), mock(NotificationGroupManager.class),
mock(NotificationLockscreenUserManager.class), mShadeController, mKeyguardMonitor,
mock(NotificationInterruptionStateProvider.class), mock(MetricsLogger.class),
mock(LockPatternUtils.class), mHandler, mActivityIntentHelper);
when(mContentIntent.isActivity()).thenReturn(true);
when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
// SBNActivityStarter expects contentIntent or fullScreenIntent to be set
mNotificationRow.getEntry().notification.getNotification().contentIntent = mContentIntent;
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
mock(LockPatternUtils.class), mHandler, mHandler, mActivityIntentHelper,
mBubbleController);
// set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute(
@@ -173,33 +188,26 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
// set up Handler to synchronously invoke the Runnable arg
doAnswer(answerVoid(Runnable::run))
.when(mHandler).post(any(Runnable.class));
doAnswer(answerVoid(Runnable::run))
.when(mHandler).postAtFrontOfQueue(any(Runnable.class));
}
@Test
public void testOnNotificationClicked_whileKeyguardVisible()
public void testOnNotificationClicked_keyGuardShowing()
throws PendingIntent.CanceledException, RemoteException {
// Given
StatusBarNotification sbn = mNotificationRow.getStatusBarNotification();
sbn.getNotification().contentIntent = mContentIntent;
sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
when(mKeyguardMonitor.isShowing()).thenReturn(true);
when(mShadeController.isOccluded()).thenReturn(true);
when(mContentIntent.isActivity()).thenReturn(true);
when(mActivityIntentHelper.wouldShowOverLockscreen(any(Intent.class), anyInt()))
.thenReturn(false);
when(mActivityIntentHelper.wouldLaunchResolverActivity(any(Intent.class), anyInt()))
.thenReturn(false);
StatusBarNotification statusBarNotification = mNotificationRow.getEntry().notification;
statusBarNotification.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
// When
mNotificationActivityStarter.onNotificationClicked(statusBarNotification,
mNotificationRow);
mNotificationActivityStarter.onNotificationClicked(sbn, mNotificationRow);
// Then
verify(mActivityStarter).dismissKeyguardThenExecute(
any(ActivityStarter.OnDismissAction.class),
any() /* cancel */,
anyBoolean() /* afterKeyguardGone */);
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mContentIntent).sendAndReturnResult(
@@ -214,9 +222,100 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
verify(mAssistManager).hideAssist();
verify(mStatusBarService).onNotificationClick(
eq(mNotificationRow.getEntry().key), any(NotificationVisibility.class));
eq(sbn.getKey()), any(NotificationVisibility.class));
// Notification is removed due to FLAG_AUTO_CANCEL
verify(mEntryManager).performRemoveNotification(eq(statusBarNotification));
verify(mEntryManager).performRemoveNotification(eq(sbn));
}
@Test
public void testOnNotificationClicked_bubble_noContentIntent_noKeyGuard()
throws RemoteException {
StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification();
// Given
sbn.getNotification().contentIntent = null;
// When
mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
// Then
verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
// This is called regardless, and simply short circuits when there is nothing to do.
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mStatusBarService).onNotificationClick(
eq(sbn.getKey()), any(NotificationVisibility.class));
// The content intent should NOT be sent on click.
verifyZeroInteractions(mContentIntent);
// Notification should not be cancelled.
verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
}
@Test
public void testOnNotificationClicked_bubble_noContentIntent_keyGuardShowing()
throws RemoteException {
StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification();
// Given
sbn.getNotification().contentIntent = null;
when(mKeyguardMonitor.isShowing()).thenReturn(true);
when(mShadeController.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
// Then
verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mStatusBarService).onNotificationClick(
eq(sbn.getKey()), any(NotificationVisibility.class));
// The content intent should NOT be sent on click.
verifyZeroInteractions(mContentIntent);
// Notification should not be cancelled.
verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
}
@Test
public void testOnNotificationClicked_bubble_withContentIntent_keyGuardShowing()
throws RemoteException {
StatusBarNotification sbn = mBubbleNotificationRow.getStatusBarNotification();
// Given
sbn.getNotification().contentIntent = mContentIntent;
when(mKeyguardMonitor.isShowing()).thenReturn(true);
when(mShadeController.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
// Then
verify(mBubbleController).expandStackAndSelectBubble(eq(sbn.getKey()));
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mStatusBarService).onNotificationClick(
eq(sbn.getKey()), any(NotificationVisibility.class));
// The content intent should NOT be sent on click.
verify(mContentIntent).getIntent();
verify(mContentIntent).isActivity();
verifyNoMoreInteractions(mContentIntent);
// Notification should not be cancelled.
verify(mEntryManager, never()).performRemoveNotification(eq(sbn));
}
}