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.view.LayoutInflater
20 import android.view.MotionEvent
21 import android.view.ViewTreeObserver
22 import android.view.ViewTreeObserver.OnPreDrawListener
23 import android.widget.FrameLayout
24 import androidx.test.filters.SmallTest
25 import androidx.test.platform.app.InstrumentationRegistry
26 import com.android.systemui.R
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.flags.FeatureFlags
29 import com.android.systemui.flags.Flags
30 import com.android.systemui.scene.ui.view.WindowRootView
31 import com.android.systemui.shade.ShadeControllerImpl
32 import com.android.systemui.shade.ShadeLogger
33 import com.android.systemui.shade.ShadeViewController
34 import com.android.systemui.statusbar.policy.ConfigurationController
35 import com.android.systemui.unfold.SysUIUnfoldComponent
36 import com.android.systemui.unfold.config.UnfoldTransitionConfig
37 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
38 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
39 import com.android.systemui.util.mockito.any
40 import com.android.systemui.util.mockito.whenever
41 import com.android.systemui.util.view.ViewUtil
42 import com.google.common.truth.Truth.assertThat
43 import org.junit.Before
44 import org.junit.Test
45 import org.mockito.ArgumentCaptor
46 import org.mockito.Mock
47 import org.mockito.Mockito.mock
48 import org.mockito.Mockito.never
49 import org.mockito.Mockito.spy
50 import org.mockito.Mockito.verify
51 import org.mockito.Mockito.`when`
52 import org.mockito.MockitoAnnotations
53 import java.util.Optional
54 import javax.inject.Provider
55 
56 @SmallTest
57 class PhoneStatusBarViewControllerTest : SysuiTestCase() {
58 
59     @Mock
60     private lateinit var shadeViewController: ShadeViewController
61     @Mock
62     private lateinit var featureFlags: FeatureFlags
63     @Mock
64     private lateinit var moveFromCenterAnimation: StatusBarMoveFromCenterAnimationController
65     @Mock
66     private lateinit var sysuiUnfoldComponent: SysUIUnfoldComponent
67     @Mock
68     private lateinit var progressProvider: ScopedUnfoldTransitionProgressProvider
69     @Mock
70     private lateinit var configurationController: ConfigurationController
71     @Mock
72     private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
73     @Mock
74     private lateinit var userChipViewModel: StatusBarUserChipViewModel
75     @Mock
76     private lateinit var centralSurfacesImpl: CentralSurfacesImpl
77     @Mock
78     private lateinit var shadeControllerImpl: ShadeControllerImpl
79     @Mock
80     private lateinit var windowRootView: Provider<WindowRootView>
81     @Mock
82     private lateinit var shadeLogger: ShadeLogger
83     @Mock
84     private lateinit var viewUtil: ViewUtil
85 
86     private lateinit var view: PhoneStatusBarView
87     private lateinit var controller: PhoneStatusBarViewController
88 
89     private val unfoldConfig = UnfoldConfig()
90 
91     @Before
92     fun setUp() {
93         MockitoAnnotations.initMocks(this)
94         `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController())
95             .thenReturn(moveFromCenterAnimation)
96         // create the view and controller on main thread as it requires main looper
97         InstrumentationRegistry.getInstrumentation().runOnMainSync {
98             val parent = FrameLayout(mContext) // add parent to keep layout params
99             view = LayoutInflater.from(mContext)
100                 .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView
101             controller = createAndInitController(view)
102         }
103     }
104 
105     @Test
106     fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() {
107         val view = createViewMock()
108         val argumentCaptor = ArgumentCaptor.forClass(
109                 ConfigurationController.ConfigurationListener::class.java)
110         InstrumentationRegistry.getInstrumentation().runOnMainSync {
111             controller = createAndInitController(view)
112         }
113 
114         verify(configurationController).addCallback(argumentCaptor.capture())
115         argumentCaptor.value.onDensityOrFontScaleChanged()
116 
117         verify(view).onDensityOrFontScaleChanged()
118     }
119 
120     @Test
121     fun onViewAttachedAndDrawn_moveFromCenterAnimationEnabled_moveFromCenterAnimationInitialized() {
122         whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS))
123                 .thenReturn(true)
124         val view = createViewMock()
125         val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java)
126         unfoldConfig.isEnabled = true
127         // create the controller on main thread as it requires main looper
128         InstrumentationRegistry.getInstrumentation().runOnMainSync {
129             controller = createAndInitController(view)
130         }
131 
132         verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture())
133         argumentCaptor.value.onPreDraw()
134 
135         verify(moveFromCenterAnimation).onViewsReady(any())
136     }
137 
138     @Test
139     fun onViewAttachedAndDrawn_statusBarAnimationDisabled_animationNotInitialized() {
140         whenever(featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS))
141                 .thenReturn(false)
142         val view = createViewMock()
143         unfoldConfig.isEnabled = true
144         // create the controller on main thread as it requires main looper
145         InstrumentationRegistry.getInstrumentation().runOnMainSync {
146             controller = createAndInitController(view)
147         }
148 
149         verify(moveFromCenterAnimation, never()).onViewsReady(any())
150     }
151 
152     @Test
153     fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
154         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false)
155         val returnVal = view.onTouchEvent(
156                         MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
157         assertThat(returnVal).isFalse()
158         verify(shadeViewController, never()).handleExternalTouch(any())
159     }
160 
161     @Test
162     fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() {
163         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
164         `when`(shadeViewController.isViewEnabled).thenReturn(false)
165         val returnVal = view.onTouchEvent(
166                 MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
167         assertThat(returnVal).isTrue()
168         verify(shadeViewController, never()).handleExternalTouch(any())
169     }
170 
171     @Test
172     fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
173         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
174         `when`(shadeViewController.isViewEnabled).thenReturn(false)
175         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
176 
177         view.onTouchEvent(event)
178 
179         verify(shadeViewController).handleExternalTouch(event)
180     }
181 
182     @Test
183     fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() {
184         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
185         `when`(shadeViewController.isViewEnabled).thenReturn(true)
186         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
187 
188         view.onTouchEvent(event)
189 
190         verify(shadeViewController).handleExternalTouch(event)
191     }
192 
193     @Test
194     fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() {
195         `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
196         `when`(shadeViewController.isFullyCollapsed).thenReturn(true)
197         val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
198 
199         view.onTouchEvent(event)
200 
201         verify(shadeViewController, never()).handleExternalTouch(any())
202     }
203 
204     private fun createViewMock(): PhoneStatusBarView {
205         val view = spy(view)
206         val viewTreeObserver = mock(ViewTreeObserver::class.java)
207         `when`(view.viewTreeObserver).thenReturn(viewTreeObserver)
208         `when`(view.isAttachedToWindow).thenReturn(true)
209         return view
210     }
211 
212     private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController {
213         return PhoneStatusBarViewController.Factory(
214             Optional.of(sysuiUnfoldComponent),
215             Optional.of(progressProvider),
216             featureFlags,
217             userChipViewModel,
218             centralSurfacesImpl,
219             shadeControllerImpl,
220             shadeViewController,
221             windowRootView,
222             shadeLogger,
223             viewUtil,
224             configurationController,
225             mStatusOverlayHoverListenerFactory
226         ).create(view).also {
227             it.init()
228         }
229     }
230 
231     private class UnfoldConfig : UnfoldTransitionConfig {
232         override var isEnabled: Boolean = false
233         override var isHingeAngleEnabled: Boolean = false
234         override val halfFoldedTimeoutMillis: Int = 0
235     }
236 }
237