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