1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.notification.stack; 18 19 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; 20 import static com.android.systemui.statusbar.StatusBarState.SHADE; 21 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; 22 23 import static org.mockito.ArgumentMatchers.any; 24 import static org.mockito.ArgumentMatchers.anyInt; 25 import static org.mockito.ArgumentMatchers.argThat; 26 import static org.mockito.Mockito.RETURNS_DEEP_STUBS; 27 import static org.mockito.Mockito.doNothing; 28 import static org.mockito.Mockito.mock; 29 import static org.mockito.Mockito.never; 30 import static org.mockito.Mockito.reset; 31 import static org.mockito.Mockito.verify; 32 import static org.mockito.Mockito.when; 33 34 import android.content.res.Resources; 35 import android.metrics.LogMaker; 36 import android.testing.AndroidTestingRunner; 37 import android.view.LayoutInflater; 38 39 import androidx.test.filters.SmallTest; 40 41 import com.android.internal.logging.MetricsLogger; 42 import com.android.internal.logging.UiEventLogger; 43 import com.android.internal.logging.nano.MetricsProto; 44 import com.android.internal.statusbar.IStatusBarService; 45 import com.android.systemui.SysuiTestCase; 46 import com.android.systemui.classifier.FalsingCollectorFake; 47 import com.android.systemui.classifier.FalsingManagerFake; 48 import com.android.systemui.colorextraction.SysuiColorExtractor; 49 import com.android.systemui.flags.FeatureFlags; 50 import com.android.systemui.media.KeyguardMediaController; 51 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 52 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; 53 import com.android.systemui.plugins.statusbar.StatusBarStateController; 54 import com.android.systemui.statusbar.LockscreenShadeTransitionController; 55 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 56 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; 57 import com.android.systemui.statusbar.NotificationRemoteInputManager; 58 import com.android.systemui.statusbar.RemoteInputController; 59 import com.android.systemui.statusbar.SysuiStatusBarStateController; 60 import com.android.systemui.statusbar.notification.DynamicPrivacyController; 61 import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController; 62 import com.android.systemui.statusbar.notification.NotificationEntryManager; 63 import com.android.systemui.statusbar.notification.collection.NotifCollection; 64 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 65 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; 66 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; 67 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; 68 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 69 import com.android.systemui.statusbar.notification.row.ForegroundServiceDungeonView; 70 import com.android.systemui.statusbar.notification.row.NotificationGutsManager; 71 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; 72 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; 73 import com.android.systemui.statusbar.phone.KeyguardBypassController; 74 import com.android.systemui.statusbar.phone.ScrimController; 75 import com.android.systemui.statusbar.phone.ShadeController; 76 import com.android.systemui.statusbar.phone.StatusBar; 77 import com.android.systemui.statusbar.policy.ConfigurationController; 78 import com.android.systemui.statusbar.policy.DeviceProvisionedController; 79 import com.android.systemui.statusbar.policy.ZenModeController; 80 import com.android.systemui.tuner.TunerService; 81 82 import org.junit.Before; 83 import org.junit.Test; 84 import org.junit.runner.RunWith; 85 import org.mockito.Answers; 86 import org.mockito.ArgumentCaptor; 87 import org.mockito.ArgumentMatcher; 88 import org.mockito.Captor; 89 import org.mockito.Mock; 90 import org.mockito.MockitoAnnotations; 91 92 /** 93 * Tests for {@link NotificationStackScrollLayoutController}. 94 */ 95 @SmallTest 96 @RunWith(AndroidTestingRunner.class) 97 public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { 98 99 @Mock private NotificationGutsManager mNotificationGutsManager; 100 @Mock private HeadsUpManagerPhone mHeadsUpManager; 101 @Mock private NotificationRoundnessManager mNotificationRoundnessManager; 102 @Mock private TunerService mTunerService; 103 @Mock private DeviceProvisionedController mDeviceProvisionedController; 104 @Mock private DynamicPrivacyController mDynamicPrivacyController; 105 @Mock private ConfigurationController mConfigurationController; 106 @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout; 107 @Mock private ZenModeController mZenModeController; 108 @Mock private KeyguardMediaController mKeyguardMediaController; 109 @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController; 110 @Mock private KeyguardBypassController mKeyguardBypassController; 111 @Mock private SysuiColorExtractor mColorExtractor; 112 @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager; 113 @Mock private MetricsLogger mMetricsLogger; 114 @Mock private Resources mResources; 115 @Mock(answer = Answers.RETURNS_SELF) 116 private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder; 117 @Mock private NotificationSwipeHelper mNotificationSwipeHelper; 118 @Mock private StatusBar mStatusBar; 119 @Mock private ScrimController mScrimController; 120 @Mock private NotificationGroupManagerLegacy mLegacyGroupManager; 121 @Mock private SectionHeaderController mSilentHeaderController; 122 @Mock private FeatureFlags mFeatureFlags; 123 @Mock private NotifPipeline mNotifPipeline; 124 @Mock private NotifCollection mNotifCollection; 125 @Mock private NotificationEntryManager mEntryManager; 126 @Mock private IStatusBarService mIStatusBarService; 127 @Mock private UiEventLogger mUiEventLogger; 128 @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController; 129 @Mock private ForegroundServiceDismissalFeatureController mFgFeatureController; 130 @Mock private ForegroundServiceSectionController mFgServicesSectionController; 131 @Mock private ForegroundServiceDungeonView mForegroundServiceDungeonView; 132 @Mock private LayoutInflater mLayoutInflater; 133 @Mock private NotificationRemoteInputManager mRemoteInputManager; 134 @Mock private VisualStabilityManager mVisualStabilityManager; 135 @Mock private ShadeController mShadeController; 136 137 @Captor 138 private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; 139 140 private NotificationStackScrollLayoutController mController; 141 142 @Before setUp()143 public void setUp() { 144 MockitoAnnotations.initMocks(this); 145 146 when(mNotificationSwipeHelperBuilder.build()).thenReturn(mNotificationSwipeHelper); 147 when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); 148 when(mFgServicesSectionController.createView(mLayoutInflater)) 149 .thenReturn(mForegroundServiceDungeonView); 150 151 mController = new NotificationStackScrollLayoutController( 152 true, 153 mNotificationGutsManager, 154 mHeadsUpManager, 155 mNotificationRoundnessManager, 156 mTunerService, 157 mDeviceProvisionedController, 158 mDynamicPrivacyController, 159 mConfigurationController, 160 mSysuiStatusBarStateController, 161 mKeyguardMediaController, 162 mKeyguardBypassController, 163 mZenModeController, 164 mColorExtractor, 165 mNotificationLockscreenUserManager, 166 mMetricsLogger, 167 new FalsingCollectorFake(), 168 new FalsingManagerFake(), 169 mResources, 170 mNotificationSwipeHelperBuilder, 171 mStatusBar, 172 mScrimController, 173 mLegacyGroupManager, 174 mLegacyGroupManager, 175 mSilentHeaderController, 176 mFeatureFlags, 177 mNotifPipeline, 178 mNotifCollection, 179 mEntryManager, 180 mLockscreenShadeTransitionController, 181 mIStatusBarService, 182 mUiEventLogger, 183 mFgFeatureController, 184 mFgServicesSectionController, 185 mLayoutInflater, 186 mRemoteInputManager, 187 mVisualStabilityManager, 188 mShadeController 189 ); 190 191 when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true); 192 } 193 194 @Test testAttach_viewAlreadyAttached()195 public void testAttach_viewAlreadyAttached() { 196 mController.attach(mNotificationStackScrollLayout); 197 198 verify(mConfigurationController).addCallback( 199 any(ConfigurationController.ConfigurationListener.class)); 200 } 201 @Test testAttach_viewAttachedAfterInit()202 public void testAttach_viewAttachedAfterInit() { 203 when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(false); 204 205 mController.attach(mNotificationStackScrollLayout); 206 207 verify(mConfigurationController, never()).addCallback( 208 any(ConfigurationController.ConfigurationListener.class)); 209 210 mController.mOnAttachStateChangeListener.onViewAttachedToWindow( 211 mNotificationStackScrollLayout); 212 213 verify(mConfigurationController).addCallback( 214 any(ConfigurationController.ConfigurationListener.class)); 215 } 216 217 @Test testOnDensityOrFontScaleChanged_reInflatesFooterViews()218 public void testOnDensityOrFontScaleChanged_reInflatesFooterViews() { 219 mController.attach(mNotificationStackScrollLayout); 220 mController.mConfigurationListener.onDensityOrFontScaleChanged(); 221 verify(mNotificationStackScrollLayout).reinflateViews(); 222 } 223 224 @Test testUpdateEmptyShadeView_notificationsVisible_zenHiding()225 public void testUpdateEmptyShadeView_notificationsVisible_zenHiding() { 226 when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(true); 227 mController.attach(mNotificationStackScrollLayout); 228 verify(mSysuiStatusBarStateController).addCallback( 229 mStateListenerArgumentCaptor.capture(), anyInt()); 230 StatusBarStateController.StateListener stateListener = 231 mStateListenerArgumentCaptor.getValue(); 232 233 setupShowEmptyShadeViewState(stateListener, true); 234 reset(mNotificationStackScrollLayout); 235 mController.updateShowEmptyShadeView(); 236 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 237 /* visible= */ true, 238 /* notifVisibleInShade= */ true); 239 240 setupShowEmptyShadeViewState(stateListener, false); 241 reset(mNotificationStackScrollLayout); 242 mController.updateShowEmptyShadeView(); 243 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 244 /* visible= */ false, 245 /* notifVisibleInShade= */ true); 246 } 247 248 @Test testUpdateEmptyShadeView_notificationsHidden_zenNotHiding()249 public void testUpdateEmptyShadeView_notificationsHidden_zenNotHiding() { 250 when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); 251 mController.attach(mNotificationStackScrollLayout); 252 verify(mSysuiStatusBarStateController).addCallback( 253 mStateListenerArgumentCaptor.capture(), anyInt()); 254 StatusBarStateController.StateListener stateListener = 255 mStateListenerArgumentCaptor.getValue(); 256 257 setupShowEmptyShadeViewState(stateListener, true); 258 reset(mNotificationStackScrollLayout); 259 mController.updateShowEmptyShadeView(); 260 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 261 /* visible= */ true, 262 /* notifVisibleInShade= */ false); 263 264 setupShowEmptyShadeViewState(stateListener, false); 265 reset(mNotificationStackScrollLayout); 266 mController.updateShowEmptyShadeView(); 267 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 268 /* visible= */ false, 269 /* notifVisibleInShade= */ false); 270 } 271 272 @Test testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView()273 public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() { 274 when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); 275 mController.attach(mNotificationStackScrollLayout); 276 verify(mSysuiStatusBarStateController).addCallback( 277 mStateListenerArgumentCaptor.capture(), anyInt()); 278 StatusBarStateController.StateListener stateListener = 279 mStateListenerArgumentCaptor.getValue(); 280 when(mNotificationStackScrollLayout.isUsingSplitNotificationShade()).thenReturn(true); 281 stateListener.onStateChanged(SHADE); 282 mController.getView().removeAllViews(); 283 284 mController.setQsExpanded(false); 285 reset(mNotificationStackScrollLayout); 286 mController.updateShowEmptyShadeView(); 287 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 288 /* visible= */ true, 289 /* notifVisibleInShade= */ false); 290 291 mController.setQsExpanded(true); 292 reset(mNotificationStackScrollLayout); 293 mController.updateShowEmptyShadeView(); 294 verify(mNotificationStackScrollLayout).updateEmptyShadeView( 295 /* visible= */ true, 296 /* notifVisibleInShade= */ false); 297 } 298 299 @Test testOnUserChange_verifySensitiveProfile()300 public void testOnUserChange_verifySensitiveProfile() { 301 when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); 302 303 ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor 304 .forClass(UserChangedListener.class); 305 306 mController.attach(mNotificationStackScrollLayout); 307 verify(mNotificationLockscreenUserManager) 308 .addUserChangedListener(userChangedCaptor.capture()); 309 reset(mNotificationStackScrollLayout); 310 311 UserChangedListener changedListener = userChangedCaptor.getValue(); 312 changedListener.onUserChanged(0); 313 verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); 314 } 315 316 @Test testOnStatePostChange_verifyIfProfileIsPublic()317 public void testOnStatePostChange_verifyIfProfileIsPublic() { 318 when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); 319 320 mController.attach(mNotificationStackScrollLayout); 321 verify(mSysuiStatusBarStateController).addCallback( 322 mStateListenerArgumentCaptor.capture(), anyInt()); 323 324 StatusBarStateController.StateListener stateListener = 325 mStateListenerArgumentCaptor.getValue(); 326 327 stateListener.onStatePostChange(); 328 verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); 329 } 330 331 @Test testOnMenuShownLogging()332 public void testOnMenuShownLogging() { 333 ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); 334 when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker( 335 MetricsProto.MetricsEvent.VIEW_UNKNOWN)); 336 337 ArgumentCaptor<OnMenuEventListener> onMenuEventListenerArgumentCaptor = 338 ArgumentCaptor.forClass(OnMenuEventListener.class); 339 340 mController.attach(mNotificationStackScrollLayout); 341 verify(mNotificationSwipeHelperBuilder).setOnMenuEventListener( 342 onMenuEventListenerArgumentCaptor.capture()); 343 344 OnMenuEventListener onMenuEventListener = onMenuEventListenerArgumentCaptor.getValue(); 345 346 onMenuEventListener.onMenuShown(row); 347 verify(row.getEntry().getSbn()).getLogMaker(); // This writes most of the log data 348 verify(mMetricsLogger).write(logMatcher(MetricsProto.MetricsEvent.ACTION_REVEAL_GEAR, 349 MetricsProto.MetricsEvent.TYPE_ACTION)); 350 } 351 352 @Test testOnMenuClickedLogging()353 public void testOnMenuClickedLogging() { 354 ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); 355 when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker( 356 MetricsProto.MetricsEvent.VIEW_UNKNOWN)); 357 358 ArgumentCaptor<OnMenuEventListener> onMenuEventListenerArgumentCaptor = 359 ArgumentCaptor.forClass(OnMenuEventListener.class); 360 361 mController.attach(mNotificationStackScrollLayout); 362 verify(mNotificationSwipeHelperBuilder).setOnMenuEventListener( 363 onMenuEventListenerArgumentCaptor.capture()); 364 365 OnMenuEventListener onMenuEventListener = onMenuEventListenerArgumentCaptor.getValue(); 366 367 onMenuEventListener.onMenuClicked(row, 0, 0, mock( 368 NotificationMenuRowPlugin.MenuItem.class)); 369 verify(row.getEntry().getSbn()).getLogMaker(); // This writes most of the log data 370 verify(mMetricsLogger).write(logMatcher(MetricsProto.MetricsEvent.ACTION_TOUCH_GEAR, 371 MetricsProto.MetricsEvent.TYPE_ACTION)); 372 } 373 374 @Test testDismissListener()375 public void testDismissListener() { 376 ArgumentCaptor<NotificationStackScrollLayout.DismissListener> 377 dismissListenerArgumentCaptor = ArgumentCaptor.forClass( 378 NotificationStackScrollLayout.DismissListener.class); 379 380 mController.attach(mNotificationStackScrollLayout); 381 382 verify(mNotificationStackScrollLayout).setDismissListener( 383 dismissListenerArgumentCaptor.capture()); 384 NotificationStackScrollLayout.DismissListener dismissListener = 385 dismissListenerArgumentCaptor.getValue(); 386 387 dismissListener.onDismiss(ROWS_ALL); 388 verify(mUiEventLogger).log(NotificationPanelEvent.fromSelection(ROWS_ALL)); 389 } 390 391 @Test testForegroundDismissEnabled()392 public void testForegroundDismissEnabled() { 393 when(mFgFeatureController.isForegroundServiceDismissalEnabled()).thenReturn(true); 394 mController.attach(mNotificationStackScrollLayout); 395 verify(mNotificationStackScrollLayout).initializeForegroundServiceSection( 396 mForegroundServiceDungeonView); 397 } 398 399 @Test testForegroundDismissaDisabled()400 public void testForegroundDismissaDisabled() { 401 when(mFgFeatureController.isForegroundServiceDismissalEnabled()).thenReturn(false); 402 mController.attach(mNotificationStackScrollLayout); 403 verify(mNotificationStackScrollLayout, never()).initializeForegroundServiceSection( 404 any(ForegroundServiceDungeonView.class)); 405 } 406 407 @Test testUpdateFooter_remoteInput()408 public void testUpdateFooter_remoteInput() { 409 ArgumentCaptor<RemoteInputController.Callback> callbackCaptor = 410 ArgumentCaptor.forClass(RemoteInputController.Callback.class); 411 doNothing().when(mRemoteInputManager).addControllerCallback(callbackCaptor.capture()); 412 when(mRemoteInputManager.isRemoteInputActive()).thenReturn(false); 413 mController.attach(mNotificationStackScrollLayout); 414 verify(mNotificationStackScrollLayout).setIsRemoteInputActive(false); 415 RemoteInputController.Callback callback = callbackCaptor.getValue(); 416 callback.onRemoteInputActive(true); 417 verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true); 418 } 419 logMatcher(int category, int type)420 private LogMaker logMatcher(int category, int type) { 421 return argThat(new LogMatcher(category, type)); 422 } 423 setupShowEmptyShadeViewState( StatusBarStateController.StateListener statusBarStateListener, boolean toShow)424 private void setupShowEmptyShadeViewState( 425 StatusBarStateController.StateListener statusBarStateListener, 426 boolean toShow) { 427 if (toShow) { 428 statusBarStateListener.onStateChanged(SHADE); 429 mController.setQsExpanded(false); 430 mController.getView().removeAllViews(); 431 } else { 432 statusBarStateListener.onStateChanged(KEYGUARD); 433 mController.setQsExpanded(true); 434 mController.getView().addContainerView(mock(ExpandableNotificationRow.class)); 435 } 436 } 437 438 static class LogMatcher implements ArgumentMatcher<LogMaker> { 439 private int mCategory, mType; 440 LogMatcher(int category, int type)441 LogMatcher(int category, int type) { 442 mCategory = category; 443 mType = type; 444 } matches(LogMaker l)445 public boolean matches(LogMaker l) { 446 return (l.getCategory() == mCategory) 447 && (l.getType() == mType); 448 } 449 toString()450 public String toString() { 451 return String.format("LogMaker(%d, %d)", mCategory, mType); 452 } 453 } 454 } 455