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.statusbar.phone 18 19 import android.app.StatusBarManager.WINDOW_STATE_HIDDEN 20 import android.app.StatusBarManager.WINDOW_STATE_HIDING 21 import android.app.StatusBarManager.WINDOW_STATE_SHOWING 22 import android.app.StatusBarManager.WINDOW_STATUS_BAR 23 import android.graphics.Insets 24 import android.platform.test.annotations.DisableFlags 25 import android.platform.test.annotations.EnableFlags 26 import android.view.InputDevice 27 import android.view.LayoutInflater 28 import android.view.MotionEvent 29 import android.view.View 30 import android.view.ViewTreeObserver 31 import android.view.ViewTreeObserver.OnPreDrawListener 32 import android.widget.FrameLayout 33 import androidx.test.ext.junit.runners.AndroidJUnit4 34 import androidx.test.filters.SmallTest 35 import androidx.test.platform.app.InstrumentationRegistry 36 import com.android.systemui.Flags as AconfigFlags 37 import com.android.systemui.SysuiTestCase 38 import com.android.systemui.battery.BatteryMeterView 39 import com.android.systemui.flags.FeatureFlags 40 import com.android.systemui.flags.Flags 41 import com.android.systemui.plugins.fakeDarkIconDispatcher 42 import com.android.systemui.res.R 43 import com.android.systemui.scene.ui.view.WindowRootView 44 import com.android.systemui.shade.ShadeControllerImpl 45 import com.android.systemui.shade.ShadeLogger 46 import com.android.systemui.shade.ShadeViewController 47 import com.android.systemui.shade.StatusBarLongPressGestureDetector 48 import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy 49 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor 50 import com.android.systemui.statusbar.CommandQueue 51 import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore 52 import com.android.systemui.statusbar.policy.Clock 53 import com.android.systemui.statusbar.policy.ConfigurationController 54 import com.android.systemui.statusbar.window.StatusBarWindowStateController 55 import com.android.systemui.testKosmos 56 import com.android.systemui.unfold.SysUIUnfoldComponent 57 import com.android.systemui.unfold.config.UnfoldTransitionConfig 58 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider 59 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel 60 import com.android.systemui.util.mockito.any 61 import com.android.systemui.util.mockito.argumentCaptor 62 import com.android.systemui.util.mockito.whenever 63 import com.android.systemui.util.view.ViewUtil 64 import com.google.common.truth.Truth.assertThat 65 import dagger.Lazy 66 import java.util.Optional 67 import javax.inject.Provider 68 import org.junit.Before 69 import org.junit.Test 70 import org.junit.runner.RunWith 71 import org.mockito.ArgumentCaptor 72 import org.mockito.ArgumentMatchers.eq 73 import org.mockito.Mock 74 import org.mockito.Mockito.mock 75 import org.mockito.Mockito.never 76 import org.mockito.Mockito.spy 77 import org.mockito.Mockito.verify 78 import org.mockito.Mockito.`when` 79 import org.mockito.MockitoAnnotations 80 81 @SmallTest 82 @RunWith(AndroidJUnit4::class) 83 class PhoneStatusBarViewControllerTest : SysuiTestCase() { 84 private val kosmos = testKosmos() 85 private val statusBarContentInsetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore 86 private val statusBarContentInsetsProvider = statusBarContentInsetsProviderStore.defaultDisplay 87 88 private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher 89 @Mock private lateinit var shadeViewController: ShadeViewController 90 @Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor 91 @Mock private lateinit var featureFlags: FeatureFlags 92 @Mock private lateinit var moveFromCenterAnimation: StatusBarMoveFromCenterAnimationController 93 @Mock private lateinit var sysuiUnfoldComponent: SysUIUnfoldComponent 94 @Mock private lateinit var progressProvider: ScopedUnfoldTransitionProgressProvider 95 @Mock private lateinit var configurationController: ConfigurationController 96 @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory 97 @Mock private lateinit var userChipViewModel: StatusBarUserChipViewModel 98 @Mock private lateinit var centralSurfacesImpl: CentralSurfacesImpl 99 @Mock private lateinit var commandQueue: CommandQueue 100 @Mock private lateinit var shadeControllerImpl: ShadeControllerImpl 101 @Mock private lateinit var windowRootView: Provider<WindowRootView> 102 @Mock private lateinit var shadeLogger: ShadeLogger 103 @Mock private lateinit var viewUtil: ViewUtil 104 @Mock private lateinit var mStatusBarLongPressGestureDetector: StatusBarLongPressGestureDetector 105 @Mock private lateinit var statusBarTouchShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy 106 private lateinit var statusBarWindowStateController: StatusBarWindowStateController 107 108 private lateinit var view: PhoneStatusBarView 109 private lateinit var controller: PhoneStatusBarViewController 110 111 private val clockView: Clock 112 get() = view.requireViewById(R.id.clock) 113 114 private val batteryView: BatteryMeterView 115 get() = view.requireViewById(R.id.battery) 116 117 private val unfoldConfig = UnfoldConfig() 118 119 @Before setUpnull120 fun setUp() { 121 MockitoAnnotations.initMocks(this) 122 123 statusBarWindowStateController = StatusBarWindowStateController(DISPLAY_ID, commandQueue) 124 125 `when`(statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()) 126 .thenReturn(Insets.NONE) 127 128 `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController()) 129 .thenReturn(moveFromCenterAnimation) 130 // create the view and controller on main thread as it requires main looper 131 InstrumentationRegistry.getInstrumentation().runOnMainSync { 132 val parent = FrameLayout(mContext) // add parent to keep layout params 133 view = 134 LayoutInflater.from(mContext).inflate(R.layout.status_bar, parent, false) 135 as PhoneStatusBarView 136 controller = createAndInitController(view) 137 } 138 } 139 140 @Test onViewAttachedAndDrawn_startListeningConfigurationControllerCallbacknull141 fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() { 142 val view = createViewMock() 143 144 InstrumentationRegistry.getInstrumentation().runOnMainSync { 145 controller = createAndInitController(view) 146 } 147 148 verify(configurationController).addCallback(any()) 149 } 150 151 @Test onViewAttachedAndDrawn_darkReceiversRegisterednull152 fun onViewAttachedAndDrawn_darkReceiversRegistered() { 153 val view = createViewMock() 154 155 InstrumentationRegistry.getInstrumentation().runOnMainSync { 156 controller = createAndInitController(view) 157 } 158 159 assertThat(fakeDarkIconDispatcher.receivers.size).isEqualTo(2) 160 assertThat(fakeDarkIconDispatcher.receivers).contains(clockView) 161 assertThat(fakeDarkIconDispatcher.receivers).contains(batteryView) 162 } 163 164 @Test onViewAttachedAndDrawn_moveFromCenterAnimationEnabled_moveFromCenterAnimationInitializednull165 fun onViewAttachedAndDrawn_moveFromCenterAnimationEnabled_moveFromCenterAnimationInitialized() { 166 whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)).thenReturn(true) 167 val view = createViewMock() 168 val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) 169 unfoldConfig.isEnabled = true 170 // create the controller on main thread as it requires main looper 171 InstrumentationRegistry.getInstrumentation().runOnMainSync { 172 controller = createAndInitController(view) 173 } 174 175 verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) 176 argumentCaptor.value.onPreDraw() 177 178 verify(moveFromCenterAnimation).onViewsReady(any()) 179 } 180 181 @Test onViewAttachedAndDrawn_statusBarAnimationDisabled_animationNotInitializednull182 fun onViewAttachedAndDrawn_statusBarAnimationDisabled_animationNotInitialized() { 183 whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) 184 .thenReturn(false) 185 val view = createViewMock() 186 unfoldConfig.isEnabled = true 187 // create the controller on main thread as it requires main looper 188 InstrumentationRegistry.getInstrumentation().runOnMainSync { 189 controller = createAndInitController(view) 190 } 191 192 verify(moveFromCenterAnimation, never()).onViewsReady(any()) 193 } 194 195 @Test onViewDetached_darkReceiversUnregisterednull196 fun onViewDetached_darkReceiversUnregistered() { 197 val view = createViewMock() 198 199 InstrumentationRegistry.getInstrumentation().runOnMainSync { 200 controller = createAndInitController(view) 201 } 202 203 assertThat(fakeDarkIconDispatcher.receivers).isNotEmpty() 204 205 controller.onViewDetached() 206 207 assertThat(fakeDarkIconDispatcher.receivers).isEmpty() 208 } 209 210 @Test handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEventnull211 fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { 212 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false) 213 val returnVal = 214 view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)) 215 assertThat(returnVal).isFalse() 216 verify(shadeViewController, never()).handleExternalTouch(any()) 217 } 218 219 @Test handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEventnull220 fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() { 221 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 222 `when`(shadeViewController.isViewEnabled).thenReturn(false) 223 val returnVal = 224 view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)) 225 assertThat(returnVal).isTrue() 226 verify(shadeViewController, never()).handleExternalTouch(any()) 227 } 228 229 @Test handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEventnull230 fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() { 231 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 232 `when`(shadeViewController.isViewEnabled).thenReturn(false) 233 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 2f, 0) 234 235 view.onTouchEvent(event) 236 237 verify(shadeViewController).handleExternalTouch(event) 238 } 239 240 @Test handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEventnull241 fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() { 242 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 243 `when`(shadeViewController.isViewEnabled).thenReturn(true) 244 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 245 246 view.onTouchEvent(event) 247 248 verify(shadeViewController).handleExternalTouch(event) 249 } 250 251 @Test handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEventnull252 fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() { 253 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 254 `when`(panelExpansionInteractor.isFullyCollapsed).thenReturn(true) 255 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) 256 257 view.onTouchEvent(event) 258 259 verify(shadeViewController, never()).handleExternalTouch(any()) 260 } 261 262 @Test 263 @DisableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP) handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOff_viewReturnsFalsenull264 fun handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOff_viewReturnsFalse() { 265 `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(false) 266 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 267 268 val returnVal = view.onInterceptTouchEvent(event) 269 270 assertThat(returnVal).isFalse() 271 } 272 273 @Test 274 @EnableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP) handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOn_viewReturnsFalsenull275 fun handleInterceptTouchEventFromStatusBar_shadeReturnsFalse_flagOn_viewReturnsFalse() { 276 `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(false) 277 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 278 279 val returnVal = view.onInterceptTouchEvent(event) 280 281 assertThat(returnVal).isFalse() 282 } 283 284 @Test 285 @DisableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP) handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOff_viewReturnsFalsenull286 fun handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOff_viewReturnsFalse() { 287 `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(true) 288 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 289 290 val returnVal = view.onInterceptTouchEvent(event) 291 292 assertThat(returnVal).isFalse() 293 } 294 295 @Test 296 @EnableFlags(com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP) handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOn_viewReturnsTruenull297 fun handleInterceptTouchEventFromStatusBar_shadeReturnsTrue_flagOn_viewReturnsTrue() { 298 `when`(shadeViewController.handleExternalInterceptTouch(any())).thenReturn(true) 299 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 300 301 val returnVal = view.onInterceptTouchEvent(event) 302 303 assertThat(returnVal).isTrue() 304 } 305 306 @Test onTouch_windowHidden_centralSurfacesNotNotifiednull307 fun onTouch_windowHidden_centralSurfacesNotNotified() { 308 val callback = getCommandQueueCallback() 309 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_HIDDEN) 310 311 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 312 313 verify(centralSurfacesImpl, never()).setInteracting(any(), any()) 314 } 315 316 @Test onTouch_windowHiding_centralSurfacesNotNotifiednull317 fun onTouch_windowHiding_centralSurfacesNotNotified() { 318 val callback = getCommandQueueCallback() 319 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_HIDING) 320 321 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 322 323 verify(centralSurfacesImpl, never()).setInteracting(any(), any()) 324 } 325 326 @Test onTouch_windowShowing_centralSurfacesNotifiednull327 fun onTouch_windowShowing_centralSurfacesNotified() { 328 val callback = getCommandQueueCallback() 329 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING) 330 331 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 332 333 verify(centralSurfacesImpl).setInteracting(any(), any()) 334 } 335 336 @Test 337 @EnableFlags(AconfigFlags.FLAG_SHADE_WINDOW_GOES_AROUND) onTouch_actionDown_propagatesToDisplayPolicynull338 fun onTouch_actionDown_propagatesToDisplayPolicy() { 339 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 340 341 verify(statusBarTouchShadeDisplayPolicy).onStatusBarTouched(eq(mContext.displayId)) 342 } 343 344 @Test 345 @EnableFlags(AconfigFlags.FLAG_SHADE_WINDOW_GOES_AROUND) onTouch_actionUp_notPropagatesToDisplayPolicynull346 fun onTouch_actionUp_notPropagatesToDisplayPolicy() { 347 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)) 348 349 verify(statusBarTouchShadeDisplayPolicy, never()).onStatusBarTouched(any()) 350 } 351 352 @Test 353 @DisableFlags(AconfigFlags.FLAG_SHADE_WINDOW_GOES_AROUND) onTouch_shadeWindowGoesAroundDisabled_notPropagatesToDisplayPolicynull354 fun onTouch_shadeWindowGoesAroundDisabled_notPropagatesToDisplayPolicy() { 355 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 356 357 verify(statusBarTouchShadeDisplayPolicy, never()).onStatusBarTouched(any()) 358 } 359 360 @Test shadeIsExpandedOnStatusIconMouseClicknull361 fun shadeIsExpandedOnStatusIconMouseClick() { 362 val view = createViewMock() 363 InstrumentationRegistry.getInstrumentation().runOnMainSync { 364 controller = createAndInitController(view) 365 } 366 val statusContainer = view.requireViewById<View>(R.id.system_icons) 367 statusContainer.dispatchTouchEvent(getActionUpEventFromSource(InputDevice.SOURCE_MOUSE)) 368 verify(shadeControllerImpl).animateExpandShade() 369 } 370 371 @Test statusIconContainerIsNotHandlingTouchScreenTouchesnull372 fun statusIconContainerIsNotHandlingTouchScreenTouches() { 373 val view = createViewMock() 374 InstrumentationRegistry.getInstrumentation().runOnMainSync { 375 controller = createAndInitController(view) 376 } 377 val statusContainer = view.requireViewById<View>(R.id.system_icons) 378 val handled = 379 statusContainer.dispatchTouchEvent( 380 getActionUpEventFromSource(InputDevice.SOURCE_TOUCHSCREEN) 381 ) 382 assertThat(handled).isFalse() 383 } 384 getActionUpEventFromSourcenull385 private fun getActionUpEventFromSource(source: Int): MotionEvent { 386 val ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) 387 ev.source = source 388 return ev 389 } 390 391 @Test shadeIsNotExpandedOnStatusBarGeneralClicknull392 fun shadeIsNotExpandedOnStatusBarGeneralClick() { 393 val view = createViewMock() 394 InstrumentationRegistry.getInstrumentation().runOnMainSync { 395 controller = createAndInitController(view) 396 } 397 view.performClick() 398 verify(shadeControllerImpl, never()).animateExpandShade() 399 } 400 getCommandQueueCallbacknull401 private fun getCommandQueueCallback(): CommandQueue.Callbacks { 402 val captor = argumentCaptor<CommandQueue.Callbacks>() 403 verify(commandQueue).addCallback(captor.capture()) 404 return captor.value!! 405 } 406 createViewMocknull407 private fun createViewMock(): PhoneStatusBarView { 408 val view = spy(view) 409 val viewTreeObserver = mock(ViewTreeObserver::class.java) 410 `when`(view.viewTreeObserver).thenReturn(viewTreeObserver) 411 `when`(view.isAttachedToWindow).thenReturn(true) 412 return view 413 } 414 createAndInitControllernull415 private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController { 416 return PhoneStatusBarViewController.Factory( 417 Optional.of(sysuiUnfoldComponent), 418 Optional.of(progressProvider), 419 featureFlags, 420 userChipViewModel, 421 centralSurfacesImpl, 422 statusBarWindowStateController, 423 shadeControllerImpl, 424 shadeViewController, 425 panelExpansionInteractor, 426 { mStatusBarLongPressGestureDetector }, 427 windowRootView, 428 shadeLogger, 429 viewUtil, 430 configurationController, 431 mStatusOverlayHoverListenerFactory, 432 fakeDarkIconDispatcher, 433 statusBarContentInsetsProviderStore, 434 Lazy { statusBarTouchShadeDisplayPolicy }, 435 ) 436 .create(view) 437 .also { it.init() } 438 } 439 440 private class UnfoldConfig : UnfoldTransitionConfig { 441 override var isEnabled: Boolean = false 442 override var isHingeAngleEnabled: Boolean = false 443 override val isHapticsEnabled: Boolean = false 444 override val halfFoldedTimeoutMillis: Int = 0 445 } 446 447 private companion object { 448 const val DISPLAY_ID = 1 449 } 450 } 451