/* * 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.wmshell; import static android.app.Notification.FLAG_BUBBLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED; import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Handler; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Pair; import android.util.SparseArray; import android.view.IWindowManager; import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.BubbleIconFactory; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.ShadeWindowLogger; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.NotificationEntryHelper; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; import com.android.wm.shell.bubbles.BubbleEntry; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.bubbles.BubbleViewInfoTask; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.StackEducationViewKt; import com.android.wm.shell.bubbles.properties.BubbleProperties; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubblesTest extends SysuiTestCase { @Mock private CommonNotifCollection mCommonNotifCollection; @Mock private BubblesManager.NotifCallback mNotifCallback; @Mock private WindowManager mWindowManager; @Mock private IActivityManager mActivityManager; @Mock private DozeParameters mDozeParameters; @Mock private ConfigurationController mConfigurationController; @Mock private ZenModeController mZenModeController; @Mock private ZenModeConfig mZenModeConfig; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private KeyguardViewMediator mKeyguardViewMediator; @Mock private KeyguardBypassController mKeyguardBypassController; @Mock private FloatingContentCoordinator mFloatingContentCoordinator; @Mock private BubbleDataRepository mDataRepository; @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private AuthController mAuthController; @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; private boolean mSysUiStateBubblesManageMenuExpanded; @Captor private ArgumentCaptor mNotifListenerCaptor; @Captor private ArgumentCaptor> mBubbleListCaptor; @Captor private ArgumentCaptor mFilterArgumentCaptor; @Captor private ArgumentCaptor mBroadcastReceiverArgumentCaptor; @Captor private ArgumentCaptor mKeyguardStateControllerCallbackCaptor; private BubblesManager mBubblesManager; private TestableBubbleController mBubbleController; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; private NotifCollectionListener mEntryListener; private NotificationTestHelper mNotificationTestHelper; private NotificationEntry mRow; private NotificationEntry mRow2; private ExpandableNotificationRow mNonBubbleNotifRow; private BubbleEntry mBubbleEntry; private BubbleEntry mBubbleEntry2; private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; private Intent mAppBubbleIntent; @Mock private ShellInit mShellInit; @Mock private ShellCommandHandler mShellCommandHandler; @Mock private ShellController mShellController; @Mock private Bubbles.BubbleExpandListener mBubbleExpandListener; @Mock private PendingIntent mDeleteIntent; @Mock private SysuiColorExtractor mColorExtractor; @Mock ColorExtractor.GradientColors mGradientColors; @Mock private ShadeController mShadeController; @Mock private NotifPipeline mNotifPipeline; @Mock private DumpManager mDumpManager; @Mock private IStatusBarService mStatusBarService; @Mock private IDreamManager mIDreamManager; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private LauncherApps mLauncherApps; @Mock private WindowManagerShellWrapper mWindowManagerShellWrapper; @Mock private BubbleLogger mBubbleLogger; @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private KeyguardStateController mKeyguardStateController; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock Transitions mTransitions; @Mock private Optional mOneHandedOptional; @Mock private UserManager mUserManager; @Mock private ShadeWindowLogger mShadeWindowLogger; @Mock private NotifPipelineFlags mNotifPipelineFlags; @Mock private Icon mAppBubbleIcon; private ShellTaskOrganizer mShellTaskOrganizer; private TaskViewTransitions mTaskViewTransitions; private TestableBubblePositioner mPositioner; private BubbleData mBubbleData; private TestableLooper mTestableLooper; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); private UserHandle mUser0; private FakeBubbleProperties mBubbleProperties; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); if (Transitions.ENABLE_SHELL_TRANSITIONS) { doReturn(true).when(mTransitions).isRegistered(); } mTaskViewTransitions = new TaskViewTransitions(mTransitions); mTestableLooper = TestableLooper.get(this); // For the purposes of this test, just run everything synchronously ShellExecutor syncExecutor = new SyncExecutor(); mUser0 = createUserHandle(/* userId= */ 0); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); when(mNotificationShadeWindowView.getViewTreeObserver()) .thenReturn(mock(ViewTreeObserver.class)); mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext, mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, syncExecutor, mColorExtractor, mDumpManager, mKeyguardStateController, mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager, mShadeWindowLogger); mNotificationShadeWindowController.setWindowRootView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); mAppBubbleIntent.setPackage(mContext.getPackageName()); mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); mSysUiState = new SysUiState(mDisplayTracker); mSysUiState.addCallback(sysUiFlags -> { mSysUiStateBubblesManageMenuExpanded = (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0; mSysUiStateBubblesExpanded = (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0; }); mPositioner = new TestableBubblePositioner(mContext, mWindowManager); mPositioner.setMaxBubbles(5); mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor); when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn( Collections.singletonList(mock(UserInfo.class))); TestableNotificationInterruptStateProviderImpl interruptionStateProvider = new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(), mock(PowerManager.class), mock(AmbientDisplayConfiguration.class), mock(StatusBarStateController.class), mock(KeyguardStateController.class), mock(BatteryController.class), mock(HeadsUpManager.class), mock(NotificationInterruptLogger.class), mock(Handler.class), mock(NotifPipelineFlags.class), mock(KeyguardNotificationVisibilityProvider.class), mock(UiEventLogger.class), mock(UserTracker.class) ); mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class), mock(ShellCommandHandler.class), null, Optional.empty(), Optional.empty(), syncExecutor); mBubbleProperties = new FakeBubbleProperties(); mBubbleController = new TestableBubbleController( mContext, mShellInit, mShellCommandHandler, mShellController, mBubbleData, mFloatingContentCoordinator, mDataRepository, mStatusBarService, mWindowManager, mWindowManagerShellWrapper, mUserManager, mLauncherApps, mBubbleLogger, mTaskStackListener, mShellTaskOrganizer, mPositioner, mock(DisplayController.class), mOneHandedOptional, Optional.of(mock(DragAndDropController.class)), syncExecutor, mock(Handler.class), mTaskViewTransitions, mock(SyncTransactionQueue.class), mock(IWindowManager.class), mBubbleProperties); mBubbleController.setExpandListener(mBubbleExpandListener); spyOn(mBubbleController); mBubblesManager = new BubblesManager( mContext, mBubbleController.asBubbles(), mNotificationShadeWindowController, mKeyguardStateController, mShadeController, mStatusBarService, mock(INotificationManager.class), mIDreamManager, mVisibilityProvider, new NotificationInterruptStateProviderWrapper(interruptionStateProvider), mZenModeController, mLockscreenUserManager, mCommonNotifCollection, mNotifPipeline, mSysUiState, mFeatureFlags, mNotifPipelineFlags, syncExecutor); mBubblesManager.addNotifCallback(mNotifCallback); // Need notifications for bubbles mNotificationTestHelper = new NotificationTestHelper( mContext, mDependency, TestableLooper.get(this), mFeatureFlags); mRow = mNotificationTestHelper.createBubble(mDeleteIntent); mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent); mNonBubbleNotifRow = mNotificationTestHelper.createRow(); mBubbleEntry = mBubblesManager.notifToBubbleEntry(mRow); mBubbleEntry2 = mBubblesManager.notifToBubbleEntry(mRow2); UserHandle handle = mock(UserHandle.class); when(handle.getIdentifier()).thenReturn(11); mBubbleEntryUser11 = mBubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); mBubbleEntry2User11 = mBubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); // Get a reference to the BubbleController's entry listener verify(mNotifPipeline, atLeastOnce()) .addCollectionListener(mNotifListenerCaptor.capture()); mEntryListener = mNotifListenerCaptor.getValue(); // Get a reference to KeyguardStateController.Callback verify(mKeyguardStateController, atLeastOnce()) .addCallback(mKeyguardStateControllerCallbackCaptor.capture()); } @After public void tearDown() { ArrayList bubbles = new ArrayList<>(mBubbleData.getBubbles()); for (int i = 0; i < bubbles.size(); i++) { mBubbleController.removeBubble(bubbles.get(i).getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE); } } @Test public void dreamingHidesBubbles() throws RemoteException { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.VISIBLE); when(mIDreamManager.isDreamingOrInPreview()).thenReturn(true); // dreaming is happening when(mKeyguardStateController.isShowing()).thenReturn(false); // device is unlocked KeyguardStateController.Callback callback = mKeyguardStateControllerCallbackCaptor.getValue(); callback.onKeyguardShowingChanged(); assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.INVISIBLE); } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), any()); } @Test public void instantiateController_registerConfigChangeListener() { verify(mShellController, times(1)).addConfigurationChangeListener(any()); } @Test public void testAddBubble() { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testHasBubbles() { assertFalse(mBubbleController.hasBubbles()); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testRemoveBubble() { mBubbleController.updateBubble(mBubbleEntry); assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey())); assertTrue(mBubbleController.hasBubbles()); verify(mNotifCallback, times(1)).invalidateNotifications(anyString()); mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey())); verify(mNotifCallback, times(2)).invalidateNotifications(anyString()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testRemoveBubble_withDismissedNotif_inOverflow() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Make it look like dismissed notif mBubbleData.getBubbleInStackWithKey(mRow.getKey()).setSuppressNotification(true); // Now remove the bubble mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); assertTrue(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())); // We don't remove the notification since the bubble is still in overflow. verify(mNotifCallback, never()).removeNotification(eq(mRow), any(), anyInt()); assertFalse(mBubbleController.hasBubbles()); } @Test public void testRemoveBubble_withDismissedNotif_notInOverflow() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); when(mCommonNotifCollection.getEntry(mRow.getKey())).thenReturn(mRow); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Make it look like dismissed notif mBubbleData.getBubbleInStackWithKey(mRow.getKey()).setSuppressNotification(true); // Now remove the bubble mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); assertFalse(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())); // Since the notif is dismissed and not in overflow, once the bubble is removed, // removeNotification gets called to really remove the notif verify(mNotifCallback, times(1)).removeNotification(eq(mRow), any(), anyInt()); assertFalse(mBubbleController.hasBubbles()); } @Test public void testDismissStack() { mBubbleController.updateBubble(mBubbleEntry); verify(mNotifCallback, times(1)).invalidateNotifications(anyString()); assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey())); mBubbleController.updateBubble(mBubbleEntry2); verify(mNotifCallback, times(2)).invalidateNotifications(anyString()); assertNotNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey())); assertTrue(mBubbleController.hasBubbles()); mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); verify(mNotifCallback, times(3)).invalidateNotifications(anyString()); assertNull(mBubbleData.getBubbleInStackWithKey(mRow.getKey())); assertNull(mBubbleData.getBubbleInStackWithKey(mRow2.getKey())); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testExpandCollapseStack() { assertStackCollapsed(); // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Expand the stack mBubbleData.setExpanded(true); assertStackExpanded(); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey()); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Make sure the notif is suppressed assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Collapse mBubbleController.collapseStack(); verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey()); assertStackCollapsed(); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testCollapseAfterChangingExpandedBubble() { // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryAdded(mRow2); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.updateBubble(mBubbleEntry2); // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry2); // Expand BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertStackExpanded(); verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged( true, mRow2.getKey()); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Last added is the one that is expanded assertEquals(mRow2.getKey(), mBubbleData.getSelectedBubble().getKey()); assertBubbleNotificationSuppressedFromShade(mBubbleEntry2); // Switch which bubble is expanded mBubbleData.setSelectedBubble(mBubbleData.getBubbleInStackWithKey( mRow.getKey())); mBubbleData.setExpanded(true); assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey()); assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // collapse for previous bubble verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged( false, mRow2.getKey()); // expand for selected bubble verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged( true, mRow.getKey()); // Collapse mBubbleController.collapseStack(); assertStackCollapsed(); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testExpansionRemovesShowInShadeAndDot() { // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); mTestableLooper.processAllMessages(); assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); // Expand mBubbleData.setExpanded(true); assertStackExpanded(); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey()); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Notif is suppressed after expansion assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Notif shouldn't show dot after expansion assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); } @Test public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() { // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // We should have bubbles & their notifs should not be suppressed assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); mTestableLooper.processAllMessages(); assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); // Expand mBubbleData.setExpanded(true); assertStackExpanded(); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey()); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Notif is suppressed after expansion assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Notif shouldn't show dot after expansion assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); // Send update mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); // Nothing should have changed // Notif is suppressed after expansion assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Notif shouldn't show dot after expansion assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); } @Test public void testRemoveLastExpanded_collapses() { // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryAdded(mRow2); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.updateBubble(mBubbleEntry2); // Expand BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); assertStackExpanded(); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getKey()); // Last added is the one that is expanded assertEquals(mRow2.getKey(), mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey()); assertBubbleNotificationSuppressedFromShade(mBubbleEntry2); // Dismiss currently expanded mBubbleController.removeBubble( mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey(), Bubbles.DISMISS_USER_GESTURE); verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getKey()); // Make sure first bubble is selected assertEquals(mRow.getKey(), mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey()); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey()); // Dismiss that one mBubbleController.removeBubble( mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey(), Bubbles.DISMISS_USER_GESTURE); // We should be collapsed verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey()); assertFalse(mBubbleController.hasBubbles()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testRemoveLastExpandedEmptyOverflow_collapses() { // Mark it as a bubble and add it explicitly mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // Expand BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); assertStackExpanded(); verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getKey()); // Block the bubble so it won't be in the overflow mBubbleController.removeBubble( mBubbleData.getBubbleInStackWithKey( stackView.getExpandedBubble().getKey()).getKey(), Bubbles.DISMISS_BLOCKED); verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey()); // We should be collapsed verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey()); assertFalse(mBubbleController.hasBubbles()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testAutoExpand_fails_noFlag() { assertStackCollapsed(); setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */); // Add the auto expand bubble mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // Expansion shouldn't change verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */, mRow.getKey()); assertStackCollapsed(); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testAutoExpand_succeeds_withFlag() { setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */); // Add the auto expand bubble mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // Expansion should change verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */, mRow.getKey()); assertStackExpanded(); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testSuppressNotif_onInitialNotif() { setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */); // Add the suppress notif bubble mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); // Notif should be suppressed because we were foreground assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Dot + flyout is hidden because notif is suppressed assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testSuppressNotif_onUpdateNotif() { mBubbleController.updateBubble(mBubbleEntry); // Should not be suppressed assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Should show dot assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); // Update to suppress notif setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */); mBubbleController.updateBubble(mBubbleEntry); // Notif should be suppressed assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Dot + flyout is hidden because notif is suppressed assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); assertFalse(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showFlyout()); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testMarkNewNotificationAsShowInShade() { mEntryListener.onEntryAdded(mRow); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); mTestableLooper.processAllMessages(); assertTrue(mBubbleData.getBubbleInStackWithKey(mRow.getKey()).showDot()); } @Test public void testAddNotif_notBubble() { mEntryListener.onEntryAdded(mNonBubbleNotifRow.getEntry()); mEntryListener.onEntryUpdated(mNonBubbleNotifRow.getEntry(), /* fromSystem= */ true); assertThat(mBubbleController.hasBubbles()).isFalse(); } @Test public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException { mBubbleController.updateBubble(mBubbleEntry); mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_AGED); verify(mDeleteIntent, never()).send(); } @Test public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException { mBubbleController.updateBubble(mBubbleEntry); mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); verify(mDeleteIntent, times(1)).send(); } @Test public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException { mBubbleController.updateBubble(mBubbleEntry); mBubbleController.updateBubble(mBubbleEntry2); mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); verify(mDeleteIntent, times(2)).send(); } @Test public void testRemoveBubble_noLongerBubbleAfterUpdate() throws PendingIntent.CanceledException { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); mRow.getSbn().getNotification().flags &= ~FLAG_BUBBLE; NotificationListenerService.Ranking ranking = new RankingBuilder( mRow.getRanking()).setCanBubble(false).build(); mRow.setRanking(ranking); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); assertFalse(mBubbleController.hasBubbles()); verify(mDeleteIntent, never()).send(); } @Test public void testRemoveBubble_entryListenerRemove() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Removes the notification mEntryListener.onEntryRemoved(mRow, REASON_APP_CANCEL); assertFalse(mBubbleController.hasBubbles()); } @Test public void removeBubble_intercepted() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); boolean intercepted = mBubblesManager.handleDismissalInterception(mRow); // Intercept! assertTrue(intercepted); // Should update show in shade state assertBubbleNotificationSuppressedFromShade(mBubbleEntry); } @Test public void removeBubble_dismissIntoOverflow_intercepted() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Dismiss the bubble mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); assertFalse(mBubbleController.hasBubbles()); // Dismiss the notification boolean intercepted = mBubblesManager.handleDismissalInterception(mRow); // Intercept dismissal since bubble is going into overflow assertTrue(intercepted); } @Test public void removeBubble_notIntercepted() { mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Dismiss the bubble mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); assertFalse(mBubbleController.hasBubbles()); // Dismiss the notification boolean intercepted = mBubblesManager.handleDismissalInterception(mRow); // Not a bubble anymore so we don't intercept dismissal. assertFalse(intercepted); } @Test public void testNotifyShadeSuppressionChange_notificationDismiss() { mEntryListener.onEntryAdded(mRow); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); mBubblesManager.handleDismissalInterception(mRow); // Should update show in shade state assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @Test public void testNotifyShadeSuppressionChange_bubbleExpanded() { mEntryListener.onEntryAdded(mRow); assertTrue(mBubbleController.hasBubbles()); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); mBubbleData.setExpanded(true); // Once a bubble is expanded the notif is suppressed assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Should notify delegate that shade state changed verify(mBubbleController).onBubbleMetadataFlagChanged( mBubbleData.getBubbleInStackWithKey(mRow.getKey())); } @Test public void testBubbleSummaryDismissal_suppressesSummaryAndBubbleFromShade() throws Exception { // GIVEN a group summary with a bubble child ExpandableNotificationRow groupSummary = mNotificationTestHelper.createGroup(0); ExpandableNotificationRow groupedBubble = mNotificationTestHelper.createBubbleInGroup(); mEntryListener.onEntryAdded(groupedBubble.getEntry()); when(mCommonNotifCollection.getEntry(groupedBubble.getEntry().getKey())) .thenReturn(groupedBubble.getEntry()); groupSummary.addChildNotification(groupedBubble); assertTrue(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getEntry().getKey())); // WHEN the summary is dismissed mBubblesManager.handleDismissalInterception(groupSummary.getEntry()); // THEN the summary and bubbled child are suppressed from the shade assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade( groupedBubble.getEntry().getKey(), groupedBubble.getEntry().getSbn().getGroupKey())); assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade( groupedBubble.getEntry().getKey(), groupedBubble.getEntry().getSbn().getGroupKey())); assertTrue(mBubbleData.isSummarySuppressed(groupSummary.getEntry().getSbn().getGroupKey())); } @Test public void testAppRemovesSummary_removesAllBubbleChildren() throws Exception { // GIVEN a group summary with a bubble child ExpandableNotificationRow groupSummary = mNotificationTestHelper.createGroup(0); ExpandableNotificationRow groupedBubble = mNotificationTestHelper.createBubbleInGroup(); mEntryListener.onEntryAdded(groupedBubble.getEntry()); when(mCommonNotifCollection.getEntry(groupedBubble.getEntry().getKey())) .thenReturn(groupedBubble.getEntry()); groupSummary.addChildNotification(groupedBubble); assertTrue(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getEntry().getKey())); // GIVEN the summary is dismissed mBubblesManager.handleDismissalInterception(groupSummary.getEntry()); // WHEN the summary is cancelled by the app mEntryListener.onEntryRemoved(groupSummary.getEntry(), REASON_APP_CANCEL); // THEN the summary and its children are removed from bubble data assertFalse(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getEntry().getKey())); assertFalse(mBubbleData.isSummarySuppressed( groupSummary.getEntry().getSbn().getGroupKey())); } @Test public void testSummaryDismissalMarksBubblesHiddenFromShadeAndDismissesNonBubbledChildren() throws Exception { // GIVEN a group summary with two (non-bubble) children and one bubble child ExpandableNotificationRow groupSummary = mNotificationTestHelper.createGroup(2); ExpandableNotificationRow groupedBubble = mNotificationTestHelper.createBubbleInGroup(); mEntryListener.onEntryAdded(groupedBubble.getEntry()); when(mCommonNotifCollection.getEntry(groupedBubble.getEntry().getKey())) .thenReturn(groupedBubble.getEntry()); groupSummary.addChildNotification(groupedBubble); // WHEN the summary is dismissed mBubblesManager.handleDismissalInterception(groupSummary.getEntry()); // THEN only the NON-bubble children are dismissed List childrenRows = groupSummary.getAttachedChildren(); verify(mNotifCallback, times(1)).removeNotification( eq(childrenRows.get(0).getEntry()), any(), eq(REASON_GROUP_SUMMARY_CANCELED)); verify(mNotifCallback, times(1)).removeNotification( eq(childrenRows.get(1).getEntry()), any(), eq(REASON_GROUP_SUMMARY_CANCELED)); verify(mNotifCallback, never()).removeNotification(eq(groupedBubble.getEntry()), any(), anyInt()); // THEN the bubble child still exists as a bubble and is suppressed from the shade assertTrue(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getEntry().getKey())); assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade( groupedBubble.getEntry().getKey(), groupedBubble.getEntry().getSbn().getGroupKey())); assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade( groupedBubble.getEntry().getKey(), groupedBubble.getEntry().getSbn().getGroupKey())); // THEN the summary is also suppressed from the shade assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade( groupSummary.getEntry().getKey(), groupSummary.getEntry().getSbn().getGroupKey())); assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade( groupSummary.getEntry().getKey(), groupSummary.getEntry().getSbn().getGroupKey())); } /** * Verifies that when the user changes, the bubbles in the overflow list is cleared. Doesn't * test the loading from the repository which would be a nice thing to add. */ @Test public void testOnUserChanged_overflowState() { int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(); int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier(); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.updateBubble(mBubbleEntry2); assertTrue(mBubbleController.hasBubbles()); mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); // Verify these are in the overflow assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey())).isNotNull(); assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull(); // Switch users switchUser(secondUserId); assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); // Give this user some bubbles mBubbleController.updateBubble(mBubbleEntryUser11); mBubbleController.updateBubble(mBubbleEntry2User11); assertTrue(mBubbleController.hasBubbles()); mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); // Verify these are in the overflow assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntryUser11.getKey())).isNotNull(); assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull(); // Would have loaded bubbles twice because of user switch verify(mDataRepository, times(2)).loadBubbles(anyInt(), anyList(), any()); } @Test public void testOnUserChanged_bubblesRestored() { int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(); int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier(); // Mock current profile when(mLockscreenUserManager.isCurrentProfile(firstUserId)).thenReturn(true); when(mLockscreenUserManager.isCurrentProfile(secondUserId)).thenReturn(false); mBubbleController.updateBubble(mBubbleEntry); assertThat(mBubbleController.hasBubbles()).isTrue(); // We start with 1 bubble assertThat(mBubbleData.getBubbles()).hasSize(1); // Switch to second user switchUser(secondUserId); // Second user has no bubbles assertThat(mBubbleController.hasBubbles()).isFalse(); // Send bubble update for first user, ensure it does not show up mBubbleController.updateBubble(mBubbleEntry2); assertThat(mBubbleController.hasBubbles()).isFalse(); // Start returning notif for first user again when(mCommonNotifCollection.getAllNotifs()).thenReturn(Arrays.asList(mRow, mRow2)); // Switch back to first user switchUser(firstUserId); // Check we now have two bubbles, one previous and one new that came in assertThat(mBubbleController.hasBubbles()).isTrue(); // Now there are 2 bubbles assertThat(mBubbleData.getBubbles()).hasSize(2); } /** * Verifies we only load the overflow data once. */ @Test public void testOverflowLoadedOnce() { // XXX when(mCommonNotifCollection.getEntry(mRow.getKey())).thenReturn(mRow); when(mCommonNotifCollection.getEntry(mRow2.getKey())).thenReturn(mRow2); mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryAdded(mRow2); mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty(); mEntryListener.onEntryRemoved(mRow, REASON_APP_CANCEL); mEntryListener.onEntryRemoved(mRow2, REASON_APP_CANCEL); assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); verify(mDataRepository, times(1)).loadBubbles(anyInt(), anyList(), any()); } /** * Verifies that shortcut deletions triggers that bubble being removed from XML. */ @Test public void testDeleteShortcutsDeletesXml() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createShortcutBubble("shortcutId"); BubbleEntry shortcutBubbleEntry = mBubblesManager.notifToBubbleEntry(row.getEntry()); mBubbleController.updateBubble(shortcutBubbleEntry); mBubbleData.dismissBubbleWithKey(shortcutBubbleEntry.getKey(), Bubbles.DISMISS_SHORTCUT_REMOVED); verify(mDataRepository, atLeastOnce()).removeBubbles(anyInt(), mBubbleListCaptor.capture()); assertThat(mBubbleListCaptor.getValue().get(0).getKey()).isEqualTo( shortcutBubbleEntry.getKey()); } /** * Verifies that the package manager for the user is used when loading info for the bubble. */ @Test public void test_bubbleViewInfoGetPackageForUser() throws Exception { final int workProfileUserId = 10; final UserHandle workUser = new UserHandle(workProfileUserId); final String workPkg = "work.pkg"; final Bubble bubble = createBubble(workProfileUserId, workPkg); assertEquals(workProfileUserId, bubble.getUser().getIdentifier()); final Context context = setUpContextWithPackageManager(workPkg, null /* AppInfo */); when(context.getResources()).thenReturn(mContext.getResources()); final Context userContext = setUpContextWithPackageManager(workPkg, mock(ApplicationInfo.class)); // If things are working correctly, CentralSurfaces.getPackageManagerForUser will call this when(context.createPackageContextAsUser(eq(workPkg), anyInt(), eq(workUser))) .thenReturn(userContext); BubbleViewInfoTask.BubbleViewInfo info = BubbleViewInfoTask.BubbleViewInfo.populate(context, mBubbleController, mBubbleController.getStackView(), new BubbleIconFactory(mContext, mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size), mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size), mContext.getResources().getColor(R.color.important_conversation), mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)), bubble, true /* skipInflation */); verify(userContext, times(1)).getPackageManager(); verify(context, times(1)).createPackageContextAsUser(eq(workPkg), eq(Context.CONTEXT_RESTRICTED), eq(workUser)); assertNotNull(info); } @Test public void testShowManageMenuChangesSysuiState() { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Expand the stack BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertStackExpanded(); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Show the menu stackView.showManageMenu(true); assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */); assertTrue(stackView.isManageMenuSettingsVisible()); assertTrue(stackView.isManageMenuDontBubbleVisible()); } @Test public void testShowManageMenuChangesSysuiState_appBubble() { mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertTrue(mBubbleController.hasBubbles()); // Expand the stack BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertStackExpanded(); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Show the menu stackView.showManageMenu(true); assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */); assertFalse(stackView.isManageMenuSettingsVisible()); assertFalse(stackView.isManageMenuDontBubbleVisible()); } @Test public void testHideManageMenuChangesSysuiState() { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Expand the stack BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertStackExpanded(); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Show the menu stackView.showManageMenu(true); assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */); // Hide the menu stackView.showManageMenu(false); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testCollapseBubbleManageMenuChangesSysuiState() { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Expand the stack BubbleStackView stackView = mBubbleController.getStackView(); mBubbleData.setExpanded(true); assertStackExpanded(); assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */); // Show the menu stackView.showManageMenu(true); assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */); // Collapse the stack mBubbleData.setExpanded(false); assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */); } @Test public void testNotificationChannelModified_channelUpdated_removesOverflowBubble() throws Exception { // Setup ExpandableNotificationRow row = mNotificationTestHelper.createShortcutBubble("shortcutId"); NotificationEntry entry = row.getEntry(); entry.getChannel().setConversationId( row.getEntry().getChannel().getParentChannelId(), "shortcutId"); mBubbleController.updateBubble(mBubblesManager.notifToBubbleEntry(row.getEntry())); assertTrue(mBubbleController.hasBubbles()); // Overflow it mBubbleData.dismissBubbleWithKey(entry.getKey(), Bubbles.DISMISS_USER_GESTURE); assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isTrue(); // Test entry.getChannel().setDeleted(true); mBubbleController.onNotificationChannelModified(entry.getSbn().getPackageName(), entry.getSbn().getUser(), entry.getChannel(), NOTIFICATION_CHANNEL_OR_GROUP_UPDATED); assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isFalse(); } @Test public void testNotificationChannelModified_channelDeleted_removesOverflowBubble() throws Exception { // Setup ExpandableNotificationRow row = mNotificationTestHelper.createShortcutBubble("shortcutId"); NotificationEntry entry = row.getEntry(); entry.getChannel().setConversationId( row.getEntry().getChannel().getParentChannelId(), "shortcutId"); mBubbleController.updateBubble(mBubblesManager.notifToBubbleEntry(row.getEntry())); assertTrue(mBubbleController.hasBubbles()); // Overflow it mBubbleData.dismissBubbleWithKey(entry.getKey(), Bubbles.DISMISS_USER_GESTURE); assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isTrue(); // Test entry.getChannel().setDeleted(true); mBubbleController.onNotificationChannelModified(entry.getSbn().getPackageName(), entry.getSbn().getUser(), entry.getChannel(), NOTIFICATION_CHANNEL_OR_GROUP_DELETED); assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isFalse(); } @Test public void testStackViewOnBackPressed_updatesBubbleDataExpandState() { mBubbleController.updateBubble(mBubbleEntry); // Expand the stack mBubbleData.setExpanded(true); assertStackExpanded(); // Hit back BubbleStackView stackView = mBubbleController.getStackView(); stackView.onBackPressed(); // Make sure we're collapsed assertStackCollapsed(); } @Test public void testRegisterUnregisterBroadcastListener() { spyOn(mContext); mBubbleController.updateBubble(mBubbleEntry); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); assertThat(mFilterArgumentCaptor.getValue() .hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)).isTrue(); assertThat(mFilterArgumentCaptor.getValue() .hasAction(Intent.ACTION_SCREEN_OFF)).isTrue(); mBubbleData.dismissBubbleWithKey(mBubbleEntry.getKey(), REASON_APP_CANCEL); // TODO: not certain why this isn't called normally when tests are run, perhaps because // it's after an animation in BSV. This calls BubbleController#removeFromWindowManagerMaybe mBubbleController.onAllBubblesAnimatedOut(); verify(mContext).unregisterReceiver(eq(mBroadcastReceiverArgumentCaptor.getValue())); } @Test public void testBroadcastReceiverCloseDialogs_notGestureNav() { spyOn(mContext); mBubbleController.updateBubble(mBubbleEntry); mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); assertStackExpanded(); } @Test public void testBroadcastReceiverCloseDialogs_reasonGestureNav() { spyOn(mContext); mBubbleController.updateBubble(mBubbleEntry); mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); i.putExtra("reason", "gestureNav"); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); assertStackCollapsed(); } @Test public void testBroadcastReceiver_screenOff() { spyOn(mContext); mBubbleController.updateBubble(mBubbleEntry); mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_SCREEN_OFF); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); assertStackCollapsed(); } @Test public void testOnStatusBarStateChanged() { mBubbleController.updateBubble(mBubbleEntry); mBubbleData.setExpanded(true); assertStackExpanded(); BubbleStackView stackView = mBubbleController.getStackView(); assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); mBubbleController.onStatusBarStateChanged(false); assertStackCollapsed(); assertThat(stackView.getVisibility()).isEqualTo(View.INVISIBLE); mBubbleController.onStatusBarStateChanged(true); assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE); } /** * Test to verify behavior for following situation: *
    *
  • status bar shade state is set to false
  • *
  • there is a bubble pending to be expanded
  • *
