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.view.InputDevice 24 import android.view.LayoutInflater 25 import android.view.MotionEvent 26 import android.view.View 27 import android.view.ViewTreeObserver 28 import android.view.ViewTreeObserver.OnPreDrawListener 29 import android.widget.FrameLayout 30 import androidx.test.filters.SmallTest 31 import androidx.test.platform.app.InstrumentationRegistry 32 import com.android.systemui.SysuiTestCase 33 import com.android.systemui.flags.FeatureFlags 34 import com.android.systemui.flags.Flags 35 import com.android.systemui.res.R 36 import com.android.systemui.scene.ui.view.WindowRootView 37 import com.android.systemui.shade.ShadeControllerImpl 38 import com.android.systemui.shade.ShadeLogger 39 import com.android.systemui.shade.ShadeViewController 40 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor 41 import com.android.systemui.statusbar.CommandQueue 42 import com.android.systemui.statusbar.policy.ConfigurationController 43 import com.android.systemui.statusbar.window.StatusBarWindowStateController 44 import com.android.systemui.unfold.SysUIUnfoldComponent 45 import com.android.systemui.unfold.config.UnfoldTransitionConfig 46 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider 47 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel 48 import com.android.systemui.util.mockito.any 49 import com.android.systemui.util.mockito.argumentCaptor 50 import com.android.systemui.util.mockito.whenever 51 import com.android.systemui.util.view.ViewUtil 52 import com.google.common.truth.Truth.assertThat 53 import java.util.Optional 54 import javax.inject.Provider 55 import org.junit.Before 56 import org.junit.Test 57 import org.mockito.ArgumentCaptor 58 import org.mockito.Mock 59 import org.mockito.Mockito.mock 60 import org.mockito.Mockito.never 61 import org.mockito.Mockito.spy 62 import org.mockito.Mockito.verify 63 import org.mockito.Mockito.`when` 64 import org.mockito.MockitoAnnotations 65 66 @SmallTest 67 class PhoneStatusBarViewControllerTest : SysuiTestCase() { 68 69 @Mock private lateinit var shadeViewController: ShadeViewController 70 @Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor 71 @Mock private lateinit var featureFlags: FeatureFlags 72 @Mock private lateinit var moveFromCenterAnimation: StatusBarMoveFromCenterAnimationController 73 @Mock private lateinit var sysuiUnfoldComponent: SysUIUnfoldComponent 74 @Mock private lateinit var progressProvider: ScopedUnfoldTransitionProgressProvider 75 @Mock private lateinit var configurationController: ConfigurationController 76 @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory 77 @Mock private lateinit var userChipViewModel: StatusBarUserChipViewModel 78 @Mock private lateinit var centralSurfacesImpl: CentralSurfacesImpl 79 @Mock private lateinit var commandQueue: CommandQueue 80 @Mock private lateinit var shadeControllerImpl: ShadeControllerImpl 81 @Mock private lateinit var windowRootView: Provider<WindowRootView> 82 @Mock private lateinit var shadeLogger: ShadeLogger 83 @Mock private lateinit var viewUtil: ViewUtil 84 private lateinit var statusBarWindowStateController: StatusBarWindowStateController 85 86 private lateinit var view: PhoneStatusBarView 87 private lateinit var controller: PhoneStatusBarViewController 88 89 private val unfoldConfig = UnfoldConfig() 90 91 @Before setUpnull92 fun setUp() { 93 MockitoAnnotations.initMocks(this) 94 95 statusBarWindowStateController = StatusBarWindowStateController(DISPLAY_ID, commandQueue) 96 97 `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController()) 98 .thenReturn(moveFromCenterAnimation) 99 // create the view and controller on main thread as it requires main looper 100 InstrumentationRegistry.getInstrumentation().runOnMainSync { 101 val parent = FrameLayout(mContext) // add parent to keep layout params 102 view = 103 LayoutInflater.from(mContext).inflate(R.layout.status_bar, parent, false) 104 as PhoneStatusBarView 105 controller = createAndInitController(view) 106 } 107 } 108 109 @Test onViewAttachedAndDrawn_startListeningConfigurationControllerCallbacknull110 fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() { 111 val view = createViewMock() 112 val argumentCaptor = ArgumentCaptor.forClass( 113 ConfigurationController.ConfigurationListener::class.java) 114 InstrumentationRegistry.getInstrumentation().runOnMainSync { 115 controller = createAndInitController(view) 116 } 117 118 verify(configurationController).addCallback(argumentCaptor.capture()) 119 argumentCaptor.value.onDensityOrFontScaleChanged() 120 121 verify(view).onDensityOrFontScaleChanged() 122 } 123 124 @Test onViewAttachedAndDrawn_moveFromCenterAnimationEnabled_moveFromCenterAnimationInitializednull125 fun onViewAttachedAndDrawn_moveFromCenterAnimationEnabled_moveFromCenterAnimationInitialized() { 126 whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)).thenReturn(true) 127 val view = createViewMock() 128 val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) 129 unfoldConfig.isEnabled = true 130 // create the controller on main thread as it requires main looper 131 InstrumentationRegistry.getInstrumentation().runOnMainSync { 132 controller = createAndInitController(view) 133 } 134 135 verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) 136 argumentCaptor.value.onPreDraw() 137 138 verify(moveFromCenterAnimation).onViewsReady(any()) 139 } 140 141 @Test onViewAttachedAndDrawn_statusBarAnimationDisabled_animationNotInitializednull142 fun onViewAttachedAndDrawn_statusBarAnimationDisabled_animationNotInitialized() { 143 whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) 144 .thenReturn(false) 145 val view = createViewMock() 146 unfoldConfig.isEnabled = true 147 // create the controller on main thread as it requires main looper 148 InstrumentationRegistry.getInstrumentation().runOnMainSync { 149 controller = createAndInitController(view) 150 } 151 152 verify(moveFromCenterAnimation, never()).onViewsReady(any()) 153 } 154 155 @Test handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEventnull156 fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { 157 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false) 158 val returnVal = 159 view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 160 assertThat(returnVal).isFalse() 161 verify(shadeViewController, never()).handleExternalTouch(any()) 162 } 163 164 @Test handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEventnull165 fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() { 166 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 167 `when`(shadeViewController.isViewEnabled).thenReturn(false) 168 val returnVal = 169 view.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 170 assertThat(returnVal).isTrue() 171 verify(shadeViewController, never()).handleExternalTouch(any()) 172 } 173 174 @Test handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEventnull175 fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() { 176 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 177 `when`(shadeViewController.isViewEnabled).thenReturn(false) 178 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) 179 180 view.onTouchEvent(event) 181 182 verify(shadeViewController).handleExternalTouch(event) 183 } 184 185 @Test handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEventnull186 fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() { 187 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 188 `when`(shadeViewController.isViewEnabled).thenReturn(true) 189 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) 190 191 view.onTouchEvent(event) 192 193 verify(shadeViewController).handleExternalTouch(event) 194 } 195 196 @Test handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEventnull197 fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() { 198 `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) 199 `when`(panelExpansionInteractor.isFullyCollapsed).thenReturn(true) 200 val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) 201 202 view.onTouchEvent(event) 203 204 verify(shadeViewController, never()).handleExternalTouch(any()) 205 } 206 207 @Test onTouch_windowHidden_centralSurfacesNotNotifiednull208 fun onTouch_windowHidden_centralSurfacesNotNotified() { 209 val callback = getCommandQueueCallback() 210 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_HIDDEN) 211 212 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 213 214 verify(centralSurfacesImpl, never()).setInteracting(any(), any()) 215 } 216 217 @Test onTouch_windowHiding_centralSurfacesNotNotifiednull218 fun onTouch_windowHiding_centralSurfacesNotNotified() { 219 val callback = getCommandQueueCallback() 220 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_HIDING) 221 222 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 223 224 verify(centralSurfacesImpl, never()).setInteracting(any(), any()) 225 } 226 227 @Test onTouch_windowShowing_centralSurfacesNotifiednull228 fun onTouch_windowShowing_centralSurfacesNotified() { 229 val callback = getCommandQueueCallback() 230 callback.setWindowState(DISPLAY_ID, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING) 231 232 controller.onTouch(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) 233 234 verify(centralSurfacesImpl).setInteracting(any(), any()) 235 } 236 237 @Test shadeIsExpandedOnStatusIconMouseClicknull238 fun shadeIsExpandedOnStatusIconMouseClick() { 239 val view = createViewMock() 240 InstrumentationRegistry.getInstrumentation().runOnMainSync { 241 controller = createAndInitController(view) 242 } 243 val statusContainer = view.requireViewById<View>(R.id.system_icons) 244 statusContainer.dispatchTouchEvent( 245 getActionUpEventFromSource(InputDevice.SOURCE_MOUSE) 246 ) 247 verify(shadeControllerImpl).animateExpandShade() 248 } 249 250 @Test statusIconContainerIsNotHandlingTouchScreenTouchesnull251 fun statusIconContainerIsNotHandlingTouchScreenTouches() { 252 val view = createViewMock() 253 InstrumentationRegistry.getInstrumentation().runOnMainSync { 254 controller = createAndInitController(view) 255 } 256 val statusContainer = view.requireViewById<View>(R.id.system_icons) 257 val handled = statusContainer.dispatchTouchEvent( 258 getActionUpEventFromSource(InputDevice.SOURCE_TOUCHSCREEN) 259 ) 260 assertThat(handled).isFalse() 261 } 262 getActionUpEventFromSourcenull263 private fun getActionUpEventFromSource(source: Int): MotionEvent { 264 val ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) 265 ev.source = source 266 return ev 267 } 268 269 @Test shadeIsNotExpandedOnStatusBarGeneralClicknull270 fun shadeIsNotExpandedOnStatusBarGeneralClick() { 271 val view = createViewMock() 272 InstrumentationRegistry.getInstrumentation().runOnMainSync { 273 controller = createAndInitController(view) 274 } 275 view.performClick() 276 verify(shadeControllerImpl, never()).animateExpandShade() 277 } 278 getCommandQueueCallbacknull279 private fun getCommandQueueCallback(): CommandQueue.Callbacks { 280 val captor = argumentCaptor<CommandQueue.Callbacks>() 281 verify(commandQueue).addCallback(captor.capture()) 282 return captor.value!! 283 } 284 createViewMocknull285 private fun createViewMock(): PhoneStatusBarView { 286 val view = spy(view) 287 val viewTreeObserver = mock(ViewTreeObserver::class.java) 288 `when`(view.viewTreeObserver).thenReturn(viewTreeObserver) 289 `when`(view.isAttachedToWindow).thenReturn(true) 290 return view 291 } 292 createAndInitControllernull293 private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController { 294 return PhoneStatusBarViewController.Factory( 295 Optional.of(sysuiUnfoldComponent), 296 Optional.of(progressProvider), 297 featureFlags, 298 userChipViewModel, 299 centralSurfacesImpl, 300 statusBarWindowStateController, 301 shadeControllerImpl, 302 shadeViewController, 303 panelExpansionInteractor, 304 windowRootView, 305 shadeLogger, 306 viewUtil, 307 configurationController, 308 mStatusOverlayHoverListenerFactory 309 ) 310 .create(view) 311 .also { it.init() } 312 } 313 314 private class UnfoldConfig : UnfoldTransitionConfig { 315 override var isEnabled: Boolean = false 316 override var isHingeAngleEnabled: Boolean = false 317 override val isHapticsEnabled: Boolean = false 318 override val halfFoldedTimeoutMillis: Int = 0 319 } 320 321 private companion object { 322 const val DISPLAY_ID = 1 323 } 324 } 325