blob: ecea14c6a522460e9e2c0a180943627509a6ec9d [file] [log] [blame]
/*
* Copyright (C) 2019 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.phone;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static org.mockito.AdditionalAnswers.answerVoid;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
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;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.dreams.IDreamManager;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationClickNotifier;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.wmshell.BubblesManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
@Mock
private AssistManager mAssistManager;
@Mock
private NotificationEntryManager mEntryManager;
@Mock
private ActivityStarter mActivityStarter;
@Mock
private NotificationClickNotifier mClickNotifier;
@Mock
private StatusBarStateController mStatusBarStateController;
@Mock
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock
private NotificationRemoteInputManager mRemoteInputManager;
@Mock
private CentralSurfaces mCentralSurfaces;
@Mock
private KeyguardStateController mKeyguardStateController;
@Mock
private NotificationInterruptStateProvider mNotificationInterruptStateProvider;
@Mock
private Handler mHandler;
@Mock
private BubblesManager mBubblesManager;
@Mock
private ShadeControllerImpl mShadeController;
@Mock
private NotifPipelineFlags mNotifPipelineFlags;
@Mock
private NotifPipeline mNotifPipeline;
@Mock
private NotificationVisibilityProvider mVisibilityProvider;
@Mock
private ActivityIntentHelper mActivityIntentHelper;
@Mock
private PendingIntent mContentIntent;
@Mock
private Intent mContentIntentInner;
@Mock
private OnUserInteractionCallback mOnUserInteractionCallback;
@Mock
private Runnable mFutureDismissalRunnable;
@Mock
private StatusBarNotificationActivityStarter mNotificationActivityStarter;
@Mock
private ActivityLaunchAnimator mActivityLaunchAnimator;
@Mock
private InteractionJankMonitor mJankMonitor;
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private NotificationTestHelper mNotificationTestHelper;
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
private final Answer<Void> mCallOnDismiss = answerVoid(
(OnDismissAction dismissAction, Runnable cancel,
Boolean afterKeyguardGone) -> dismissAction.onDismiss());
private ArrayList<NotificationEntry> mActiveNotifications;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mContentIntent.isActivity()).thenReturn(true);
when(mContentIntent.getCreatorUserHandle()).thenReturn(UserHandle.of(1));
when(mContentIntent.getIntent()).thenReturn(mContentIntentInner);
mNotificationTestHelper = new NotificationTestHelper(
mContext,
mDependency,
TestableLooper.get(this));
// Create standard notification with contentIntent
mNotificationRow = mNotificationTestHelper.createRow();
StatusBarNotification sbn = mNotificationRow.getEntry().getSbn();
sbn.getNotification().contentIntent = mContentIntent;
sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
// Create bubble notification row with contentIntent
mBubbleNotificationRow = mNotificationTestHelper.createBubble();
StatusBarNotification bubbleSbn = mBubbleNotificationRow.getEntry().getSbn();
bubbleSbn.getNotification().contentIntent = mContentIntent;
bubbleSbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
mActiveNotifications = new ArrayList<>();
mActiveNotifications.add(mNotificationRow.getEntry());
mActiveNotifications.add(mBubbleNotificationRow.getEntry());
when(mEntryManager.getVisibleNotifications()).thenReturn(mActiveNotifications);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
when(mOnUserInteractionCallback.registerFutureDismissal(eq(mNotificationRow.getEntry()),
anyInt())).thenReturn(mFutureDismissalRunnable);
when(mVisibilityProvider.obtain(anyString(), anyBoolean()))
.thenAnswer(invocation -> NotificationVisibility.obtain(
invocation.getArgument(0), 0, 1, false));
when(mVisibilityProvider.obtain(any(NotificationEntry.class), anyBoolean()))
.thenAnswer(invocation -> NotificationVisibility.obtain(
invocation.<NotificationEntry>getArgument(0).getKey(), 0, 1, false));
HeadsUpManagerPhone headsUpManager = mock(HeadsUpManagerPhone.class);
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
new NotificationLaunchAnimatorControllerProvider(
mock(NotificationShadeWindowViewController.class), mock(
NotificationListContainer.class),
headsUpManager,
mJankMonitor);
mNotificationActivityStarter =
new StatusBarNotificationActivityStarter(
getContext(),
mock(CommandQueue.class),
mHandler,
mUiBgExecutor,
mEntryManager,
mNotifPipeline,
mVisibilityProvider,
headsUpManager,
mActivityStarter,
mClickNotifier,
mock(StatusBarStateController.class),
mStatusBarKeyguardViewManager,
mock(KeyguardManager.class),
mock(IDreamManager.class),
Optional.of(mBubblesManager),
() -> mAssistManager,
mRemoteInputManager,
mock(NotificationGroupManagerLegacy.class),
mock(NotificationLockscreenUserManager.class),
mShadeController,
mKeyguardStateController,
mNotificationInterruptStateProvider,
mock(LockPatternUtils.class),
mock(StatusBarRemoteInputCallback.class),
mActivityIntentHelper,
mNotifPipelineFlags,
mock(MetricsLogger.class),
mock(StatusBarNotificationActivityStarterLogger.class),
mOnUserInteractionCallback,
mCentralSurfaces,
mock(NotificationPresenter.class),
mock(NotificationPanelViewController.class),
mActivityLaunchAnimator,
notificationAnimationProvider
);
// set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute(
any(OnDismissAction.class), any(), anyBoolean());
// set up addAfterKeyguardGoneRunnable to synchronously invoke the Runnable arg
doAnswer(answerVoid(Runnable::run))
.when(mStatusBarKeyguardViewManager)
.addAfterKeyguardGoneRunnable(any(Runnable.class));
// set up addPostCollapseAction to synchronously invoke the Runnable arg
doAnswer(answerVoid(Runnable::run))
.when(mShadeController).addPostCollapseAction(any(Runnable.class));
// set up Handler to synchronously invoke the Runnable arg
doAnswer(answerVoid(Runnable::run))
.when(mHandler).post(any(Runnable.class));
}
@Test
public void testOnNotificationClicked_keyGuardShowing()
throws PendingIntent.CanceledException, RemoteException {
// To get the order right, collect posted runnables and run them later
List<Runnable> runnables = new ArrayList<>();
doAnswer(answerVoid(r -> runnables.add((Runnable) r)))
.when(mHandler).post(any(Runnable.class));
// Given
NotificationEntry entry = mNotificationRow.getEntry();
Notification notification = entry.getSbn().getNotification();
notification.contentIntent = mContentIntent;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mCentralSurfaces.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mNotificationRow);
// Run the collected runnables in fifo order, the way post() really does.
while (!runnables.isEmpty()) runnables.remove(0).run();
// Then
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(any(),
eq(false) /* animate */, any(), any());
verify(mAssistManager).hideAssist();
InOrder orderVerifier = Mockito.inOrder(mClickNotifier, mOnUserInteractionCallback,
mFutureDismissalRunnable);
// Notification calls dismiss callback to remove notification due to FLAG_AUTO_CANCEL
orderVerifier.verify(mOnUserInteractionCallback)
.registerFutureDismissal(eq(entry), eq(REASON_CLICK));
orderVerifier.verify(mClickNotifier).onNotificationClick(
eq(entry.getKey()), any(NotificationVisibility.class));
orderVerifier.verify(mFutureDismissalRunnable).run();
}
@Test
public void testOnNotificationClicked_bubble_noContentIntent_noKeyGuard()
throws RemoteException {
NotificationEntry entry = mBubbleNotificationRow.getEntry();
StatusBarNotification sbn = entry.getSbn();
// Given
sbn.getNotification().contentIntent = null;
// When
mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
// Then
verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry()));
// This is called regardless, and simply short circuits when there is nothing to do.
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mClickNotifier).onNotificationClick(
eq(entry.getKey()), any(NotificationVisibility.class));
// The content intent should NOT be sent on click.
verifyZeroInteractions(mContentIntent);
// Notification should not be cancelled.
verify(mOnUserInteractionCallback, never())
.registerFutureDismissal(eq(mNotificationRow.getEntry()), anyInt());
verify(mFutureDismissalRunnable, never()).run();
}
@Test
public void testOnNotificationClicked_bubble_noContentIntent_keyGuardShowing()
throws RemoteException {
NotificationEntry entry = mBubbleNotificationRow.getEntry();
StatusBarNotification sbn = entry.getSbn();
// Given
sbn.getNotification().contentIntent = null;
when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mCentralSurfaces.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
// Then
verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry()));
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mClickNotifier).onNotificationClick(
eq(entry.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), any(), anyInt());
}
@Test
public void testOnNotificationClicked_bubble_withContentIntent_keyGuardShowing()
throws RemoteException {
NotificationEntry entry = mBubbleNotificationRow.getEntry();
StatusBarNotification sbn = entry.getSbn();
// Given
sbn.getNotification().contentIntent = mContentIntent;
when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mCentralSurfaces.isOccluded()).thenReturn(true);
// When
mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
// Then
verify(mBubblesManager).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry());
verify(mShadeController, atLeastOnce()).collapsePanel();
verify(mAssistManager).hideAssist();
verify(mClickNotifier).onNotificationClick(
eq(entry.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), any(), anyInt());
}
@Test
public void testOnFullScreenIntentWhenDozing_wakeUpDevice() {
// GIVEN entry that can has a full screen intent that can show
Notification.Builder nb = new Notification.Builder(mContext, "a")
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setFullScreenIntent(mock(PendingIntent.class), true);
StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0,
"tag" + System.currentTimeMillis(), 0, 0,
nb.build(), new UserHandle(0), null, 0);
NotificationEntry entry = mock(NotificationEntry.class);
when(entry.getImportance()).thenReturn(NotificationManager.IMPORTANCE_HIGH);
when(entry.getSbn()).thenReturn(sbn);
when(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(eq(entry)))
.thenReturn(true);
// WHEN
mNotificationActivityStarter.handleFullScreenIntent(entry);
// THEN display should try wake up for the full screen intent
verify(mCentralSurfaces).wakeUpForFullScreenIntent();
}
}