1 /* <lambda>null2 * Copyright (C) 2023 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.scene.ui.viewmodel 20 21 import android.platform.test.annotations.DisableFlags 22 import android.platform.test.annotations.EnableFlags 23 import android.view.MotionEvent 24 import android.view.MotionEvent.ACTION_DOWN 25 import android.view.MotionEvent.ACTION_OUTSIDE 26 import android.view.View 27 import androidx.test.ext.junit.runners.AndroidJUnit4 28 import androidx.test.filters.SmallTest 29 import com.android.compose.animation.scene.DefaultEdgeDetector 30 import com.android.systemui.SysuiTestCase 31 import com.android.systemui.classifier.fakeFalsingManager 32 import com.android.systemui.coroutines.collectLastValue 33 import com.android.systemui.flags.EnableSceneContainer 34 import com.android.systemui.kosmos.testScope 35 import com.android.systemui.lifecycle.activateIn 36 import com.android.systemui.power.data.repository.fakePowerRepository 37 import com.android.systemui.scene.domain.interactor.sceneInteractor 38 import com.android.systemui.scene.fakeOverlaysByKeys 39 import com.android.systemui.scene.sceneContainerConfig 40 import com.android.systemui.scene.sceneContainerViewModelFactory 41 import com.android.systemui.scene.shared.model.Overlays 42 import com.android.systemui.scene.shared.model.Scenes 43 import com.android.systemui.scene.shared.model.fakeSceneDataSource 44 import com.android.systemui.shade.data.repository.fakeShadeRepository 45 import com.android.systemui.shade.domain.interactor.shadeInteractor 46 import com.android.systemui.shade.shared.flag.DualShade 47 import com.android.systemui.shade.shared.model.ShadeMode 48 import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository 49 import com.android.systemui.testKosmos 50 import com.android.systemui.util.mockito.mock 51 import com.android.systemui.util.mockito.whenever 52 import com.google.common.truth.Truth.assertThat 53 import com.google.common.truth.Truth.assertWithMessage 54 import kotlinx.coroutines.ExperimentalCoroutinesApi 55 import kotlinx.coroutines.Job 56 import kotlinx.coroutines.test.runCurrent 57 import kotlinx.coroutines.test.runTest 58 import org.junit.Before 59 import org.junit.Test 60 import org.junit.runner.RunWith 61 62 @OptIn(ExperimentalCoroutinesApi::class) 63 @SmallTest 64 @RunWith(AndroidJUnit4::class) 65 @EnableSceneContainer 66 class SceneContainerViewModelTest : SysuiTestCase() { 67 68 private val kosmos = testKosmos() 69 private val testScope by lazy { kosmos.testScope } 70 private val sceneInteractor by lazy { kosmos.sceneInteractor } 71 private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } 72 private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } 73 private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } 74 private val fakeRemoteInputRepository by lazy { kosmos.fakeRemoteInputRepository } 75 private val falsingManager by lazy { kosmos.fakeFalsingManager } 76 private val view = mock<View>() 77 78 private lateinit var underTest: SceneContainerViewModel 79 80 private lateinit var activationJob: Job 81 private var motionEventHandler: SceneContainerViewModel.MotionEventHandler? = null 82 83 @Before 84 fun setUp() { 85 underTest = 86 kosmos.sceneContainerViewModelFactory.create( 87 view, 88 { motionEventHandler -> 89 this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler 90 }, 91 ) 92 activationJob = Job() 93 underTest.activateIn(testScope, activationJob) 94 } 95 96 @Test 97 fun activate_setsMotionEventHandler() = 98 testScope.runTest { 99 runCurrent() 100 assertThat(motionEventHandler).isNotNull() 101 } 102 103 @Test 104 fun deactivate_clearsMotionEventHandler() = 105 testScope.runTest { 106 activationJob.cancel() 107 runCurrent() 108 109 assertThat(motionEventHandler).isNull() 110 } 111 112 @Test 113 fun isVisible() = 114 testScope.runTest { 115 assertThat(underTest.isVisible).isTrue() 116 117 sceneInteractor.setVisible(false, "reason") 118 runCurrent() 119 assertThat(underTest.isVisible).isFalse() 120 121 sceneInteractor.setVisible(true, "reason") 122 runCurrent() 123 assertThat(underTest.isVisible).isTrue() 124 } 125 126 @Test 127 fun sceneTransition() = 128 testScope.runTest { 129 val currentScene by collectLastValue(underTest.currentScene) 130 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 131 132 fakeSceneDataSource.changeScene(Scenes.Shade) 133 134 assertThat(currentScene).isEqualTo(Scenes.Shade) 135 } 136 137 @Test 138 fun canChangeScene_whenAllowed_switchingFromGone_returnsTrue() = 139 testScope.runTest { 140 val currentScene by collectLastValue(underTest.currentScene) 141 fakeSceneDataSource.changeScene(toScene = Scenes.Gone) 142 runCurrent() 143 assertThat(currentScene).isEqualTo(Scenes.Gone) 144 145 sceneContainerConfig.sceneKeys 146 .filter { it != currentScene } 147 .forEach { toScene -> 148 assertWithMessage("Scene $toScene incorrectly protected when allowed") 149 .that(underTest.canChangeScene(toScene = toScene)) 150 .isTrue() 151 } 152 } 153 154 @Test 155 fun canChangeScene_whenAllowed_switchingFromLockscreen_returnsTrue() = 156 testScope.runTest { 157 val currentScene by collectLastValue(underTest.currentScene) 158 fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) 159 runCurrent() 160 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 161 162 sceneContainerConfig.sceneKeys 163 .filter { it != currentScene } 164 .forEach { toScene -> 165 assertWithMessage("Scene $toScene incorrectly protected when allowed") 166 .that(underTest.canChangeScene(toScene = toScene)) 167 .isTrue() 168 } 169 } 170 171 @Test 172 fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingProtectedScenes_returnsFalse() = 173 testScope.runTest { 174 falsingManager.setIsFalseTouch(true) 175 val currentScene by collectLastValue(underTest.currentScene) 176 fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) 177 runCurrent() 178 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 179 180 sceneContainerConfig.sceneKeys 181 .filter { it != currentScene } 182 .filter { 183 // Moving to the Communal and Dream scene is not currently falsing protected. 184 it != Scenes.Communal && it != Scenes.Dream 185 } 186 .forEach { toScene -> 187 assertWithMessage("Protected scene $toScene not properly protected") 188 .that(underTest.canChangeScene(toScene = toScene)) 189 .isFalse() 190 } 191 } 192 193 @Test 194 fun canChangeScene_whenNotAllowed_fromLockscreen_toFalsingUnprotectedScenes_returnsTrue() = 195 testScope.runTest { 196 falsingManager.setIsFalseTouch(true) 197 val currentScene by collectLastValue(underTest.currentScene) 198 fakeSceneDataSource.changeScene(toScene = Scenes.Lockscreen) 199 runCurrent() 200 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 201 202 sceneContainerConfig.sceneKeys 203 .filter { 204 // Moving to the Communal scene is not currently falsing protected. 205 it == Scenes.Communal 206 } 207 .forEach { toScene -> 208 assertWithMessage("Unprotected scene $toScene is incorrectly protected") 209 .that(underTest.canChangeScene(toScene = toScene)) 210 .isTrue() 211 } 212 } 213 214 @Test 215 fun canChangeScene_whenNotAllowed_fromGone_toAnyOtherScene_returnsTrue() = 216 testScope.runTest { 217 falsingManager.setIsFalseTouch(true) 218 val currentScene by collectLastValue(underTest.currentScene) 219 fakeSceneDataSource.changeScene(toScene = Scenes.Gone) 220 runCurrent() 221 assertThat(currentScene).isEqualTo(Scenes.Gone) 222 223 sceneContainerConfig.sceneKeys 224 .filter { it != currentScene } 225 .forEach { toScene -> 226 assertWithMessage("Protected scene $toScene not properly protected") 227 .that(underTest.canChangeScene(toScene = toScene)) 228 .isTrue() 229 } 230 } 231 232 @Test 233 fun userInput() = 234 testScope.runTest { 235 assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() 236 underTest.onMotionEvent(mock()) 237 assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() 238 } 239 240 @Test 241 fun userInputOnEmptySpace_insideEvent() = 242 testScope.runTest { 243 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() 244 val insideMotionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) 245 underTest.onEmptySpaceMotionEvent(insideMotionEvent) 246 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() 247 } 248 249 @Test 250 fun userInputOnEmptySpace_outsideEvent_remoteInputActive() = 251 testScope.runTest { 252 fakeRemoteInputRepository.isRemoteInputActive.value = true 253 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() 254 val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) 255 underTest.onEmptySpaceMotionEvent(outsideMotionEvent) 256 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isTrue() 257 } 258 259 @Test 260 fun userInputOnEmptySpace_outsideEvent_remoteInputInactive() = 261 testScope.runTest { 262 fakeRemoteInputRepository.isRemoteInputActive.value = false 263 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() 264 val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) 265 underTest.onEmptySpaceMotionEvent(outsideMotionEvent) 266 assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() 267 } 268 269 @Test 270 fun remoteUserInteraction_keepsContainerVisible() = 271 testScope.runTest { 272 sceneInteractor.setVisible(false, "reason") 273 runCurrent() 274 assertThat(underTest.isVisible).isFalse() 275 sceneInteractor.onRemoteUserInputStarted("reason") 276 runCurrent() 277 assertThat(underTest.isVisible).isTrue() 278 279 underTest.onMotionEvent( 280 mock { whenever(actionMasked).thenReturn(MotionEvent.ACTION_UP) } 281 ) 282 runCurrent() 283 284 assertThat(underTest.isVisible).isFalse() 285 } 286 287 @Test 288 fun getActionableContentKey_noOverlays_returnsCurrentScene() = 289 testScope.runTest { 290 val currentScene by collectLastValue(underTest.currentScene) 291 val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) 292 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 293 assertThat(currentOverlays).isEmpty() 294 295 val actionableContentKey = 296 underTest.getActionableContentKey( 297 currentScene = checkNotNull(currentScene), 298 currentOverlays = checkNotNull(currentOverlays), 299 overlayByKey = kosmos.fakeOverlaysByKeys, 300 ) 301 302 assertThat(actionableContentKey).isEqualTo(Scenes.Lockscreen) 303 } 304 305 @Test 306 fun getActionableContentKey_multipleOverlays_returnsTopOverlay() = 307 testScope.runTest { 308 val currentScene by collectLastValue(underTest.currentScene) 309 val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) 310 fakeSceneDataSource.showOverlay(Overlays.QuickSettingsShade) 311 fakeSceneDataSource.showOverlay(Overlays.NotificationsShade) 312 assertThat(currentScene).isEqualTo(Scenes.Lockscreen) 313 assertThat(currentOverlays) 314 .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade) 315 316 val actionableContentKey = 317 underTest.getActionableContentKey( 318 currentScene = checkNotNull(currentScene), 319 currentOverlays = checkNotNull(currentOverlays), 320 overlayByKey = kosmos.fakeOverlaysByKeys, 321 ) 322 323 assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade) 324 } 325 326 @Test 327 @DisableFlags(DualShade.FLAG_NAME) 328 fun edgeDetector_singleShade_usesDefaultEdgeDetector() = 329 testScope.runTest { 330 val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) 331 fakeShadeRepository.setShadeLayoutWide(false) 332 assertThat(shadeMode).isEqualTo(ShadeMode.Single) 333 334 assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) 335 } 336 337 @Test 338 @DisableFlags(DualShade.FLAG_NAME) 339 fun edgeDetector_splitShade_usesDefaultEdgeDetector() = 340 testScope.runTest { 341 val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) 342 fakeShadeRepository.setShadeLayoutWide(true) 343 assertThat(shadeMode).isEqualTo(ShadeMode.Split) 344 345 assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) 346 } 347 348 @Test 349 @EnableFlags(DualShade.FLAG_NAME) 350 fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() = 351 testScope.runTest { 352 val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) 353 fakeShadeRepository.setShadeLayoutWide(false) 354 355 assertThat(shadeMode).isEqualTo(ShadeMode.Dual) 356 assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) 357 } 358 359 @Test 360 @EnableFlags(DualShade.FLAG_NAME) 361 fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() = 362 testScope.runTest { 363 val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) 364 fakeShadeRepository.setShadeLayoutWide(true) 365 366 assertThat(shadeMode).isEqualTo(ShadeMode.Dual) 367 assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) 368 } 369 } 370