• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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