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 package com.android.compose.animation.scene.benchmark
18
19 import android.content.Intent
20 import android.util.DisplayMetrics
21 import androidx.compose.ui.unit.Density
22 import androidx.test.platform.app.InstrumentationRegistry
23 import androidx.test.uiautomator.By
24 import androidx.test.uiautomator.BySelector
25 import androidx.test.uiautomator.Direction
26 import androidx.test.uiautomator.UiDevice
27 import androidx.test.uiautomator.Until
28 import com.android.compose.animation.scene.demo.calculateWindowSizeClass
29 import com.android.compose.animation.scene.demo.shouldUseSplitScenes
30 import kotlin.math.roundToInt
31 import kotlin.properties.ReadOnlyProperty
32 import kotlin.reflect.KProperty
33
34 /**
35 * This file contains utilities to perform benchmark tests for the demo app of SceneTransitionLayout
36 * given a [SceneTransitionLayoutBenchmarkScope].
37 *
38 * These abstractions are necessary to share test code between AndroidX tests written with the
39 * MacrobenchmarkRule and Platform tests written with Platform helpers.
40 */
41 interface SceneTransitionLayoutBenchmarkScope {
42 /** Start an activity using [intent]. */
startActivitynull43 fun startActivity(intent: Intent)
44 }
45
46 fun SceneTransitionLayoutBenchmarkScope.startDemoActivity(
47 initialScene: String,
48 notificationsInShade: Int? = null,
49 enableOverlays: Boolean = false,
50 ) {
51 val intent =
52 (context().packageManager.getLaunchIntentForPackage(StlDemoConstants.PACKAGE)
53 ?: error("Unable to acquire intent for package ${StlDemoConstants.PACKAGE}"))
54 .apply {
55 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
56 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
57 putExtra(StlDemoConstants.INITIAL_SCENE_EXTRA, initialScene)
58 putExtra(StlDemoConstants.FULLSCREEN_EXTRA, true)
59 putExtra(StlDemoConstants.DISABLE_RIPPLE_EXTRA, true)
60 if (enableOverlays) {
61 putExtra(StlDemoConstants.OVERLAYS_EXTRA, true)
62 }
63
64 notificationsInShade?.let { putExtra(StlDemoConstants.NOTIFICATIONS_IN_SHADE, it) }
65 }
66
67 val device = device()
68 device.pressHome()
69 startActivity(intent)
70 device.waitForObject(sceneSelector(initialScene))
71 }
72
SceneTransitionLayoutBenchmarkScopenull73 fun SceneTransitionLayoutBenchmarkScope.setupSwipeFromScene(
74 fromScene: String,
75 toScene: String,
76 toContentIsOverlay: Boolean,
77 ) {
78 startDemoActivity(initialScene = fromScene, enableOverlays = toContentIsOverlay)
79
80 // Wait for the root SceneTransitionLayout to be there. Note that startDemoActivity already
81 // waited for fromScene, so we know it's there.
82 val device = device()
83 device.waitForObject(StlDemoConstants.ROOT_STL_SELECTOR_IDLE)
84
85 // Verify that toScene is not there yet.
86 device.waitUntilGone(sceneSelector(toScene))
87 }
88
swipeFromScenenull89 fun swipeFromScene(
90 fromScene: String,
91 toContent: String,
92 direction: Direction,
93 toContentIsOverlay: Boolean,
94 swipeOn: BySelector? = null,
95 ) {
96 // Swipe in the given direction.
97 val densityDpi = context().resources.configuration.densityDpi
98 val density = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
99 val swipeSpeed = 1_500 // in dp/s
100 val device = device()
101
102 val swipeOn = swipeOn ?: StlDemoConstants.ROOT_STL_SELECTOR_IDLE
103 device.waitForObject(swipeOn)
104 device
105 .findObject(swipeOn)
106 .swipe(direction, /* percent= */ 0.9f, /* speed= */ (swipeSpeed * density).roundToInt())
107
108 if (!toContentIsOverlay) {
109 // Wait for fromScene to disappear.
110 device.waitUntilGone(sceneSelector(fromScene))
111 }
112
113 // Check that we are at toContent.
114 device.waitForObject(contentSelector(toContent, toContentIsOverlay))
115
116 // Wait for the layout to be idle.
117 device.waitForObject(StlDemoConstants.ROOT_STL_SELECTOR_IDLE)
118 }
119
120 /**
121 * Navigate back to [previousScene] assuming that we are currently on [currentContent] and that
122 * going back will land us at [previousScene].
123 */
navigateBackToPreviousScenenull124 fun navigateBackToPreviousScene(
125 previousScene: String,
126 currentContent: String,
127 currentContentIsOverlay: Boolean,
128 ) {
129 val device = device()
130 val currentContentSelector = contentSelector(currentContent, currentContentIsOverlay)
131 device.waitUntilGone(sceneSelector(previousScene))
132 device.waitForObject(currentContentSelector)
133
134 device.pressBack()
135 device.waitUntilGone(currentContentSelector)
136 device.waitForObject(sceneSelector(previousScene))
137 device.waitForObject(StlDemoConstants.ROOT_STL_SELECTOR_IDLE)
138 }
139
instrumentationnull140 private fun instrumentation() = InstrumentationRegistry.getInstrumentation()
141
142 private fun context() = instrumentation().targetContext
143
144 private fun device() = UiDevice.getInstance(instrumentation())
145
146 private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 5_000) {
147 if (!wait(Until.hasObject(selector), timeout)) {
148 error("Did not find $selector within $timeout ms")
149 }
150 }
151
waitUntilGonenull152 private fun UiDevice.waitUntilGone(selector: BySelector, timeout: Long = 5_000) {
153 if (!wait(Until.gone(selector), timeout)) {
154 error("$selector is still there after waiting $timeout ms")
155 }
156 }
157
sceneSelectornull158 private fun sceneSelector(scene: String) = By.res("scene:$scene")
159
160 private fun overlaySelector(overlay: String) = By.res("overlay:$overlay")
161
162 private fun contentSelector(toContent: String, toContentIsOverlay: Boolean): BySelector {
163 return if (toContentIsOverlay) overlaySelector(toContent) else sceneSelector(toContent)
164 }
165
166 object StlDemoConstants {
167 const val PACKAGE = "com.android.compose.animation.scene.demo.app"
168 val LOCKSCREEN_SCENE by AdaptiveScene("Lockscreen", "SplitLockscreen")
169 val SHADE_SCENE by AdaptiveScene("Shade", "SplitShade")
170 const val QUICK_SETTINGS_SCENE = "QuickSettings"
171 const val NOTIFICATIONS_OVERLAY = "NotificationsOverlay"
172 const val QUICK_SETTINGS_OVERLAY = "QuickSettingsOverlay"
173
174 internal const val INITIAL_SCENE_EXTRA = "initial_scene"
175 internal const val FULLSCREEN_EXTRA = "fullscreen"
176 internal const val DISABLE_RIPPLE_EXTRA = "disable_ripple"
177 internal const val NOTIFICATIONS_IN_SHADE = "notifications_in_shade"
178 internal const val OVERLAYS_EXTRA = "overlays"
179 internal val ROOT_STL_SELECTOR_IDLE = By.res("SystemUiSceneTransitionLayout:idle")
180 val STL_START_HALF_SELECTOR = By.res("StlStartHalf")
181 val STL_END_HALF_SELECTOR = By.res("StlEndHalf")
182 }
183
184 /** A scene whose key depends on whether we are using split scenes or not. */
185 private class AdaptiveScene(private val normalScene: String, private val splitScene: String) :
186 ReadOnlyProperty<Any, String> {
getValuenull187 override fun getValue(thisRef: Any, property: KProperty<*>): String {
188 val context = context()
189 val density = Density(context)
190 return if (shouldUseSplitScenes(calculateWindowSizeClass(context, density))) {
191 splitScene
192 } else {
193 normalScene
194 }
195 }
196 }
197