1 /*
2  * Copyright (C) 2024 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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.statusbar
20 
21 import android.animation.ObjectAnimator
22 import android.platform.test.flag.junit.FlagsParameterization
23 import android.testing.TestableLooper
24 import androidx.test.filters.SmallTest
25 import com.android.internal.logging.testing.UiEventLoggerFake
26 import com.android.systemui.SysuiTestCase
27 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
28 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
29 import com.android.systemui.coroutines.collectLastValue
30 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
31 import com.android.systemui.flags.DisableSceneContainer
32 import com.android.systemui.flags.EnableSceneContainer
33 import com.android.systemui.flags.parameterizeSceneContainerFlag
34 import com.android.systemui.jank.interactionJankMonitor
35 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
36 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
37 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
38 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
39 import com.android.systemui.keyguard.shared.model.KeyguardState
40 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
41 import com.android.systemui.kosmos.testScope
42 import com.android.systemui.plugins.statusbar.StatusBarStateController
43 import com.android.systemui.scene.domain.interactor.sceneInteractor
44 import com.android.systemui.scene.shared.model.Scenes
45 import com.android.systemui.shade.domain.interactor.shadeInteractor
46 import com.android.systemui.testKosmos
47 import com.android.systemui.util.kotlin.JavaAdapter
48 import com.android.systemui.util.mockito.mock
49 import com.google.common.truth.Truth.assertThat
50 import kotlinx.coroutines.ExperimentalCoroutinesApi
51 import kotlinx.coroutines.test.runCurrent
52 import kotlinx.coroutines.test.runTest
53 import org.junit.Assert.assertEquals
54 import org.junit.Assert.assertFalse
55 import org.junit.Assert.assertTrue
56 import org.junit.Before
57 import org.junit.Test
58 import org.junit.runner.RunWith
59 import org.mockito.ArgumentMatchers.anyFloat
60 import org.mockito.ArgumentMatchers.eq
61 import org.mockito.Mockito
62 import org.mockito.Mockito.mock
63 import org.mockito.Mockito.verify
64 import org.mockito.MockitoAnnotations
65 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
66 import platform.test.runner.parameterized.Parameters
67 
68 @SmallTest
69 @RunWith(ParameterizedAndroidJunit4::class)
70 @TestableLooper.RunWithLooper
71 class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
72 
73     private val kosmos = testKosmos()
74     private val testScope = kosmos.testScope
75     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
76     private val mockDarkAnimator = mock<ObjectAnimator>()
77 
78     private lateinit var underTest: StatusBarStateControllerImpl
79     private lateinit var uiEventLogger: UiEventLoggerFake
80 
81     companion object {
82         @JvmStatic
83         @Parameters(name = "{0}")
getParamsnull84         fun getParams(): List<FlagsParameterization> {
85             return parameterizeSceneContainerFlag()
86         }
87     }
88 
89     init {
90         mSetFlagsRule.setFlagsParameterization(flags)
91     }
92 
93     @Before
setUpnull94     fun setUp() {
95         MockitoAnnotations.initMocks(this)
96 
97         uiEventLogger = UiEventLoggerFake()
98         underTest =
99             object :
100                 StatusBarStateControllerImpl(
101                     uiEventLogger,
102                     { kosmos.interactionJankMonitor },
103                     JavaAdapter(testScope.backgroundScope),
104                     { kosmos.keyguardTransitionInteractor },
105                     { kosmos.shadeInteractor },
106                     { kosmos.deviceUnlockedInteractor },
107                     { kosmos.sceneInteractor },
108                     { kosmos.keyguardClockInteractor },
109                 ) {
110                 override fun createDarkAnimator(): ObjectAnimator {
111                     return mockDarkAnimator
112                 }
113             }
114     }
115 
116     @Test
117     @DisableSceneContainer
testChangeState_loggednull118     fun testChangeState_logged() {
119         TestableLooper.get(this).runWithLooper {
120             underTest.state = StatusBarState.KEYGUARD
121             underTest.state = StatusBarState.SHADE
122             underTest.state = StatusBarState.SHADE_LOCKED
123         }
124 
125         val logs = uiEventLogger.logs
126         assertEquals(3, logs.size)
127         val ids = logs.map(UiEventLoggerFake.FakeUiEvent::eventId)
128         assertEquals(StatusBarStateEvent.STATUS_BAR_STATE_KEYGUARD.id, ids[0])
129         assertEquals(StatusBarStateEvent.STATUS_BAR_STATE_SHADE.id, ids[1])
130         assertEquals(StatusBarStateEvent.STATUS_BAR_STATE_SHADE_LOCKED.id, ids[2])
131     }
132 
133     @Test
testSetDozeAmountInternal_onlySetsOncenull134     fun testSetDozeAmountInternal_onlySetsOnce() {
135         val listener = mock(StatusBarStateController.StateListener::class.java)
136         underTest.addCallback(listener)
137 
138         underTest.setAndInstrumentDozeAmount(null, 0.5f, false /* animated */)
139         underTest.setAndInstrumentDozeAmount(null, 0.5f, false /* animated */)
140         verify(listener).onDozeAmountChanged(eq(0.5f), anyFloat())
141     }
142 
143     @Test
144     @DisableSceneContainer
testSetState_appliesState_sameStateButDifferentUpcomingStatenull145     fun testSetState_appliesState_sameStateButDifferentUpcomingState() {
146         underTest.state = StatusBarState.SHADE
147         underTest.setUpcomingState(StatusBarState.KEYGUARD)
148 
149         assertEquals(underTest.state, StatusBarState.SHADE)
150 
151         // We should return true (state change was applied) despite going from SHADE to SHADE, since
152         // the upcoming state was set to KEYGUARD.
153         assertTrue(underTest.setState(StatusBarState.SHADE))
154     }
155 
156     @Test
157     @DisableSceneContainer
testSetState_appliesState_differentStateEqualToUpcomingStatenull158     fun testSetState_appliesState_differentStateEqualToUpcomingState() {
159         underTest.state = StatusBarState.SHADE
160         underTest.setUpcomingState(StatusBarState.KEYGUARD)
161 
162         assertEquals(underTest.state, StatusBarState.SHADE)
163 
164         // Make sure we apply a SHADE -> KEYGUARD state change when the upcoming state is KEYGUARD.
165         assertTrue(underTest.setState(StatusBarState.KEYGUARD))
166     }
167 
168     @Test
169     @DisableSceneContainer
testSetState_doesNotApplyState_currentAndUpcomingStatesSamenull170     fun testSetState_doesNotApplyState_currentAndUpcomingStatesSame() {
171         underTest.state = StatusBarState.SHADE
172         underTest.setUpcomingState(StatusBarState.SHADE)
173 
174         assertEquals(underTest.state, StatusBarState.SHADE)
175 
176         // We're going from SHADE -> SHADE, and the upcoming state is also SHADE, this should not do
177         // anything.
178         assertFalse(underTest.setState(StatusBarState.SHADE))
179 
180         // Double check that we can still force it to happen.
181         assertTrue(underTest.setState(StatusBarState.SHADE, true /* force */))
182     }
183 
184     @Test
testSetDozeAmount_immediatelyChangesDozeAmount_lockscreenTransitionFromAodnull185     fun testSetDozeAmount_immediatelyChangesDozeAmount_lockscreenTransitionFromAod() {
186         // Put controller in AOD state
187         underTest.setAndInstrumentDozeAmount(null, 1f, false)
188 
189         // When waking from doze, CentralSurfaces#updateDozingState will update the dozing state
190         // before the doze amount changes
191         underTest.setIsDozing(false)
192 
193         // Animate the doze amount to 0f, as would normally happen
194         underTest.setAndInstrumentDozeAmount(null, 0f, true)
195 
196         // Check that the doze amount is immediately set to a value slightly less than 1f. This is
197         // to ensure that any scrim implementation changes its opacity immediately rather than
198         // waiting an extra frame. Waiting an extra frame will cause a relayout (which is expensive)
199         // and cause us to drop a frame during the LOCKSCREEN_TRANSITION_FROM_AOD CUJ.
200         assertEquals(0.99f, underTest.dozeAmount, 0.009f)
201     }
202 
203     @Test
testSetDreamState_invokesCallbacknull204     fun testSetDreamState_invokesCallback() {
205         val listener = mock(StatusBarStateController.StateListener::class.java)
206         underTest.addCallback(listener)
207 
208         underTest.setIsDreaming(true)
209         verify(listener).onDreamingChanged(true)
210 
211         Mockito.clearInvocations(listener)
212 
213         underTest.setIsDreaming(false)
214         verify(listener).onDreamingChanged(false)
215     }
216 
217     @Test
testSetDreamState_getterReturnsCurrentStatenull218     fun testSetDreamState_getterReturnsCurrentState() {
219         underTest.setIsDreaming(true)
220         assertTrue(underTest.isDreaming())
221 
222         underTest.setIsDreaming(false)
223         assertFalse(underTest.isDreaming())
224     }
225 
226     @Test
227     @EnableSceneContainer
start_hydratesStatusBarState_whileLockednull228     fun start_hydratesStatusBarState_whileLocked() =
229         testScope.runTest {
230             var statusBarState = underTest.state
231             val listener =
232                 object : StatusBarStateController.StateListener {
233                     override fun onStateChanged(newState: Int) {
234                         statusBarState = newState
235                     }
236                 }
237             underTest.addCallback(listener)
238 
239             val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
240             val deviceUnlockStatus by
241                 collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
242 
243             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
244                 AuthenticationMethodModel.Password
245             )
246             runCurrent()
247             assertThat(deviceUnlockStatus!!.isUnlocked).isFalse()
248 
249             kosmos.sceneInteractor.changeScene(
250                 toScene = Scenes.Lockscreen,
251                 loggingReason = "reason"
252             )
253             runCurrent()
254             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
255 
256             // Call start to begin hydrating based on the scene framework:
257             underTest.start()
258 
259             kosmos.sceneInteractor.changeScene(toScene = Scenes.Bouncer, loggingReason = "reason")
260             runCurrent()
261             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
262             assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
263 
264             kosmos.sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = "reason")
265             runCurrent()
266             assertThat(currentScene).isEqualTo(Scenes.Shade)
267             assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED)
268 
269             kosmos.sceneInteractor.changeScene(
270                 toScene = Scenes.QuickSettings,
271                 loggingReason = "reason"
272             )
273             runCurrent()
274             assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
275             assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED)
276 
277             kosmos.sceneInteractor.changeScene(toScene = Scenes.Communal, loggingReason = "reason")
278             runCurrent()
279             assertThat(currentScene).isEqualTo(Scenes.Communal)
280             assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
281 
282             kosmos.sceneInteractor.changeScene(
283                 toScene = Scenes.Lockscreen,
284                 loggingReason = "reason"
285             )
286             runCurrent()
287             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
288             assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
289         }
290 
291     @Test
292     @EnableSceneContainer
start_hydratesStatusBarState_whileUnlockednull293     fun start_hydratesStatusBarState_whileUnlocked() =
294         testScope.runTest {
295             var statusBarState = underTest.state
296             val listener =
297                 object : StatusBarStateController.StateListener {
298                     override fun onStateChanged(newState: Int) {
299                         statusBarState = newState
300                     }
301                 }
302             underTest.addCallback(listener)
303 
304             val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
305             val deviceUnlockStatus by
306                 collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
307             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
308                 AuthenticationMethodModel.Password
309             )
310             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
311                 SuccessFingerprintAuthenticationStatus(0, true)
312             )
313             runCurrent()
314 
315             assertThat(deviceUnlockStatus!!.isUnlocked).isTrue()
316 
317             kosmos.sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
318             runCurrent()
319             assertThat(currentScene).isEqualTo(Scenes.Gone)
320 
321             // Call start to begin hydrating based on the scene framework:
322             underTest.start()
323 
324             kosmos.sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = "reason")
325             runCurrent()
326             assertThat(currentScene).isEqualTo(Scenes.Shade)
327             assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
328 
329             kosmos.sceneInteractor.changeScene(
330                 toScene = Scenes.QuickSettings,
331                 loggingReason = "reason"
332             )
333             runCurrent()
334             assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
335             assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
336         }
337 
338     @Test
leaveOpenOnKeyguard_whenGone_isFalsenull339     fun leaveOpenOnKeyguard_whenGone_isFalse() =
340         testScope.runTest {
341             underTest.start()
342             underTest.setLeaveOpenOnKeyguardHide(true)
343 
344             keyguardTransitionRepository.sendTransitionSteps(
345                 from = KeyguardState.AOD,
346                 to = KeyguardState.LOCKSCREEN,
347                 testScope = testScope,
348             )
349             assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(true)
350 
351             keyguardTransitionRepository.sendTransitionSteps(
352                 from = KeyguardState.LOCKSCREEN,
353                 to = KeyguardState.GONE,
354                 testScope = testScope,
355             )
356             assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(false)
357         }
358 }
359