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