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(initialScene: String) {
47 val intent =
48 (context().packageManager.getLaunchIntentForPackage(StlDemoConstants.PACKAGE)
49 ?: error("Unable to acquire intent for package ${StlDemoConstants.PACKAGE}"))
50 .apply {
51 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
52 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
53 putExtra(StlDemoConstants.INITIAL_SCENE_EXTRA, initialScene)
54 putExtra(StlDemoConstants.FULLSCREEN_EXTRA, true)
55 putExtra(StlDemoConstants.DISABLE_RIPPLE_EXTRA, true)
56 }
57
58 val device = device()
59 device.pressHome()
60 startActivity(intent)
61 device.waitForObject(sceneSelector(initialScene))
62 }
63
SceneTransitionLayoutBenchmarkScopenull64 fun SceneTransitionLayoutBenchmarkScope.setupSwipeFromScene(fromScene: String, toScene: String) {
65 startDemoActivity(initialScene = fromScene)
66
67 // Wait for the root SceneTransitionLayout to be there. Note that startDemoActivity already
68 // waited for fromScene, so we know it's there.
69 val device = device()
70 device.waitForObject(StlDemoConstants.ROOT_STL_SELECTOR)
71
72 // Verify that toScene is not there yet.
73 device.waitUntilGone(sceneSelector(toScene))
74 }
75
swipeFromScenenull76 fun swipeFromScene(fromScene: String, toScene: String, direction: Direction) {
77 // Swipe in the given direction.
78 val densityDpi = context().resources.configuration.densityDpi
79 val density = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
80 val swipeSpeed = 1_500 // in dp/s
81 val device = device()
82 device
83 .findObject(StlDemoConstants.ROOT_STL_SELECTOR)
84 .swipe(direction, /* percent= */ 0.9f, /* speed= */ (swipeSpeed * density).roundToInt())
85
86 // Wait for fromScene to disappear.
87 device.waitUntilGone(sceneSelector(fromScene))
88
89 // Check that we are at toScene.
90 device.waitForObject(sceneSelector(toScene))
91 }
92
93 /**
94 * Navigate back to [previousScene] assuming that we are currently on [currentScene] and that going
95 * back will land us at [previousScene].
96 */
navigateBackToPreviousScenenull97 fun navigateBackToPreviousScene(previousScene: String, currentScene: String) {
98 val device = device()
99 device.waitUntilGone(sceneSelector(previousScene))
100 device.waitForObject(sceneSelector(currentScene))
101
102 device.pressBack()
103 device.waitUntilGone(sceneSelector(currentScene))
104 device.waitForObject(sceneSelector(previousScene))
105 }
106
instrumentationnull107 private fun instrumentation() = InstrumentationRegistry.getInstrumentation()
108
109 private fun context() = instrumentation().targetContext
110
111 private fun device() = UiDevice.getInstance(instrumentation())
112
113 private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 5_000) {
114 if (!wait(Until.hasObject(selector), timeout)) {
115 error("Did not find $selector within $timeout ms")
116 }
117 }
118
waitUntilGonenull119 private fun UiDevice.waitUntilGone(selector: BySelector, timeout: Long = 5_000) {
120 if (!wait(Until.gone(selector), timeout)) {
121 error("$selector is still there after waiting $timeout ms")
122 }
123 }
124
sceneSelectornull125 private fun sceneSelector(scene: String) = By.res("scene:$scene")
126
127 object StlDemoConstants {
128 const val PACKAGE = "com.android.compose.animation.scene.demo.app"
129 val LOCKSCREEN_SCENE by AdaptiveScene("Lockscreen", "SplitLockscreen")
130 val SHADE_SCENE by AdaptiveScene("Shade", "SplitShade")
131 const val QUICK_SETTINGS_SCENE = "QuickSettings"
132
133 internal const val INITIAL_SCENE_EXTRA = "initial_scene"
134 internal const val FULLSCREEN_EXTRA = "fullscreen"
135 internal const val DISABLE_RIPPLE_EXTRA = "disable_ripple"
136 internal val ROOT_STL_SELECTOR = By.res("SystemUiSceneTransitionLayout")
137 }
138
139 /** A scene whose key depends on whether we are using split scenes or not. */
140 private class AdaptiveScene(private val normalScene: String, private val splitScene: String) :
141 ReadOnlyProperty<Any, String> {
getValuenull142 override fun getValue(thisRef: Any, property: KProperty<*>): String {
143 val context = context()
144 val density = Density(context)
145 return if (shouldUseSplitScenes(calculateWindowSizeClass(context, density))) {
146 splitScene
147 } else {
148 normalScene
149 }
150 }
151 }
152