* Test that duplicate status bar state updates to false do not clear the * pending bubble to be * expanded. */ @Test public void testOnStatusBarStateChanged_statusBarChangeDoesNotClearExpandingBubble() { mBubbleController.updateBubble(mBubbleEntry); mBubbleController.onStatusBarStateChanged(false); // Set the bubble to expand once status bar state changes mBubbleController.expandStackAndSelectBubble(mBubbleEntry); // Check that stack is currently collapsed assertStackCollapsed(); // Post status bar state change update with the same value mBubbleController.onStatusBarStateChanged(false); // Stack should remain collapsed assertStackCollapsed(); // Post status bar state change which should trigger bubble to expand mBubbleController.onStatusBarStateChanged(true); assertStackExpanded(); } /** * Test to verify behavior for the following scenario: *
    *
  1. device is locked with keyguard on, status bar shade state updates to * false
  2. *
  3. notification entry is marked to be a bubble and it is set to auto-expand
  4. *
  5. device unlock starts, status bar shade state receives another update to * false
  6. *
  7. device is unlocked and status bar shade state is set to true
  8. *
  9. bubble should be expanded
  10. *
*/ @Test public void testOnStatusBarStateChanged_newAutoExpandedBubbleRemainsExpanded() { // Set device as locked mBubbleController.onStatusBarStateChanged(false); // Create a auto-expanded bubble NotificationEntry entry = mNotificationTestHelper.createAutoExpandedBubble(); mEntryListener.onEntryAdded(entry); // When unlocking, we may receive duplicate updates with shade=false, ensure they don't // clear the expanded state mBubbleController.onStatusBarStateChanged(false); mBubbleController.onStatusBarStateChanged(true); // After unlocking, stack should be expanded assertStackExpanded(); } @Test public void testSetShouldAutoExpand_notifiesFlagChanged() { mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey()); assertThat(b.shouldAutoExpand()).isFalse(); // Set it to the same thing b.setShouldAutoExpand(false); // Verify it doesn't notify verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any()); // Set it to something different b.setShouldAutoExpand(true); verify(mBubbleController).onBubbleMetadataFlagChanged(b); } @Test public void testUpdateBubble_skipsDndSuppressListNotifs() { mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), true, /* isDismissable */ mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, mRow.shouldSuppressPeek()); mBubbleEntry.getBubbleMetadata().setFlags( Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); mBubbleController.updateBubble(mBubbleEntry); Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey()); assertThat(b.shouldAutoExpand()).isFalse(); assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull(); } @Test public void testOnRankingUpdate_DndSuppressListNotif() { // It's in the stack mBubbleController.updateBubble(mBubbleEntry); assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue(); // Set current user profile SparseArray userInfos = new SparseArray<>(); userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(), mock(UserInfo.class)); mBubbleController.onCurrentProfilesChanged(userInfos); // Send ranking update that the notif is suppressed from the list. HashMap> entryDataByKey = new HashMap<>(); mBubbleEntry = new BubbleEntry(mRow.getSbn(), mRow.getRanking(), true /* isDismissable */, mRow.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */, mRow.shouldSuppressPeek()); Pair pair = new Pair(mBubbleEntry, true); entryDataByKey.put(mBubbleEntry.getKey(), pair); NotificationListenerService.RankingMap rankingMap = mock(NotificationListenerService.RankingMap.class); when(rankingMap.getOrderedKeys()).thenReturn(new String[]{mBubbleEntry.getKey()}); mBubbleController.onRankingUpdated(rankingMap, entryDataByKey); // Should no longer be in the stack assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse(); } /** * Verifies that if a bubble is in the overflow and a non-interruptive notification update * comes in for it, it stays in the overflow but the entry is updated. */ @Test public void testNonInterruptiveUpdate_doesntBubbleFromOverflow() { mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Dismiss the bubble so it's in the overflow mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())).isTrue(); // Update the entry to not show in shade setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true); mBubbleController.updateBubble(mBubbleEntry, /* suppressFlyout= */ false, /* showInShade= */ true); // Check that the update was applied - shouldn't be show in shade assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) verify(mBubbleController, times(1)).inflateAndAdd( any(Bubble.class), anyBoolean(), anyBoolean()); } /** * Verifies that if a bubble is active, and a non-interruptive notification update comes in for * it, it doesn't trigger a new inflate and add for that bubble. */ @Test public void testNonInterruptiveUpdate_doesntTriggerInflate() { mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Update the entry to not show in shade setMetadataFlags(mRow, Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true); mBubbleController.updateBubble(mBubbleEntry, /* suppressFlyout= */ false, /* showInShade= */ true); // Check that the update was applied - shouldn't be show in shade assertBubbleNotificationSuppressedFromShade(mBubbleEntry); // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) verify(mBubbleController, times(1)).inflateAndAdd( any(Bubble.class), anyBoolean(), anyBoolean()); } /** * Verifies that if a bubble is in the overflow and a non-interruptive notification update * comes in for it with FLAG_BUBBLE that the flag is removed. */ @Test public void testNonInterruptiveUpdate_doesntOverrideOverflowFlagBubble() { mEntryListener.onEntryAdded(mRow); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true); assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry); // Dismiss the bubble so it's in the overflow mBubbleController.removeBubble( mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getKey())).isTrue(); // Once it's in the overflow it's not actively a bubble (doesn't have FLAG_BUBBLE) Bubble b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey()); assertThat(b.isBubble()).isFalse(); // Send a non-notifying update that has FLAG_BUBBLE mRow.getSbn().getNotification().flags = FLAG_BUBBLE; assertThat(mRow.getSbn().getNotification().isBubbleNotification()).isTrue(); mBubbleController.updateBubble(mBubbleEntry, /* suppressFlyout= */ false, /* showInShade= */ true); // Verify that it still doesn't have FLAG_BUBBLE because it's in the overflow. b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey()); assertThat(b.isBubble()).isFalse(); } @Test public void testNonSystemUpdatesIgnored() { mEntryListener.onEntryAdded(mRow); assertThat(mBubbleController.hasBubbles()).isTrue(); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ false); // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded) verify(mBubbleController, times(1)).inflateAndAdd( any(Bubble.class), anyBoolean(), anyBoolean()); } @Test public void testShowStackEdu_isNotConversationBubble() { // Setup setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(false /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Click on bubble Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); assertFalse(bubble.isConversation()); bubble.getIconView().callOnClick(); // Check education is not shown BubbleStackView stackView = mBubbleController.getStackView(); assertFalse(stackView.isStackEduVisible()); } @Test public void testShowStackEdu_isConversationBubble() { // Setup setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Click on bubble Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); assertTrue(bubble.isConversation()); bubble.getIconView().callOnClick(); // Check education is shown BubbleStackView stackView = mBubbleController.getStackView(); assertTrue(stackView.isStackEduVisible()); } @Test public void testShowStackEdu_isSeenConversationBubble() { // Setup setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, true); BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); mBubbleController.updateBubble(bubbleEntry); assertTrue(mBubbleController.hasBubbles()); // Click on bubble Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); assertTrue(bubble.isConversation()); bubble.getIconView().callOnClick(); // Check education is not shown BubbleStackView stackView = mBubbleController.getStackView(); assertFalse(stackView.isStackEduVisible()); } @Test public void testShowOrHideAppBubble_addsAndExpand() { assertThat(mBubbleController.isStackExpanded()).isFalse(); mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), /* showInShade= */ eq(false)); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); } @Test public void testShowOrHideAppBubble_expandIfCollapsed() { mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.collapseStack(); assertThat(mBubbleController.isStackExpanded()).isFalse(); // Calling this while collapsed will expand the app bubble mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); } @Test public void testShowOrHideAppBubble_collapseIfSelected() { mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); // Calling this while the app bubble is expanded should collapse the stack mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isFalse(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0); } @Test public void testShowOrHideAppBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() { UserHandle user10 = createUserHandle(/* userId = */ 10); String appBubbleKey = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), user10); mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10); // Calling this while the app bubble is expanded should collapse the stack mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey); assertThat(mBubbleController.isStackExpanded()).isFalse(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10); } @Test public void testShowOrHideAppBubbleOnUser10AndThenUser0_user0BubbleExpanded() { UserHandle user10 = createUserHandle(/* userId = */ 10); mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon); String appBubbleUser0Key = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0); mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleUser0Key); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles()).hasSize(2); assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0); assertThat(mBubbleData.getBubbles().get(1).getUser()).isEqualTo(user10); } @Test public void testShowOrHideAppBubble_selectIfNotSelected() { mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); mBubbleController.updateBubble(mBubbleEntry); mBubbleController.expandStackAndSelectBubble(mBubbleEntry); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); assertThat(mBubbleController.isStackExpanded()).isTrue(); mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon); assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo( Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0)); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); } @Test public void testCreateBubbleFromOngoingNotification() { NotificationEntry notif = new NotificationEntryBuilder() .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true) .setCanBubble(true) .build(); BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif); assertTrue("Ongoing Notifis should be dismissable", bubble.isDismissable()); } @Test public void testCreateBubbleFromNoDismissNotification() { NotificationEntry notif = new NotificationEntryBuilder() .setFlag(mContext, Notification.FLAG_NO_DISMISS, true) .setCanBubble(true) .build(); BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif); assertFalse("FLAG_NO_DISMISS Notifs should be non-dismissable", bubble.isDismissable()); } @Test public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() { mBubbleProperties.mIsBubbleBarEnabled = false; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertStackMode(); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); assertStackMode(); assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1); } @Test public void registerBubbleBarListener_barEnabled_smallScreen_shouldBeIgnored() { mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(false); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertStackMode(); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); assertStackMode(); assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1); } @Test public void registerBubbleBarListener_switchToBarAndBackToStack() { mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); assertTrue(mBubbleController.hasBubbles()); assertStackMode(); assertThat(mBubbleData.getBubbles()).hasSize(1); assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0)); assertBubbleIsInflatedForStack(mBubbleData.getOverflow()); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); assertBarMode(); assertThat(mBubbleData.getBubbles()).hasSize(1); assertBubbleIsInflatedForBar(mBubbleData.getBubbles().get(0)); assertBubbleIsInflatedForBar(mBubbleData.getOverflow()); mBubbleController.unregisterBubbleStateListener(); assertStackMode(); assertThat(mBubbleData.getBubbles()).hasSize(1); assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0)); assertBubbleIsInflatedForStack(mBubbleData.getOverflow()); } @Test public void switchBetweenBarAndStack_noBubbles_shouldBeIgnored() { mBubbleProperties.mIsBubbleBarEnabled = false; mPositioner.setIsLargeScreen(true); assertFalse(mBubbleController.hasBubbles()); assertNoBubbleContainerViews(); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); assertNoBubbleContainerViews(); mBubbleController.unregisterBubbleStateListener(); assertNoBubbleContainerViews(); } @Test public void bubbleBarBubbleExpandedAndCollapsed() { mBubbleProperties.mIsBubbleBarEnabled = true; mPositioner.setIsLargeScreen(true); mEntryListener.onEntryAdded(mRow); mBubbleController.updateBubble(mBubbleEntry); FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 500, 1000); assertThat(mBubbleController.getLayerView().isExpanded()).isTrue(); mBubbleController.collapseStack(); assertThat(mBubbleController.getLayerView().isExpanded()).isFalse(); } /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); NotificationEntry workEntry = new NotificationEntryBuilder() .setPkg(pkg) .setUser(userHandle) .build(); workEntry.setBubbleMetadata(getMetadata()); workEntry.setFlagBubble(true); return new Bubble(mBubblesManager.notifToBubbleEntry(workEntry), null, mock(Bubbles.PendingIntentCanceledListener.class), new SyncExecutor()); } private BubbleEntry createBubbleEntry(boolean isConversation) { NotificationEntry notificationEntry = mNotificationTestHelper.createBubble(mDeleteIntent); if (isConversation) { ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) .setId("shortcutId") .build(); NotificationEntryHelper.modifyRanking(notificationEntry) .setIsConversation(true) .setShortcutInfo(shortcutInfo) .build(); } return mBubblesManager.notifToBubbleEntry(notificationEntry); } /** Creates a context that will return a PackageManager with specific AppInfo. */ private Context setUpContextWithPackageManager(String pkg, ApplicationInfo info) throws Exception { final PackageManager pm = mock(PackageManager.class); when(pm.getApplicationInfo(eq(pkg), anyInt())).thenReturn(info); if (info != null) { Drawable d = mock(Drawable.class); when(d.getBounds()).thenReturn(new Rect()); when(pm.getApplicationIcon(anyString())).thenReturn(d); when(pm.getUserBadgedIcon(any(), any())).thenReturn(d); } final Context context = mock(Context.class); when(context.getPackageName()).thenReturn(pkg); when(context.getPackageManager()).thenReturn(pm); return context; } /** * Sets the bubble metadata flags for this entry. These flags are normally set by * NotificationManagerService when the notification is sent, however, these tests do not * go through that path so we set them explicitly when testing. */ private void setMetadataFlags(NotificationEntry entry, int flag, boolean enableFlag) { Notification.BubbleMetadata bubbleMetadata = entry.getSbn().getNotification().getBubbleMetadata(); int flags = bubbleMetadata.getFlags(); if (enableFlag) { flags |= flag; } else { flags &= ~flag; } bubbleMetadata.setFlags(flags); } /** * Set preferences boolean value for key * Used to setup global state for stack view education tests */ private void setPrefBoolean(String key, boolean enabled) { mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) .edit().putBoolean(key, enabled).apply(); } private Notification.BubbleMetadata getMetadata() { Intent target = new Intent(mContext, BubblesTestActivity.class); PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, FLAG_MUTABLE); return new Notification.BubbleMetadata.Builder( bubbleIntent, Icon.createWithResource( mContext, com.android.wm.shell.R.drawable.bubble_ic_create_bubble)) .build(); } private void switchUser(int userId) { when(mLockscreenUserManager.isCurrentProfile(anyInt())).thenAnswer( (Answer) invocation -> invocation.getArgument(0) == userId); SparseArray userInfos = new SparseArray<>(1); userInfos.put(userId, mock(UserInfo.class)); mBubbleController.onCurrentProfilesChanged(userInfos); mBubbleController.onUserChanged(userId); } private UserHandle createUserHandle(int userId) { UserHandle user = mock(UserHandle.class); when(user.getIdentifier()).thenReturn(userId); return user; } /** * Asserts that the bubble stack is expanded and also validates the cached state is updated. */ private void assertStackExpanded() { assertTrue(mBubbleController.isStackExpanded()); assertTrue(mBubbleController.getImplCachedState().isStackExpanded()); } /** * Asserts that the bubble stack is collapsed and also validates the cached state is updated. */ private void assertStackCollapsed() { assertFalse(mBubbleController.isStackExpanded()); assertFalse(mBubbleController.getImplCachedState().isStackExpanded()); } /** Asserts that both the bubble stack and bar views don't exist. */ private void assertNoBubbleContainerViews() { assertThat(mBubbleController.getStackView()).isNull(); assertThat(mBubbleController.getLayerView()).isNull(); } /** Asserts that the stack is created and the bar is null. */ private void assertStackMode() { assertThat(mBubbleController.getStackView()).isNotNull(); assertThat(mBubbleController.getLayerView()).isNull(); } /** Asserts that the given bubble has the stack expanded view inflated. */ private void assertBubbleIsInflatedForStack(BubbleViewProvider b) { assertThat(b.getIconView()).isNotNull(); assertThat(b.getExpandedView()).isNotNull(); assertThat(b.getBubbleBarExpandedView()).isNull(); } /** Asserts that the bar is created and the stack is null. */ private void assertBarMode() { assertThat(mBubbleController.getStackView()).isNull(); assertThat(mBubbleController.getLayerView()).isNotNull(); } /** Asserts that the given bubble has the bar expanded view inflated. */ private void assertBubbleIsInflatedForBar(BubbleViewProvider b) { // the icon view should be inflated for the overflow but not for other bubbles when showing // in the bar if (b instanceof Bubble) { assertThat(b.getIconView()).isNull(); } else if (b instanceof BubbleOverflow) { assertThat(b.getIconView()).isNotNull(); } assertThat(b.getExpandedView()).isNull(); assertThat(b.getBubbleBarExpandedView()).isNotNull(); } /** * Asserts that a bubble notification is suppressed from the shade and also validates the cached * state is updated. */ private void assertBubbleNotificationSuppressedFromShade(BubbleEntry entry) { assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade( entry.getKey(), entry.getGroupKey())); assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade( entry.getKey(), entry.getGroupKey())); } /** * Asserts that a bubble notification is not suppressed from the shade and also validates the * cached state is updated. */ private void assertBubbleNotificationNotSuppressedFromShade(BubbleEntry entry) { assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade( entry.getKey(), entry.getGroupKey())); assertFalse(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade( entry.getKey(), entry.getGroupKey())); } /** * Asserts that the system ui states associated to bubbles are in the correct state. */ private void assertSysuiStates(boolean stackExpanded, boolean manageMenuExpanded) { assertThat(mSysUiStateBubblesExpanded).isEqualTo(stackExpanded); assertThat(mSysUiStateBubblesManageMenuExpanded).isEqualTo(manageMenuExpanded); } private static class FakeBubbleStateListener implements Bubbles.BubbleStateListener { @Override public void onBubbleStateChange(BubbleBarUpdate update) { } } private static class FakeBubbleProperties implements BubbleProperties { boolean mIsBubbleBarEnabled = false; @Override public boolean isBubbleBarEnabled() { return mIsBubbleBarEnabled; } } }