1 /*
<lambda>null2  * 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 package com.android.systemui.bouncer.ui.composable
18 
19 import android.app.AlertDialog
20 import android.platform.test.annotations.MotionTest
21 import android.testing.TestableLooper.RunWithLooper
22 import android.view.View
23 import androidx.activity.BackEventCompat
24 import androidx.compose.animation.core.Animatable
25 import androidx.compose.animation.core.tween
26 import androidx.compose.foundation.layout.Box
27 import androidx.compose.material3.Text
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.LaunchedEffect
30 import androidx.compose.runtime.remember
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.geometry.Offset
34 import androidx.compose.ui.geometry.isFinite
35 import androidx.compose.ui.geometry.isUnspecified
36 import androidx.compose.ui.semantics.SemanticsNode
37 import androidx.compose.ui.test.junit4.AndroidComposeTestRule
38 import androidx.test.ext.junit.runners.AndroidJUnit4
39 import androidx.test.filters.LargeTest
40 import com.android.compose.animation.scene.ObservableTransitionState
41 import com.android.compose.animation.scene.Scale
42 import com.android.compose.animation.scene.SceneKey
43 import com.android.compose.animation.scene.SceneScope
44 import com.android.compose.animation.scene.UserAction
45 import com.android.compose.animation.scene.UserActionResult
46 import com.android.compose.animation.scene.isElement
47 import com.android.compose.animation.scene.testing.lastAlphaForTesting
48 import com.android.compose.animation.scene.testing.lastScaleForTesting
49 import com.android.compose.theme.PlatformTheme
50 import com.android.systemui.SysuiTestCase
51 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
52 import com.android.systemui.bouncer.ui.BouncerDialogFactory
53 import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
54 import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
55 import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
56 import com.android.systemui.flags.EnableSceneContainer
57 import com.android.systemui.kosmos.Kosmos
58 import com.android.systemui.kosmos.Kosmos.Fixture
59 import com.android.systemui.lifecycle.ExclusiveActivatable
60 import com.android.systemui.lifecycle.rememberViewModel
61 import com.android.systemui.motion.createSysUiComposeMotionTestRule
62 import com.android.systemui.qs.ui.viewmodel.fakeQsSceneAdapter
63 import com.android.systemui.scene.domain.interactor.sceneInteractor
64 import com.android.systemui.scene.domain.startable.sceneContainerStartable
65 import com.android.systemui.scene.sceneContainerViewModelFactory
66 import com.android.systemui.scene.shared.model.SceneContainerConfig
67 import com.android.systemui.scene.shared.model.Scenes
68 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
69 import com.android.systemui.scene.ui.composable.Scene
70 import com.android.systemui.scene.ui.composable.SceneContainer
71 import com.android.systemui.scene.ui.composable.SceneContainerTransitions
72 import com.android.systemui.testKosmos
73 import kotlin.time.Duration.Companion.seconds
74 import kotlinx.coroutines.awaitCancellation
75 import kotlinx.coroutines.flow.Flow
76 import kotlinx.coroutines.flow.MutableStateFlow
77 import kotlinx.coroutines.flow.flowOf
78 import org.json.JSONObject
79 import org.junit.Before
80 import org.junit.Rule
81 import org.junit.Test
82 import org.junit.runner.RunWith
83 import org.mockito.MockitoAnnotations
84 import org.mockito.kotlin.mock
85 import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
86 import platform.test.motion.compose.ComposeRecordingSpec
87 import platform.test.motion.compose.MotionControl
88 import platform.test.motion.compose.feature
89 import platform.test.motion.compose.recordMotion
90 import platform.test.motion.compose.runTest
91 import platform.test.motion.golden.DataPoint
92 import platform.test.motion.golden.DataPointType
93 import platform.test.motion.golden.DataPointTypes
94 import platform.test.motion.golden.FeatureCapture
95 import platform.test.motion.golden.UnknownTypeException
96 import platform.test.screenshot.DeviceEmulationSpec
97 import platform.test.screenshot.Displays.Phone
98 
99 /** MotionTest for the Bouncer Predictive Back animation */
100 @LargeTest
101 @RunWith(AndroidJUnit4::class)
102 @RunWithLooper
103 @EnableSceneContainer
104 @MotionTest
105 class BouncerPredictiveBackTest : SysuiTestCase() {
106 
107     private val deviceSpec = DeviceEmulationSpec(Phone)
108     private val kosmos = testKosmos()
109 
110     @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
111     private val androidComposeTestRule =
112         motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
113 
114     private val sceneInteractor by lazy { kosmos.sceneInteractor }
115     private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
116     private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
117     private val Kosmos.sceneContainerConfig by Fixture {
118         val navigationDistances = mapOf(Scenes.Lockscreen to 1, Scenes.Bouncer to 0)
119         SceneContainerConfig(
120             sceneKeys,
121             initialSceneKey,
122             SceneContainerTransitions,
123             emptyList(),
124             navigationDistances,
125         )
126     }
127     private val view = mock<View>()
128 
129     private val transitionState by lazy {
130         MutableStateFlow<ObservableTransitionState>(
131             ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
132         )
133     }
134 
135     private val sceneContainerViewModel by lazy {
136         kosmos.sceneContainerViewModelFactory
137             .create(view) {}
138             .apply { setTransitionState(transitionState) }
139     }
140 
141     private val bouncerDialogFactory =
142         object : BouncerDialogFactory {
143             override fun invoke(): AlertDialog {
144                 throw AssertionError()
145             }
146         }
147     private val bouncerSceneActionsViewModelFactory =
148         object : BouncerUserActionsViewModel.Factory {
149             override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
150         }
151     private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
152     private val bouncerSceneContentViewModelFactory =
153         object : BouncerSceneContentViewModel.Factory {
154             override fun create() = bouncerSceneContentViewModel
155         }
156     private val bouncerScene =
157         BouncerScene(
158             bouncerSceneActionsViewModelFactory,
159             bouncerSceneContentViewModelFactory,
160             bouncerDialogFactory,
161         )
162 
163     @Before
164     fun setUp() {
165         MockitoAnnotations.initMocks(this)
166 
167         bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
168 
169         val startable = kosmos.sceneContainerStartable
170         startable.start()
171     }
172 
173     @Test
174     fun bouncerPredictiveBackMotion() =
175         motionTestRule.runTest(timeout = 30.seconds) {
176             val motion =
177                 recordMotion(
178                     content = { play ->
179                         PlatformTheme {
180                             BackGestureAnimation(play)
181                             SceneContainer(
182                                 viewModel =
183                                     rememberViewModel("BouncerPredictiveBackTest") {
184                                         sceneContainerViewModel
185                                     },
186                                 sceneByKey =
187                                     mapOf(
188                                         Scenes.Lockscreen to FakeLockscreen(),
189                                         Scenes.Bouncer to bouncerScene,
190                                     ),
191                                 initialSceneKey = Scenes.Bouncer,
192                                 sceneTransitions = SceneContainerTransitions,
193                                 overlayByKey = emptyMap(),
194                                 dataSourceDelegator = kosmos.sceneDataSourceDelegator,
195                                 qsSceneAdapter = { kosmos.fakeQsSceneAdapter },
196                             )
197                         }
198                     },
199                     ComposeRecordingSpec(
200                         MotionControl(
201                             delayRecording = {
202                                 awaitCondition {
203                                     sceneInteractor.transitionState.value.isTransitioning()
204                                 }
205                             }
206                         ) {
207                             awaitCondition {
208                                 sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
209                             }
210                         }
211                     ) {
212                         feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
213                         feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
214                         feature(
215                             isElement(Bouncer.Elements.Content),
216                             positionInRoot,
217                             "content_offset",
218                         )
219                         feature(
220                             isElement(Bouncer.Elements.Background),
221                             elementAlpha,
222                             "background_alpha",
223                         )
224                     },
225                 )
226 
227             assertThat(motion).timeSeriesMatchesGolden()
228         }
229 
230     @Composable
231     private fun BackGestureAnimation(play: Boolean) {
232         val backProgress = remember { Animatable(0f) }
233 
234         LaunchedEffect(play) {
235             if (play) {
236                 val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
237                 androidComposeTestRule.runOnUiThread {
238                     dispatcher.dispatchOnBackStarted(backEvent())
239                 }
240                 backProgress.animateTo(
241                     targetValue = 1f,
242                     animationSpec = tween(durationMillis = 500),
243                 ) {
244                     androidComposeTestRule.runOnUiThread {
245                         dispatcher.dispatchOnBackProgressed(
246                             backEvent(progress = backProgress.value)
247                         )
248                         if (backProgress.value == 1f) {
249                             dispatcher.onBackPressed()
250                         }
251                     }
252                 }
253             }
254         }
255     }
256 
257     private fun backEvent(progress: Float = 0f): BackEventCompat {
258         return BackEventCompat(
259             touchX = 0f,
260             touchY = 0f,
261             progress = progress,
262             swipeEdge = BackEventCompat.EDGE_LEFT,
263         )
264     }
265 
266     private class FakeLockscreen : ExclusiveActivatable(), Scene {
267         override val key: SceneKey = Scenes.Lockscreen
268         override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
269 
270         @Composable
271         override fun SceneScope.Content(modifier: Modifier) {
272             Box(modifier = modifier, contentAlignment = Alignment.Center) {
273                 Text(text = "Fake Lockscreen")
274             }
275         }
276 
277         override suspend fun onActivated() = awaitCancellation()
278     }
279 
280     companion object {
281         private val elementAlpha =
282             FeatureCapture<SemanticsNode, Float>("alpha") {
283                 DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
284             }
285 
286         private val elementScale =
287             FeatureCapture<SemanticsNode, Scale>("scale") {
288                 DataPoint.of(it.lastScaleForTesting, scale)
289             }
290 
291         private val scale: DataPointType<Scale> =
292             DataPointType(
293                 "scale",
294                 jsonToValue = {
295                     when (it) {
296                         "unspecified" -> Scale.Unspecified
297                         "default" -> Scale.Default
298                         "zero" -> Scale.Zero
299                         is JSONObject -> {
300                             val pivot = it.get("pivot")
301                             Scale(
302                                 scaleX = it.getDouble("x").toFloat(),
303                                 scaleY = it.getDouble("y").toFloat(),
304                                 pivot =
305                                     when (pivot) {
306                                         "unspecified" -> Offset.Unspecified
307                                         "infinite" -> Offset.Infinite
308                                         is JSONObject ->
309                                             Offset(
310                                                 pivot.getDouble("x").toFloat(),
311                                                 pivot.getDouble("y").toFloat(),
312                                             )
313                                         else -> throw UnknownTypeException()
314                                     },
315                             )
316                         }
317                         else -> throw UnknownTypeException()
318                     }
319                 },
320                 valueToJson = {
321                     when (it) {
322                         Scale.Unspecified -> "unspecified"
323                         Scale.Default -> "default"
324                         Scale.Zero -> "zero"
325                         else -> {
326                             JSONObject().apply {
327                                 put("x", it.scaleX)
328                                 put("y", it.scaleY)
329                                 put(
330                                     "pivot",
331                                     when {
332                                         it.pivot.isUnspecified -> "unspecified"
333                                         !it.pivot.isFinite -> "infinite"
334                                         else ->
335                                             JSONObject().apply {
336                                                 put("x", it.pivot.x)
337                                                 put("y", it.pivot.y)
338                                             }
339                                     },
340                                 )
341                             }
342                         }
343                     }
344                 },
345             )
346     }
347 }
348