1 /* 2 * Copyright (C) 2021 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.shade 18 19 import android.testing.AndroidTestingRunner 20 import android.testing.TestableLooper.RunWithLooper 21 import android.view.KeyEvent 22 import android.view.MotionEvent 23 import android.view.ViewGroup 24 import androidx.test.filters.SmallTest 25 import com.android.keyguard.KeyguardMessageAreaController 26 import com.android.keyguard.KeyguardSecurityContainerController 27 import com.android.keyguard.LockIconViewController 28 import com.android.keyguard.dagger.KeyguardBouncerComponent 29 import com.android.systemui.R 30 import com.android.systemui.SysuiTestCase 31 import com.android.systemui.back.domain.interactor.BackActionInteractor 32 import com.android.systemui.bouncer.data.factory.BouncerMessageFactory 33 import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository 34 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor 35 import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil 36 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel 37 import com.android.systemui.classifier.FalsingCollectorFake 38 import com.android.systemui.dock.DockManager 39 import com.android.systemui.dump.logcatLogBuffer 40 import com.android.systemui.flags.FakeFeatureFlags 41 import com.android.systemui.flags.Flags 42 import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor 43 import com.android.systemui.keyguard.KeyguardUnlockAnimationController 44 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 45 import com.android.systemui.keyguard.shared.model.TransitionStep 46 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel 47 import com.android.systemui.log.BouncerLogger 48 import com.android.systemui.power.domain.interactor.PowerInteractor 49 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler 50 import com.android.systemui.statusbar.LockscreenShadeTransitionController 51 import com.android.systemui.statusbar.NotificationInsetsController 52 import com.android.systemui.statusbar.NotificationShadeDepthController 53 import com.android.systemui.statusbar.NotificationShadeWindowController 54 import com.android.systemui.statusbar.SysuiStatusBarStateController 55 import com.android.systemui.statusbar.notification.data.repository.NotificationExpansionRepository 56 import com.android.systemui.statusbar.notification.stack.AmbientState 57 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 58 import com.android.systemui.statusbar.phone.CentralSurfaces 59 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController 60 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager 61 import com.android.systemui.statusbar.window.StatusBarWindowStateController 62 import com.android.systemui.unfold.UnfoldTransitionProgressProvider 63 import com.android.systemui.user.data.repository.FakeUserRepository 64 import com.android.systemui.util.mockito.any 65 import com.android.systemui.util.time.FakeSystemClock 66 import com.google.common.truth.Truth.assertThat 67 import java.util.Optional 68 import kotlinx.coroutines.ExperimentalCoroutinesApi 69 import kotlinx.coroutines.flow.emptyFlow 70 import kotlinx.coroutines.test.TestScope 71 import kotlinx.coroutines.test.runTest 72 import org.junit.Before 73 import org.junit.Test 74 import org.junit.runner.RunWith 75 import org.mockito.ArgumentCaptor 76 import org.mockito.Mock 77 import org.mockito.Mockito.anyFloat 78 import org.mockito.Mockito.mock 79 import org.mockito.Mockito.never 80 import org.mockito.Mockito.verify 81 import org.mockito.Mockito.`when` as whenever 82 import org.mockito.MockitoAnnotations 83 84 @OptIn(ExperimentalCoroutinesApi::class) 85 @SmallTest 86 @RunWith(AndroidTestingRunner::class) 87 @RunWithLooper(setAsMainLooper = true) 88 class NotificationShadeWindowViewControllerTest : SysuiTestCase() { 89 90 @Mock private lateinit var view: NotificationShadeWindowView 91 @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController 92 @Mock private lateinit var centralSurfaces: CentralSurfaces 93 @Mock private lateinit var backActionInteractor: BackActionInteractor 94 @Mock private lateinit var powerInteractor: PowerInteractor 95 @Mock private lateinit var dockManager: DockManager 96 @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController 97 @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController 98 @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController 99 @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController 100 @Mock private lateinit var shadeLogger: ShadeLogger 101 @Mock private lateinit var ambientState: AmbientState 102 @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel 103 @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController 104 @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager 105 @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController 106 @Mock 107 private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController 108 @Mock private lateinit var lockIconViewController: LockIconViewController 109 @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController 110 @Mock private lateinit var pulsingGestureListener: PulsingGestureListener 111 @Mock 112 private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener 113 @Mock private lateinit var notificationInsetsController: NotificationInsetsController 114 @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory 115 @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent 116 @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController 117 @Mock 118 private lateinit var unfoldTransitionProgressProvider: 119 Optional<UnfoldTransitionProgressProvider> 120 @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor 121 @Mock 122 lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel 123 @Mock lateinit var keyEventInteractor: KeyEventInteractor 124 private val notificationExpansionRepository = NotificationExpansionRepository() 125 126 private lateinit var fakeClock: FakeSystemClock 127 private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> 128 private lateinit var interactionEventHandler: InteractionEventHandler 129 130 private lateinit var underTest: NotificationShadeWindowViewController 131 132 private lateinit var testScope: TestScope 133 134 @Before 135 fun setUp() { 136 MockitoAnnotations.initMocks(this) 137 whenever(view.bottom).thenReturn(VIEW_BOTTOM) 138 whenever(view.findViewById<ViewGroup>(R.id.keyguard_bouncer_container)) 139 .thenReturn(mock(ViewGroup::class.java)) 140 whenever(keyguardBouncerComponentFactory.create(any(ViewGroup::class.java))) 141 .thenReturn(keyguardBouncerComponent) 142 whenever(keyguardBouncerComponent.securityContainerController) 143 .thenReturn(keyguardSecurityContainerController) 144 whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition) 145 .thenReturn(emptyFlow<TransitionStep>()) 146 147 val featureFlags = FakeFeatureFlags() 148 featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true) 149 featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) 150 featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) 151 featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) 152 featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) 153 154 testScope = TestScope() 155 fakeClock = FakeSystemClock() 156 underTest = 157 NotificationShadeWindowViewController( 158 lockscreenShadeTransitionController, 159 FalsingCollectorFake(), 160 sysuiStatusBarStateController, 161 dockManager, 162 notificationShadeDepthController, 163 view, 164 notificationPanelViewController, 165 ShadeExpansionStateManager(), 166 stackScrollLayoutController, 167 statusBarKeyguardViewManager, 168 statusBarWindowStateController, 169 lockIconViewController, 170 centralSurfaces, 171 backActionInteractor, 172 powerInteractor, 173 notificationShadeWindowController, 174 unfoldTransitionProgressProvider, 175 keyguardUnlockAnimationController, 176 notificationInsetsController, 177 ambientState, 178 shadeLogger, 179 pulsingGestureListener, 180 mLockscreenHostedDreamGestureListener, 181 keyguardBouncerViewModel, 182 keyguardBouncerComponentFactory, 183 mock(KeyguardMessageAreaController.Factory::class.java), 184 keyguardTransitionInteractor, 185 primaryBouncerToGoneTransitionViewModel, 186 notificationExpansionRepository, 187 featureFlags, 188 fakeClock, 189 BouncerMessageInteractor( 190 FakeBouncerMessageRepository(), 191 mock(BouncerMessageFactory::class.java), 192 FakeUserRepository(), 193 CountDownTimerUtil(), 194 featureFlags 195 ), 196 BouncerLogger(logcatLogBuffer("BouncerLog")), 197 keyEventInteractor, 198 ) 199 underTest.setupExpandedStatusBar() 200 201 interactionEventHandlerCaptor = ArgumentCaptor.forClass(InteractionEventHandler::class.java) 202 verify(view).setInteractionEventHandler(interactionEventHandlerCaptor.capture()) 203 interactionEventHandler = interactionEventHandlerCaptor.value 204 } 205 206 // Note: So far, these tests only cover interactions with the status bar view controller. More 207 // tests need to be added to test the rest of handleDispatchTouchEvent. 208 209 @Test 210 fun handleDispatchTouchEvent_nullStatusBarViewController_returnsFalse() = 211 testScope.runTest { 212 underTest.setStatusBarViewController(null) 213 214 val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 215 216 assertThat(returnVal).isFalse() 217 } 218 219 @Test 220 fun handleDispatchTouchEvent_downTouchBelowView_sendsTouchToSb() = 221 testScope.runTest { 222 underTest.setStatusBarViewController(phoneStatusBarViewController) 223 val ev = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0) 224 whenever(phoneStatusBarViewController.sendTouchToView(ev)).thenReturn(true) 225 226 val returnVal = interactionEventHandler.handleDispatchTouchEvent(ev) 227 228 verify(phoneStatusBarViewController).sendTouchToView(ev) 229 assertThat(returnVal).isTrue() 230 } 231 232 @Test 233 fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() = 234 testScope.runTest { 235 underTest.setStatusBarViewController(phoneStatusBarViewController) 236 val downEvBelow = 237 MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0) 238 interactionEventHandler.handleDispatchTouchEvent(downEvBelow) 239 240 val nextEvent = 241 MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0) 242 whenever(phoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true) 243 244 val returnVal = interactionEventHandler.handleDispatchTouchEvent(nextEvent) 245 246 verify(phoneStatusBarViewController).sendTouchToView(nextEvent) 247 assertThat(returnVal).isTrue() 248 } 249 250 @Test 251 fun handleDispatchTouchEvent_downAndPanelCollapsedAndInSbBoundAndSbWindowShow_sendsTouchToSb() = 252 testScope.runTest { 253 underTest.setStatusBarViewController(phoneStatusBarViewController) 254 whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true) 255 whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true) 256 whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) 257 .thenReturn(true) 258 whenever(phoneStatusBarViewController.sendTouchToView(DOWN_EVENT)).thenReturn(true) 259 260 val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 261 262 verify(phoneStatusBarViewController).sendTouchToView(DOWN_EVENT) 263 assertThat(returnVal).isTrue() 264 } 265 266 @Test 267 fun handleDispatchTouchEvent_panelNotCollapsed_returnsNull() = 268 testScope.runTest { 269 underTest.setStatusBarViewController(phoneStatusBarViewController) 270 whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true) 271 whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) 272 .thenReturn(true) 273 // Item we're testing 274 whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(false) 275 276 val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 277 278 verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT) 279 assertThat(returnVal).isNull() 280 } 281 282 @Test 283 fun handleDispatchTouchEvent_touchNotInSbBounds_returnsNull() = 284 testScope.runTest { 285 underTest.setStatusBarViewController(phoneStatusBarViewController) 286 whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true) 287 whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true) 288 // Item we're testing 289 whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) 290 .thenReturn(false) 291 292 val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 293 294 verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT) 295 assertThat(returnVal).isNull() 296 } 297 298 @Test 299 fun handleDispatchTouchEvent_sbWindowNotShowing_noSendTouchToSbAndReturnsTrue() = 300 testScope.runTest { 301 underTest.setStatusBarViewController(phoneStatusBarViewController) 302 whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true) 303 whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) 304 .thenReturn(true) 305 // Item we're testing 306 whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(false) 307 308 val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 309 310 verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT) 311 assertThat(returnVal).isTrue() 312 } 313 314 @Test 315 fun handleDispatchTouchEvent_downEventSentToSbThenAnotherEvent_sendsTouchToSb() = 316 testScope.runTest { 317 underTest.setStatusBarViewController(phoneStatusBarViewController) 318 whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true) 319 whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true) 320 whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) 321 .thenReturn(true) 322 323 // Down event first 324 interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) 325 326 // Then another event 327 val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) 328 whenever(phoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true) 329 330 val returnVal = interactionEventHandler.handleDispatchTouchEvent(nextEvent) 331 332 verify(phoneStatusBarViewController).sendTouchToView(nextEvent) 333 assertThat(returnVal).isTrue() 334 } 335 336 @Test 337 fun handleDispatchTouchEvent_launchAnimationRunningTimesOut() = 338 testScope.runTest { 339 // GIVEN touch dispatcher in a state that returns true 340 underTest.setStatusBarViewController(phoneStatusBarViewController) 341 whenever(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn( 342 true 343 ) 344 assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() 345 346 // WHEN launch animation is running for 2 seconds 347 fakeClock.setUptimeMillis(10000) 348 underTest.setExpandAnimationRunning(true) 349 fakeClock.advanceTime(2000) 350 351 // THEN touch is ignored 352 assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isFalse() 353 354 // WHEN Launch animation is running for 6 seconds 355 fakeClock.advanceTime(4000) 356 357 // THEN move is ignored, down is handled, and window is notified 358 assertThat(interactionEventHandler.handleDispatchTouchEvent(MOVE_EVENT)).isFalse() 359 assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() 360 verify(notificationShadeWindowController).setLaunchingActivity(false) 361 } 362 363 @Test 364 fun shouldInterceptTouchEvent_statusBarKeyguardViewManagerShouldIntercept() { 365 // down event should be intercepted by keyguardViewManager 366 whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) 367 .thenReturn(true) 368 369 // Then touch should not be intercepted 370 val shouldIntercept = interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT) 371 assertThat(shouldIntercept).isTrue() 372 } 373 374 @Test 375 fun testGetKeyguardMessageArea() = 376 testScope.runTest { 377 underTest.keyguardMessageArea 378 verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area) 379 } 380 381 @Test 382 fun forwardsDispatchKeyEvent() { 383 val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) 384 interactionEventHandler.dispatchKeyEvent(keyEvent) 385 verify(keyEventInteractor).dispatchKeyEvent(keyEvent) 386 } 387 388 @Test 389 fun forwardsDispatchKeyEventPreIme() { 390 val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) 391 interactionEventHandler.dispatchKeyEventPreIme(keyEvent) 392 verify(keyEventInteractor).dispatchKeyEventPreIme(keyEvent) 393 } 394 395 @Test 396 fun forwardsInterceptMediaKey() { 397 val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP) 398 interactionEventHandler.interceptMediaKey(keyEvent) 399 verify(keyEventInteractor).interceptMediaKey(keyEvent) 400 } 401 402 companion object { 403 private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) 404 private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) 405 private const val VIEW_BOTTOM = 100 406 } 407 } 408