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