Merge "Macro Benchmark for Semantics" into androidx-main am: 8c1fc2090d
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/3073541
Change-Id: If29f697772dbcdb9681819dc16edf63bc523f091
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index a3da118..a4cf022 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -30,6 +30,7 @@
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation(libs.kotlinStdlib)
+ implementation(libs.material)
implementation(project(":activity:activity-compose"))
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.cardview:cardview:1.0.0")
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 0eadc27..d249e08 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -20,12 +20,11 @@
<uses-permission android:name="android.permission.INTERNET"/>
<application
- android:label="Jetpack Compose Macrobenchmark Target"
android:allowBackup="false"
- android:supportsRtl="true"
android:icon="@mipmap/ic_launcher"
+ android:label="Jetpack Compose Macrobenchmark Target"
+ android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning">
-
<!-- Profileable to enable macrobenchmark profiling -->
<profileable android:shell="true"/>
@@ -37,8 +36,8 @@
-->
<activity
android:name=".TrivialStartupActivity"
- android:label="C Trivial"
- android:exported="true">
+ android:exported="true"
+ android:label="C Trivial">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -50,8 +49,8 @@
</activity>
<activity
android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
- android:label="C StaticScrollingWithChrome Init"
- android:exported="true">
+ android:exported="true"
+ android:label="C StaticScrollingWithChrome Init">
<intent-filter>
<action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
@@ -63,8 +62,8 @@
</activity>
<activity
android:name=".TrivialStartupTracingActivity"
- android:label="C TrivialTracing"
- android:exported="true">
+ android:exported="true"
+ android:label="C TrivialTracing">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -92,8 +91,8 @@
</activity>
<activity
android:name=".LazyColumnActivity"
- android:label="C LazyColumn"
- android:exported="true">
+ android:exported="true"
+ android:label="C LazyColumn">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -105,8 +104,8 @@
</activity>
<activity
android:name=".FrameExperimentActivity"
- android:label="FrameExp"
- android:exported="true">
+ android:exported="true"
+ android:label="FrameExp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -176,7 +175,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".ViewPagerActivity"
android:exported="true"
@@ -186,7 +184,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".RecyclerViewAsCarouselActivity"
android:exported="true"
@@ -196,7 +193,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".PagerAsCarouselActivity"
android:exported="true"
@@ -206,7 +202,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".PagerActivity"
android:exported="true"
@@ -216,7 +211,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".TrivialTracingActivity"
android:exported="true">
@@ -225,7 +219,8 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
- <activity android:name=".AndroidViewListActivity"
+ <activity
+ android:name=".AndroidViewListActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat">
<intent-filter>
@@ -233,7 +228,8 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
- <activity android:name=".RecyclerViewListActivity"
+ <activity
+ android:name=".RecyclerViewListActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat">
<intent-filter>
@@ -241,11 +237,10 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
- <activity
+ <activity
android:name=".VectorsListActivity"
- android:label="Compose vectors list"
- android:exported="true">
+ android:exported="true"
+ android:label="Compose vectors list">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -255,11 +250,10 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
-
<activity
android:name=".CrossfadeActivity"
- android:label="Compose Crossfade Benchmark"
- android:exported="true">
+ android:exported="true"
+ android:label="Compose Crossfade Benchmark">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -268,9 +262,9 @@
<action android:name="androidx.compose.integration.macrobenchmark.target.CROSSFADE_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
- </activity>
-
- <activity android:name=".PagerOfLazyGridActivity"
+ </activity>
+ <activity
+ android:name=".PagerOfLazyGridActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat">
<intent-filter>
@@ -278,5 +272,20 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <activity
+ android:name=".FormFillingActivity"
+ android:exported="true"
+ android:label="Compose Form Filling Benchmark"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.AppCompat.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.FORM_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FormFillingActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FormFillingActivity.kt
new file mode 100644
index 0000000..2958dfe
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FormFillingActivity.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.EditText
+import android.widget.LinearLayout
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.LocalTextStyle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.trace
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+class FormFillingActivity : ComponentActivity() {
+ private lateinit var lazyListState: LazyListState
+ private lateinit var formView: FormView
+ private lateinit var type: String
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val rowHeightDp: Dp
+ val fontSize: TextUnit
+ when (intent.getIntExtra(MODE, 0)) {
+ FRAME_MEASUREMENT_MODE -> {
+ // Larger number of rows to stress the system while measuring frame info.
+ rowHeightDp = 30.dp
+ fontSize = 5.sp
+ }
+ CREATE_ANI_MODE -> {
+ // Smaller number of rows so that we have no dropped frames.
+ rowHeightDp = 100.dp
+ fontSize = 10.sp
+ }
+ else -> error("Invalid Mode")
+ }
+
+ type = checkNotNull(intent.getStringExtra(TYPE)) { "No type specified." }
+ when (type) {
+ COMPOSE ->
+ setContent {
+ lazyListState = rememberLazyListState()
+ FormComposable(lazyListState, rowHeightDp, fontSize)
+ }
+ VIEW -> {
+ val rowHeightPx = rowHeightDp.value * resources.displayMetrics.densityDpi / 160f
+ formView = FormView(this, rowHeightPx, fontSize)
+ setContentView(formView)
+ }
+ else -> error("Unknown Type")
+ }
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ when (type) {
+ COMPOSE -> lazyListState.requestScrollToItem(lazyListState.firstVisibleItemIndex + 100)
+ VIEW -> formView.scrollToPosition(formView.lastVisibleItemIndex + 100)
+ else -> error("Unknown Type")
+ }
+ super.onNewIntent(intent)
+ }
+
+ @Composable
+ private fun FormComposable(lazyListState: LazyListState, rowHeight: Dp, fontSize: TextUnit) {
+ val textStyle = LocalTextStyle.current.copy(fontSize = fontSize)
+ LazyColumn(state = lazyListState) {
+ items(data.size) { index ->
+ val person = data[index]
+ Row(
+ modifier =
+ Modifier.height(rowHeight).semantics {
+ customActions =
+ listOf(CustomAccessibilityAction("customAction") { false })
+ }
+ ) {
+ BasicTextField(
+ value = person.title,
+ onValueChange = { person.title = it },
+ textStyle = textStyle
+ )
+ BasicTextField(
+ value = person.firstName,
+ onValueChange = { person.firstName = it },
+ textStyle = textStyle
+ )
+ BasicTextField(
+ value = person.middleName,
+ onValueChange = { person.middleName = it },
+ textStyle = textStyle
+ )
+ BasicTextField(
+ value = person.lastName,
+ onValueChange = { person.lastName = it },
+ textStyle = textStyle
+ )
+ BasicTextField(
+ value = person.age.toString(),
+ onValueChange = { person.age = it.toInt() },
+ textStyle = textStyle
+ )
+ }
+ }
+ }
+ }
+
+ private class FormView(context: Context, rowHeight: Float, fontSize: TextUnit) :
+ RecyclerView(context) {
+ private val linearLayoutManager: LinearLayoutManager
+
+ init {
+ setHasFixedSize(true)
+ linearLayoutManager = LinearLayoutManager(context, VERTICAL, false)
+ layoutManager = linearLayoutManager
+ adapter = DemoAdapter(data, rowHeight, fontSize)
+ }
+
+ val lastVisibleItemIndex: Int
+ get() = linearLayoutManager.findLastVisibleItemPosition()
+
+ override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+ return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+ }
+
+ override fun sendAccessibilityEvent(eventType: Int) {
+ return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+ }
+ }
+
+ private class RowView(context: Context, content: (RowView) -> Unit) : LinearLayout(context) {
+ init {
+ gravity = Gravity.CENTER_VERTICAL
+ content(this)
+ }
+
+ override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+ return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+ }
+
+ override fun sendAccessibilityEvent(eventType: Int) {
+ return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+ }
+ }
+
+ @SuppressLint("AppCompatCustomView")
+ private class EditTextView(context: Context, fontSize: Float) : EditText(context) {
+ init {
+ textSize = fontSize
+ gravity = Gravity.CENTER_VERTICAL
+ }
+
+ fun replaceText(newText: String) {
+ text.replace(0, length(), newText, 0, newText.length)
+ }
+
+ override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+ return trace(CREATE_ANI_TRACE) { super.createAccessibilityNodeInfo() }
+ }
+
+ override fun sendAccessibilityEvent(eventType: Int) {
+ return trace(ACCESSIBILITY_EVENT_TRACE) { super.sendAccessibilityEvent(eventType) }
+ }
+ }
+
+ private class DemoAdapter(
+ val data: List<FormData>,
+ val rowHeightPx: Float,
+ textSize: TextUnit
+ ) : Adapter<DemoAdapter.DemoViewHolder>() {
+
+ private class DemoViewHolder(
+ val title: EditTextView,
+ val firstName: EditTextView,
+ val middleName: EditTextView,
+ val lastName: EditTextView,
+ val age: EditTextView,
+ itemRoot: View
+ ) : ViewHolder(itemRoot)
+
+ val textSize = textSize.value
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
+ val title = EditTextView(parent.context, textSize)
+ val firstName = EditTextView(parent.context, textSize)
+ val middleName = EditTextView(parent.context, textSize)
+ val lastName = EditTextView(parent.context, textSize)
+ val age = EditTextView(parent.context, textSize)
+
+ return DemoViewHolder(
+ title,
+ firstName,
+ middleName,
+ lastName,
+ age,
+ RowView(parent.context) {
+ it.minimumHeight = rowHeightPx.fastRoundToInt()
+ it.addView(title)
+ it.addView(firstName)
+ it.addView(middleName)
+ it.addView(lastName)
+ it.addView(age)
+ }
+ )
+ }
+
+ override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
+ val formData = data.elementAt(position)
+ holder.title.replaceText(formData.title)
+ holder.firstName.replaceText(formData.firstName)
+ holder.middleName.replaceText(formData.middleName)
+ holder.lastName.replaceText(formData.lastName)
+ holder.age.replaceText(formData.age.toString())
+ }
+
+ override fun getItemCount(): Int = data.size
+ }
+
+ private data class FormData(
+ var title: String = "",
+ var firstName: String = "",
+ var middleName: String = "",
+ var lastName: String = "",
+ var age: Int = 0,
+ )
+
+ private companion object {
+ private const val TYPE = "TYPE"
+ private const val COMPOSE = "Compose"
+ private const val VIEW = "View"
+ private const val MODE = "MODE"
+ private const val CREATE_ANI_MODE = 1
+ private const val FRAME_MEASUREMENT_MODE = 2
+ private const val CREATE_ANI_TRACE = "createAccessibilityNodeInfo"
+ private const val ACCESSIBILITY_EVENT_TRACE = "sendAccessibilityEvent"
+ private val data by lazy {
+ List(200000) {
+ FormData(
+ title = "Mr",
+ firstName = "John $it",
+ middleName = "Ace $it",
+ lastName = "Doe $it",
+ age = it
+ )
+ }
+ }
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt
new file mode 100644
index 0000000..9b05e73
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FormFillingBenchmark.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark
+
+import android.app.Instrumentation
+import android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
+import android.content.Intent
+import android.os.Build.VERSION_CODES.N
+import android.provider.Settings.Secure
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.TraceSectionMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.Configurator
+import androidx.test.uiautomator.UiDevice
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SdkSuppress(minSdkVersion = N)
+@LargeTest
+@RunWith(Parameterized::class)
+class FormFillingBenchmark(private var talkbackEnabled: Boolean, private val type: String) {
+
+ @get:Rule val benchmarkRule = MacrobenchmarkRule()
+ private lateinit var instrumentation: Instrumentation
+ private var previousTalkbackSettings: String? = null
+ private lateinit var device: UiDevice
+
+ @Test
+ fun createAccessibilityNodeInfo() {
+ if (!talkbackEnabled) return
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE,
+ metrics =
+ @OptIn(ExperimentalMetricApi::class)
+ listOf(
+ TraceSectionMetric(
+ sectionName = CREATE_ANI_TRACE,
+ mode = TraceSectionMetric.Mode.Sum
+ ),
+ TraceSectionMetric(
+ sectionName = ACCESSIBILITY_EVENT_TRACE,
+ mode = TraceSectionMetric.Mode.Sum
+ )
+ ),
+ iterations = 10,
+ setupBlock = {
+ if (iteration == 0) {
+ startActivityAndWait(
+ Intent()
+ .setAction("$PACKAGE.$ACTIVITY")
+ .putExtra(TYPE, type)
+ .putExtra(MODE, CREATE_ANI_MODE)
+ )
+ device.waitForIdle()
+
+ // Run one iteration to allow the scroll position to stabilize, and to remove
+ // the effect of the initial frame which draws the accessibility focus box.
+ performScrollAndWait(millis = 10_000)
+ }
+ },
+ measureBlock = {
+
+ // Scroll and pause to allow all frames to complete, for the accessibility events
+ // to be sent, for talkback to assign focus, and finally for talkback to trigger
+ // createAccessibilityNodeInfo calls which is the thing we want to measure.
+ performScrollAndWait(millis = 10_000)
+ }
+ )
+ }
+
+ @Test
+ fun frameInfo() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE,
+ metrics = listOf(FrameTimingMetric()),
+ iterations = 10,
+ setupBlock = {
+ if (iteration == 0) {
+ startActivityAndWait(
+ Intent()
+ .setAction("$PACKAGE.$ACTIVITY")
+ .putExtra(TYPE, type)
+ .putExtra(MODE, FRAME_MEASUREMENT_MODE)
+ )
+ Thread.sleep(2_000)
+ device.waitForIdle()
+
+ // Run one iteration to allow the scroll position to stabilize, and to remove
+ // the effect of the initial frame which draws the accessibility focus box.
+ performScrollAndWait(millis = 20)
+ }
+ },
+ measureBlock = {
+ // Instead of using an animation to scroll (Where the number of frames triggered
+ // is not deterministic, we attempt to scroll 100 times with an aim to scroll once
+ // every frame deadline of 20ms.
+ repeat(100) { performScrollAndWait(millis = 20) }
+ Thread.sleep(10_000)
+ }
+ )
+ }
+
+ @Before
+ fun setUp() {
+ Configurator.getInstance().uiAutomationFlags = FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
+ instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ if (talkbackEnabled) {
+ previousTalkbackSettings = instrumentation.enableTalkback()
+ // Wait for talkback to turn on.
+ Thread.sleep(2_000)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ if (talkbackEnabled) {
+ instrumentation.disableTalkback(previousTalkbackSettings)
+ // Wait for talkback to turn off.
+ Thread.sleep(2_000)
+ }
+ }
+
+ private fun performScrollAndWait(millis: Long) {
+ // We don't use UI Automator to scroll because UI Automator itself is an accessibility
+ // service, and this affects the benchmark. Instead we send an event to the activity that
+ // requests it to scroll.
+ instrumentation.context.startActivity(
+ Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setAction("$PACKAGE.$ACTIVITY")
+ )
+
+ // Pause to allow all frames to complete, for the accessibility events to be sent,
+ // for talkback to assign focus, and finally for talkback to trigger
+ // createAccessibilityNodeInfo calls which is the thing we want to measure.
+ Thread.sleep(millis)
+ }
+
+ companion object {
+ private const val PACKAGE = "androidx.compose.integration.macrobenchmark.target"
+ private const val ACTIVITY = "FORM_ACTIVITY"
+ private const val TYPE = "TYPE"
+ private const val COMPOSE = "Compose"
+ private const val VIEW = "View"
+ const val MODE = "MODE"
+ const val CREATE_ANI_MODE = 1
+ const val FRAME_MEASUREMENT_MODE = 2
+ const val CREATE_ANI_TRACE = "createAccessibilityNodeInfo"
+ const val ACCESSIBILITY_EVENT_TRACE = "sendAccessibilityEvent"
+
+ // Manually set up LastPass on the device and use these parameters when running locally.
+ // @Parameterized.Parameters(name = "LastPassEnabled=true, type={1}")
+ // @JvmStatic
+ // fun parameters() = mutableListOf<Array<Any>>().also {
+ // for (type in arrayOf(COMPOSE, VIEW)) {
+ // it.add(arrayOf(false, type))
+ // }
+ // }
+
+ @Parameterized.Parameters(name = "TalkbackEnabled={0}, type={1}")
+ @JvmStatic
+ fun parameters() =
+ mutableListOf<Array<Any>>().also {
+ for (talkbackEnabled in arrayOf(false, true)) {
+ for (type in arrayOf(COMPOSE, VIEW)) {
+ it.add(arrayOf(talkbackEnabled, type))
+ }
+ }
+ }
+ }
+}
+
+private fun Instrumentation.enableTalkback(): String? {
+ val talkback =
+ "com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService"
+ val previousTalkbackSettings =
+ Secure.getString(context.contentResolver, Secure.ENABLED_ACCESSIBILITY_SERVICES)
+ UiDevice.getInstance(this)
+ .executeShellCommand("settings put secure enabled_accessibility_services $talkback")
+ return previousTalkbackSettings
+}
+
+private fun Instrumentation.disableTalkback(previousTalkbackSettings: String? = null): String {
+ return UiDevice.getInstance(this)
+ .executeShellCommand(
+ if (previousTalkbackSettings == null || previousTalkbackSettings == "") {
+ "settings delete secure enabled_accessibility_services"
+ } else {
+ "settings put secure enabled_accessibility_services $previousTalkbackSettings"
+ }
+ )
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index bc2bca0..496c90e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -208,7 +208,7 @@
// flaky, so we use this callback to test accessibility events.
@VisibleForTesting
internal var onSendAccessibilityEvent: (AccessibilityEvent) -> Boolean = {
- view.parent.requestSendAccessibilityEvent(view, it)
+ trace("sendAccessibilityEvent") { view.parent.requestSendAccessibilityEvent(view, it) }
}
private val accessibilityManager: AccessibilityManager =
@@ -1519,7 +1519,7 @@
event.contentDescription = contentDescription.fastJoinToString(",")
}
- return trace("sendEvent") { sendEvent(event) }
+ return sendEvent(event)
}
/**
@@ -2239,47 +2239,45 @@
try {
val subtreeChangedSemanticsNodesIds = MutableIntSet()
for (notification in boundsUpdateChannel) {
- trace("AccessibilityLoopIteration") {
- if (isEnabled) {
- for (i in subtreeChangedLayoutNodes.indices) {
- val layoutNode = subtreeChangedLayoutNodes.valueAt(i)
- trace("sendSubtreeChangeAccessibilityEvents") {
- sendSubtreeChangeAccessibilityEvents(
- layoutNode,
- subtreeChangedSemanticsNodesIds
- )
- }
- trace("sendTypeViewScrolledAccessibilityEvent") {
- sendTypeViewScrolledAccessibilityEvent(layoutNode)
- }
+ if (isEnabled) {
+ for (i in subtreeChangedLayoutNodes.indices) {
+ val layoutNode = subtreeChangedLayoutNodes.valueAt(i)
+ trace("sendSubtreeChangeAccessibilityEvents") {
+ sendSubtreeChangeAccessibilityEvents(
+ layoutNode,
+ subtreeChangedSemanticsNodesIds
+ )
}
- subtreeChangedSemanticsNodesIds.clear()
- // When the bounds of layout nodes change, we will not always get semantics
- // change notifications because bounds is not part of semantics. And bounds
- // change from a layout node without semantics will affect the global bounds
- // of it children which has semantics. Bounds change will affect which nodes
- // are covered and which nodes are not, so the currentSemanticsNodes is not
- // up to date anymore.
- // After the subtree events are sent, accessibility services will get the
- // current visible/invisible state. We also try to do semantics tree diffing
- // to send out the proper accessibility events and update our copy here so
- // that
- // our incremental changes (represented by accessibility events) are
- // consistent
- // with accessibility services. That is: change - notify - new change -
- // notify, if we don't do the tree diffing and update our copy here, we will
- // combine old change and new change, which is missing finer-grained
- // notification.
- if (!checkingForSemanticsChanges) {
- checkingForSemanticsChanges = true
- handler.post(semanticsChangeChecker)
+ trace("sendTypeViewScrolledAccessibilityEvent") {
+ sendTypeViewScrolledAccessibilityEvent(layoutNode)
}
}
- subtreeChangedLayoutNodes.clear()
- pendingHorizontalScrollEvents.clear()
- pendingVerticalScrollEvents.clear()
- delay(SendRecurringAccessibilityEventsIntervalMillis)
+ subtreeChangedSemanticsNodesIds.clear()
+ // When the bounds of layout nodes change, we will not always get semantics
+ // change notifications because bounds is not part of semantics. And bounds
+ // change from a layout node without semantics will affect the global bounds
+ // of it children which has semantics. Bounds change will affect which nodes
+ // are covered and which nodes are not, so the currentSemanticsNodes is not
+ // up to date anymore.
+ // After the subtree events are sent, accessibility services will get the
+ // current visible/invisible state. We also try to do semantics tree diffing
+ // to send out the proper accessibility events and update our copy here so
+ // that
+ // our incremental changes (represented by accessibility events) are
+ // consistent
+ // with accessibility services. That is: change - notify - new change -
+ // notify, if we don't do the tree diffing and update our copy here, we will
+ // combine old change and new change, which is missing finer-grained
+ // notification.
+ if (!checkingForSemanticsChanges) {
+ checkingForSemanticsChanges = true
+ handler.post(semanticsChangeChecker)
+ }
}
+ subtreeChangedLayoutNodes.clear()
+ pendingHorizontalScrollEvents.clear()
+ pendingVerticalScrollEvents.clear()
+ delay(SendRecurringAccessibilityEventsIntervalMillis)
}
} finally {
subtreeChangedLayoutNodes.clear()