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