From b845016b9f2b6614f00c12f389c0b5bd4ea0bf92 Mon Sep 17 00:00:00 2001 From: Leticia Santos Date: Thu, 19 Feb 2026 18:19:56 -0500 Subject: [PATCH 01/16] [Menu] Fix icon-label padding being applied when the item has no selected icon displayed. Test: existing Relnote: Fixed icon-label padding being applied when the item has a selectedLeadingIcon that is not currently displayed Change-Id: Iba820e6a78b1e0a4e322cfe1ee89574ca065d4e9 --- .../src/commonMain/kotlin/androidx/compose/material3/Menu.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt index 24ae6dce44c01..b64672062f6a6 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt @@ -1129,6 +1129,7 @@ internal fun DropdownMenuItemContent( val itemShape = shapeByInteraction(shapes, selected, morphSpec) val hasLeadingIcon = leadingIcon != null || selectedLeadingIcon != null + val hasLeadingIconDisplayed = leadingIcon != null || (selectedLeadingIcon != null && selected) val hasTrailingIcon = trailingIcon != null Surface( @@ -1208,7 +1209,8 @@ internal fun DropdownMenuItemContent( Modifier.layoutId(TextLayoutId) .padding( start = - if (hasLeadingIcon) { + if (hasLeadingIconDisplayed) { + // Only add this padding if there's an icon displayed. DropdownMenuIconTextPadding } else { 0.dp From 132e481288c2de012d8a610c9d33de422d9f2e4c Mon Sep 17 00:00:00 2001 From: Julie Date: Tue, 24 Feb 2026 14:35:18 +0800 Subject: [PATCH 02/16] Bump up camerax versions to 1.7.0-alpha01 Test: ./gradlew camera:camera Change-Id: Ie82f67aa5bdb353f140614ccf0bfd4882a987096 --- libraryversions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraryversions.toml b/libraryversions.toml index b72a8502a1534..1f924163cd84a 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -14,13 +14,13 @@ BIOMETRIC = "1.4.0-alpha05" BLUETOOTH = "1.0.0-alpha02" BROWSER = "1.10.0-alpha04" BUILDSRC_TESTS = "1.0.0-alpha01" -CAMERA = "1.6.0-rc01" +CAMERA = "1.7.0-alpha01" CAMERA_COMMON = "1.0.0-alpha01" -CAMERA_FEATURE_COMBINATION_QUERY = "1.6.0-rc01" +CAMERA_FEATURE_COMBINATION_QUERY = "1.7.0-alpha01" CAMERA_MEDIA3 = "1.0.0-alpha05" CAMERA_PIPE_TESTING = "1.0.0-alpha01" CAMERA_TESTING = "1.0.0-alpha01" -CAMERA_VIEWFINDER = "1.6.0-rc01" +CAMERA_VIEWFINDER = "1.7.0-alpha01" CARDVIEW = "1.1.0-alpha01" CAR_APP = "1.8.0-alpha03" COLLECTION = "1.7.0-alpha01" From 7ed59ed969f30ce1bbcbf55c9d2aa4aa9d100728 Mon Sep 17 00:00:00 2001 From: Andrew Bailey Date: Tue, 24 Feb 2026 13:36:40 -0500 Subject: [PATCH 03/16] Use base Anchor class to build composition traces Relnote: """ Fixed an issue in the LinkTable that would prevent diagnostic traces from being populated correctly """ Test: UiErrorTraceTests Change-Id: I5986897dad1e146f93a391ac94815e8120c10561 --- .../compose/runtime/tooling/ComposeStackTraceBuilder.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt index 693de0dc0bd6e..9549fd6a3b155 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/tooling/ComposeStackTraceBuilder.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.GapComposer.CompositionContextHolder import androidx.compose.runtime.RememberObserverHolder import androidx.compose.runtime.composer.GroupSourceInformation -import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter @@ -90,7 +89,7 @@ internal abstract class ComposeStackTraceBuilder { sourceInfo != null && (sourceInfo.key == defaultsKey || (sourceInfo.key == 0 && - child is GapAnchor && + child is Anchor && groupKeyOf(child) == defaultsKey)) // If sourceInformation is null, it means that default group does not capture @@ -117,7 +116,7 @@ internal abstract class ComposeStackTraceBuilder { private fun sourceInformationOf(group: Any) = when (group) { - is GapAnchor -> sourceInformationOf(group) + is Anchor -> sourceInformationOf(group) is GroupSourceInformation -> group else -> error("Unexpected child source info $group") } @@ -181,7 +180,7 @@ internal abstract class ComposeStackTraceBuilder { children.fastForEach { child -> // find the edge that leads to target anchor when (child) { - is GapAnchor -> { + is Anchor -> { // edge found, return if (child == target) { appendTraceFrame(sourceInformation.key, sourceInformation, child) From b3ee3e720dc4d0b04c54f3464afca4eae84c8df7 Mon Sep 17 00:00:00 2001 From: Rahul Ravikumar Date: Mon, 23 Feb 2026 16:36:45 -0800 Subject: [PATCH 04/16] Fix race condition when emitting trace events for coroutine suspends and resumes. * Previously we held a reference to the `ThreadTrack` that was used when a coroutine started. * However if the coroutine resumed on a different `Thread`, when we introduce a race condition where the `PooledTracePacketArray` which both threads operate on will race. * We expect `PooledTracePacketArray` to effectively behave like a `ThreadLocal`, and therefore rather than use the reference to the `ThreadTrack` from a different thread, we instead switch to a `ThreadTrack` that is safe to use. Test: Existing unit tests pass. Ran end to end tests. No change in coroutine tracing benchmarks. ``` Before: TracingBenchmark.beginEndCoroutine_writeOnly timeNs min 572,487.2, median 573,807.8, max 652,955.5 instructions min 592,150.9, median 592,664.1, max 630,026.5 l1DMisses min 7,177.1, median 7,211.7, max 8,210.5 branchMisses min 9,120.1, median 9,163.4, max 13,471.6 allocationCount min 936.0, median 936.0, max 936.0 After: timeNs min 569,071.0, median 571,729.1, max 651,268.3 instructions min 592,260.7, median 592,781.7, max 628,709.9 l1DMisses min 6,179.4, median 6,260.3, max 7,254.3 branchMisses min 9,752.4, median 9,773.5, max 13,924.0 allocationCount min 936.0, median 936.0, max 936.0 ``` Relnote: Fix race condition when emitting trace events for coroutine suspends and resumes. Change-Id: Ie145e2ae7de1960fa4036e0c05a5a69a1f0ee407 --- .../benchmark/driver/TracingBenchmark.kt | 2 + tracing/tracing/api/current.txt | 4 +- tracing/tracing/api/restricted_current.txt | 12 ++-- tracing/tracing/build.gradle | 10 +-- .../androidx/tracing/ContextElements.kt | 24 +++---- .../kotlin/androidx/tracing/PerfettoTracer.kt | 24 +++---- .../kotlin/androidx/tracing/SliceTrack.kt | 2 +- .../kotlin/androidx/tracing/TraceTokens.kt | 51 ++++++++------ .../kotlin/androidx/tracing/Track.kt | 1 + .../tracing/ContextElements.desktop.kt | 68 ------------------- .../tracing/ContextElements.jvmAndAndroid.kt} | 26 ++++--- 11 files changed, 91 insertions(+), 133 deletions(-) delete mode 100644 tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.kt rename tracing/tracing/src/{androidMain/kotlin/androidx/tracing/ContextElements.android.kt => jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt} (78%) diff --git a/tracing/benchmark/src/androidTest/java/androidx/tracing/benchmark/driver/TracingBenchmark.kt b/tracing/benchmark/src/androidTest/java/androidx/tracing/benchmark/driver/TracingBenchmark.kt index 3b7808aa39014..9523403f9ffa4 100644 --- a/tracing/benchmark/src/androidTest/java/androidx/tracing/benchmark/driver/TracingBenchmark.kt +++ b/tracing/benchmark/src/androidTest/java/androidx/tracing/benchmark/driver/TracingBenchmark.kt @@ -30,6 +30,7 @@ import androidx.tracing.benchmark.CATEGORY import androidx.tracing.wire.TraceSink import kotlin.coroutines.CoroutineContext import kotlin.test.assertEquals +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest @@ -111,6 +112,7 @@ class TracingBenchmark { fun referenceForBeginEndCoroutine() = runTest { benchmarkRule.measureRepeated { runBlocking { + val coroutineContext = currentCoroutineContext() withContext(coroutineContext + TestThreadContextElement()) { repeat(32) { BlackHole.consume(it) } } diff --git a/tracing/tracing/api/current.txt b/tracing/tracing/api/current.txt index ffbe26999fb93..b3d6118beba0b 100644 --- a/tracing/tracing/api/current.txt +++ b/tracing/tracing/api/current.txt @@ -68,16 +68,18 @@ package androidx.tracing { field public int type; } - public abstract class PlatformThreadContextElement extends kotlin.coroutines.AbstractCoroutineContextElement implements java.lang.AutoCloseable androidx.tracing.PropagationToken { + public abstract class PlatformThreadContextElement extends kotlin.coroutines.AbstractCoroutineContextElement implements java.lang.AutoCloseable androidx.tracing.PropagationToken { method @SuppressCompatibility @androidx.tracing.DelicateTracingApi public kotlin.coroutines.CoroutineContext.Element? contextElementOrNull(); method @InaccessibleFromKotlin public String getCategory(); method @InaccessibleFromKotlin public java.util.List getFlowIds(); method @InaccessibleFromKotlin public String getName(); + method @InaccessibleFromKotlin public T getTracer(); method @InaccessibleFromKotlin public void setCategory(String); method @InaccessibleFromKotlin public void setName(String); property public String category; property public java.util.List flowIds; property public String name; + property public T tracer; } public abstract class Poolable> { diff --git a/tracing/tracing/api/restricted_current.txt b/tracing/tracing/api/restricted_current.txt index 39a8299edbd58..674ef0c347484 100644 --- a/tracing/tracing/api/restricted_current.txt +++ b/tracing/tracing/api/restricted_current.txt @@ -9,8 +9,8 @@ package androidx.tracing { method @kotlin.PublishedApi internal static long monotonicId(); } - public final class ContextElements_androidKt { - method @kotlin.PublishedApi internal static androidx.tracing.PlatformThreadContextElement buildThreadContextElement(String category, String name, java.util.List flowIds, kotlin.jvm.functions.Function1 updateThreadContextBlock, kotlin.jvm.functions.Function1 restoreThreadContextBlock, kotlin.jvm.functions.Function1,kotlin.Unit> close); + public final class ContextElements_jvmAndAndroidKt { + method @kotlin.PublishedApi internal static androidx.tracing.PlatformThreadContextElement buildThreadContextElement(T tracer, String category, String name, java.util.List flowIds, kotlin.jvm.functions.Function1 updateThreadContextBlock, kotlin.jvm.functions.Function1 restoreThreadContextBlock, kotlin.jvm.functions.Function1,kotlin.Unit> close); } public abstract class Counter { @@ -85,24 +85,26 @@ package androidx.tracing { field public int type; } - public abstract class PlatformThreadContextElement extends kotlin.coroutines.AbstractCoroutineContextElement implements java.lang.AutoCloseable androidx.tracing.PropagationToken { + public abstract class PlatformThreadContextElement extends kotlin.coroutines.AbstractCoroutineContextElement implements java.lang.AutoCloseable androidx.tracing.PropagationToken { method @SuppressCompatibility @androidx.tracing.DelicateTracingApi public kotlin.coroutines.CoroutineContext.Element? contextElementOrNull(); method @InaccessibleFromKotlin public String getCategory(); method @InaccessibleFromKotlin public java.util.List getFlowIds(); method @InaccessibleFromKotlin public String getName(); + method @InaccessibleFromKotlin public T getTracer(); method @InaccessibleFromKotlin public void setCategory(String); method @InaccessibleFromKotlin public void setName(String); property public String category; property public java.util.List flowIds; property public String name; + property public T tracer; field @kotlin.PublishedApi internal static final androidx.tracing.PlatformThreadContextElement.Companion Companion; - field @kotlin.PublishedApi internal static final kotlin.coroutines.CoroutineContext.Key> KEY; + field @kotlin.PublishedApi internal static final kotlin.coroutines.CoroutineContext.Key> KEY; field @kotlin.PublishedApi internal static final int STATE_BEGIN = 1; // 0x1 field @kotlin.PublishedApi internal static final int STATE_END = 0; // 0x0 } @kotlin.PublishedApi internal static final class PlatformThreadContextElement.Companion { - property @kotlin.PublishedApi internal kotlin.coroutines.CoroutineContext.Key> KEY; + property @kotlin.PublishedApi internal kotlin.coroutines.CoroutineContext.Key> KEY; property @kotlin.PublishedApi internal static int STATE_BEGIN; property @kotlin.PublishedApi internal static int STATE_END; } diff --git a/tracing/tracing/build.gradle b/tracing/tracing/build.gradle index d9046a4ae3f63..1f77a3f5ff087 100644 --- a/tracing/tracing/build.gradle +++ b/tracing/tracing/build.gradle @@ -33,17 +33,17 @@ androidXMultiplatform { commonMain.dependencies { api(libs.kotlinCoroutinesCore) api(libs.androidx.annotation) - implementation("androidx.collection:collection:1.4.5") + implementation("androidx.collection:collection:1.5.0") } commonTest.dependencies { - implementation(libs.kotlinTest) - implementation(libs.kotlinCoroutinesCore) - implementation(libs.kotlinCoroutinesTest) + implementation(libs.kotlinTest) + implementation(libs.kotlinCoroutinesCore) + implementation(libs.kotlinCoroutinesTest) } androidMain.dependencies { - implementation("androidx.annotation:annotation:1.8.1") + implementation("androidx.annotation:annotation:1.9.1") } desktopTest.dependencies { diff --git a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/ContextElements.kt b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/ContextElements.kt index 0757564fd2e6b..a9a3967fdc3b2 100644 --- a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/ContextElements.kt +++ b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/ContextElements.kt @@ -25,26 +25,23 @@ import kotlin.coroutines.CoroutineContext */ // False positive: https://youtrack.jetbrains.com/issue/KTIJ-22326 @Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE") -public abstract class PlatformThreadContextElement +public abstract class PlatformThreadContextElement internal constructor( + public open val tracer: T, public open var category: String, public open var name: String, public open val flowIds: List, ) : AbstractCoroutineContextElement(key = KEY), PropagationToken, AutoCloseable { - // Always starts in a begin state. + // Always starts in a `begin` state. @JvmField internal var started: Int = STATE_BEGIN - // Default to an empty thread track to ensure that this is non-null. - // We will always swap this with the real owner. - @JvmField internal var owner: ThreadTrack = EmptyTraceContext.thread - /** * This method is called **before a coroutine is resumed** on a thread that belongs to a * dispatcher. */ internal abstract fun updateThreadContext(context: CoroutineContext): S - /** This method is called **after** a coroutine is suspend on the current thread. */ + /** This method is called **after** a coroutine is suspended on the current thread. */ internal abstract fun restoreThreadContext(context: CoroutineContext, oldState: S) @Suppress("NOTHING_TO_INLINE") @@ -68,22 +65,25 @@ internal constructor( internal companion object { // Used to represent that the current slice has begun. @PublishedApi internal const val STATE_BEGIN: Int = 1 + // Used to represent that the current slice has ended. @PublishedApi internal const val STATE_END: Int = 0 + @PublishedApi @JvmField - internal val KEY: CoroutineContext.Key> = - object : CoroutineContext.Key> {} + internal val KEY: CoroutineContext.Key> = + object : CoroutineContext.Key> {} } } /** Builds an instance of the Platform specific [PlatformThreadContextElement]. */ @PublishedApi -internal expect fun buildThreadContextElement( +internal expect fun buildThreadContextElement( + tracer: T, category: String, name: String, flowIds: List, updateThreadContextBlock: (context: CoroutineContext) -> Unit, restoreThreadContextBlock: (context: CoroutineContext) -> Unit, - close: (platformThreadContextElement: PlatformThreadContextElement<*>) -> Unit, -): PlatformThreadContextElement + close: (platformThreadContextElement: PlatformThreadContextElement<*, T>) -> Unit, +): PlatformThreadContextElement diff --git a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/PerfettoTracer.kt b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/PerfettoTracer.kt index 29a8cb29f507d..9b8f6689762ff 100644 --- a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/PerfettoTracer.kt +++ b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/PerfettoTracer.kt @@ -79,13 +79,14 @@ public class PerfettoTracer(context: TraceContext) : Tracer(isEnabled = context. @ExperimentalContextPropagation override fun tokenForManualPropagation(): PropagationToken { - return inheritedPropagationToken(parent = null, track = currentThreadTrack()) + return inheritedPropagationToken(parent = null, tracer = this) } @DelicateTracingApi - override suspend fun tokenFromCoroutineContext(): PlatformThreadContextElement<*> { - val parent = currentCoroutineContext()[PlatformThreadContextElement.KEY] - val current = inheritedCoroutinePropagationToken(parent, currentThreadTrack()) + override suspend fun tokenFromCoroutineContext(): + PlatformThreadContextElement<*, PerfettoTracer> { + val parent = currentCoroutineContext().platformThreadContextElement() + val current = inheritedCoroutinePropagationToken(parent, tracer = this) return current } @@ -105,11 +106,12 @@ public class PerfettoTracer(context: TraceContext) : Tracer(isEnabled = context. token = PropagationUnsupportedToken, ) } else { + @Suppress("UNCHECKED_CAST") val parent = - token as? PlatformThreadContextElement<*> + token as? PlatformThreadContextElement<*, PerfettoTracer> ?: throw IllegalArgumentException("Unsupported token type $token") val track = currentThreadTrack() - val tokenElement = inheritedPropagationToken(parent = parent, track = track) + val tokenElement = inheritedPropagationToken(parent = parent, tracer = this) track.beginCoroutineSection(category = category, name = name, token = tokenElement) } } @@ -138,17 +140,15 @@ public class PerfettoTracer(context: TraceContext) : Tracer(isEnabled = context. tokenFromCoroutineContext() } else { // Context Propagation is explicit. + @Suppress("UNCHECKED_CAST") val parent = - token as? PlatformThreadContextElement<*> + token as? PlatformThreadContextElement<*, PerfettoTracer> ?: throw IllegalArgumentException("Unsupported token type $token") - inheritedCoroutinePropagationToken( - parent = parent, - track = currentThreadTrack(), - ) + inheritedCoroutinePropagationToken(parent = parent, tracer = this) } tokenElement.name = name tokenElement.category = category - val track = tokenElement.owner + val track = tokenElement.tracer.currentThreadTrack() track.beginCoroutineSection(category = category, name = name, token = tokenElement) } } diff --git a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/SliceTrack.kt b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/SliceTrack.kt index b18e363cd6442..bfbdbc2e0128b 100644 --- a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/SliceTrack.kt +++ b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/SliceTrack.kt @@ -90,7 +90,7 @@ public abstract class SliceTrack( internal inline fun beginCoroutineSection( category: String, name: String, - token: PlatformThreadContextElement<*>, + token: PlatformThreadContextElement<*, PerfettoTracer>, ): EventMetadataCloseable { eventMetadataCloseable.metadata = EmptyEventMetadata eventMetadataCloseable.closeable = EmptyCloseable diff --git a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/TraceTokens.kt b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/TraceTokens.kt index bc43919e7ef74..253ac6dc147fa 100644 --- a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/TraceTokens.kt +++ b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/TraceTokens.kt @@ -20,17 +20,29 @@ package androidx.tracing import androidx.tracing.PlatformThreadContextElement.Companion.STATE_BEGIN import androidx.tracing.PlatformThreadContextElement.Companion.STATE_END +import kotlin.coroutines.CoroutineContext + +@Suppress("NOTHING_TO_INLINE") +internal inline fun CoroutineContext.platformThreadContextElement(): + PlatformThreadContextElement<*, PerfettoTracer>? { + // This is a safe thing to do, given `PlatformThreadContextElement` is always `PerfettoTracer` + // aware. + @Suppress("UNCHECKED_CAST") + return this[PlatformThreadContextElement.KEY] + as? PlatformThreadContextElement<*, PerfettoTracer> +} @Suppress("NOTHING_TO_INLINE") internal inline fun inheritedPropagationToken( - parent: PlatformThreadContextElement<*>?, - track: ThreadTrack, -): PlatformThreadContextElement<*> { + parent: PlatformThreadContextElement<*, PerfettoTracer>?, + tracer: PerfettoTracer, +): PlatformThreadContextElement<*, PerfettoTracer> { val token = buildThreadContextElement( // Placeholder to be filled in by beginSection* APIs. // Start off with the parent category and names so we have something consistent // when using the PlatformThreadContextElement for explicit trace propagation. + tracer = tracer, category = parent?.category ?: DEFAULT_STRING, name = parent?.name ?: DEFAULT_STRING, flowIds = parent?.flowIds ?: listOf(monotonicId()), @@ -40,32 +52,32 @@ internal inline fun inheritedPropagationToken( if ( element.synchronizedCompareAndSet(expected = STATE_BEGIN, newValue = STATE_END) ) { - element.owner.endSection() + element.tracer.currentThreadTrack().endSection() } }, ) - token.owner = track return token } @Suppress("NOTHING_TO_INLINE") internal fun inheritedCoroutinePropagationToken( - parent: PlatformThreadContextElement<*>?, - track: ThreadTrack, -): PlatformThreadContextElement<*> { + parent: PlatformThreadContextElement<*, PerfettoTracer>?, + tracer: PerfettoTracer, +): PlatformThreadContextElement<*, PerfettoTracer> { val token = buildThreadContextElement( // Placeholder to be filled in by beginSection* APIs. // Start off with the parent category and names so we have something consistent // when using the PlatformThreadContextElement for explicit trace propagation. + tracer = tracer, category = parent?.category ?: DEFAULT_STRING, name = parent?.name ?: DEFAULT_STRING, flowIds = parent?.flowIds ?: listOf(monotonicId()), // This method is called before a coroutine is resumed on a thread that // belongs to a dispatcher. This can be called more than once. So avoid creating // slices unless we transition to `STATE_END`. - updateThreadContextBlock = { context -> - val contextElement = context[PlatformThreadContextElement.KEY] + updateThreadContextBlock = { context: CoroutineContext -> + val contextElement = context.platformThreadContextElement() val category = contextElement?.category val name = contextElement?.name if ( @@ -78,11 +90,13 @@ internal fun inheritedCoroutinePropagationToken( ) ) { val result = - contextElement.owner.beginCoroutineSection( - category = category, - name = name, - token = contextElement, - ) + contextElement.tracer + .currentThreadTrack() + .beginCoroutineSection( + category = category, + name = name, + token = contextElement, + ) result.metadata.dispatchToTraceSink() } }, @@ -90,7 +104,7 @@ internal fun inheritedCoroutinePropagationToken( // This method might be called more than once as well. So we want to be // idempotent. restoreThreadContextBlock = { context -> - val contextElement = context[PlatformThreadContextElement.KEY] + val contextElement = context.platformThreadContextElement() val name = contextElement?.name if ( contextElement != null && @@ -100,7 +114,7 @@ internal fun inheritedCoroutinePropagationToken( newValue = STATE_END, ) ) { - contextElement.owner.endSection() + contextElement.tracer.currentThreadTrack().endSection() } }, close = { platformThreadContextElement -> @@ -111,10 +125,9 @@ internal fun inheritedCoroutinePropagationToken( newValue = STATE_END, ) ) { - platformThreadContextElement.owner.endSection() + platformThreadContextElement.tracer.currentThreadTrack().endSection() } }, ) - token.owner = track return token } diff --git a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/Track.kt b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/Track.kt index 127332a3f73fb..49279f268c6d0 100644 --- a/tracing/tracing/src/commonMain/kotlin/androidx/tracing/Track.kt +++ b/tracing/tracing/src/commonMain/kotlin/androidx/tracing/Track.kt @@ -63,6 +63,7 @@ public abstract class Track( // this would be private, but internal prevents getters from being created @JvmField // avoid getter generation + @Volatile internal var currentPacketArray: PooledTracePacketArray? = pool.obtainTracePacketArray() internal fun flush() { diff --git a/tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.kt b/tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.kt deleted file mode 100644 index 6e1488a7bdc43..0000000000000 --- a/tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2025 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.tracing - -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.ThreadContextElement - -internal class DefaultThreadContextElement( - override var category: String, - override var name: String, - override val flowIds: List, - internal val updateThreadContextBlock: (context: CoroutineContext) -> Unit, - internal val restoreThreadContextBlock: (context: CoroutineContext) -> Unit, - internal val close: (element: PlatformThreadContextElement<*>) -> Unit, -) : - ThreadContextElement, - PlatformThreadContextElement(category = category, name = name, flowIds = flowIds) { - /** - * This method is called **before a coroutine is resumed** on a thread that belongs to a - * dispatcher. - */ - override fun restoreThreadContext(context: CoroutineContext, oldState: Unit) { - restoreThreadContextBlock(context) - } - - /** This method is called **after** a coroutine is suspend on the current thread. */ - override fun updateThreadContext(context: CoroutineContext) { - updateThreadContextBlock(context) - } - - override fun close() { - close(this) - owner = EmptyTraceContext.thread - } -} - -@PublishedApi -internal actual fun buildThreadContextElement( - category: String, - name: String, - flowIds: List, - updateThreadContextBlock: (context: CoroutineContext) -> Unit, - restoreThreadContextBlock: (context: CoroutineContext) -> Unit, - close: (platformThreadContextElement: PlatformThreadContextElement<*>) -> Unit, -): PlatformThreadContextElement { - return DefaultThreadContextElement( - category = category, - name = name, - flowIds = flowIds, - updateThreadContextBlock = updateThreadContextBlock, - restoreThreadContextBlock = restoreThreadContextBlock, - close = close, - ) -} diff --git a/tracing/tracing/src/androidMain/kotlin/androidx/tracing/ContextElements.android.kt b/tracing/tracing/src/jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt similarity index 78% rename from tracing/tracing/src/androidMain/kotlin/androidx/tracing/ContextElements.android.kt rename to tracing/tracing/src/jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt index 5ecbb749239ab..cc7267deddf5b 100644 --- a/tracing/tracing/src/androidMain/kotlin/androidx/tracing/ContextElements.android.kt +++ b/tracing/tracing/src/jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Android Open Source Project + * Copyright 2026 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. @@ -19,17 +19,22 @@ package androidx.tracing import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ThreadContextElement -@Suppress("KmpSignatureClash") // also defined in desktop -internal class DefaultThreadContextElement( +internal class DefaultThreadContextElement( + override val tracer: T, override var category: String, override var name: String, override val flowIds: List, internal val updateThreadContextBlock: (context: CoroutineContext) -> Unit, internal val restoreThreadContextBlock: (context: CoroutineContext) -> Unit, - internal val close: (element: PlatformThreadContextElement<*>) -> Unit, + internal val close: (element: PlatformThreadContextElement<*, T>) -> Unit, ) : ThreadContextElement, - PlatformThreadContextElement(category = category, name = name, flowIds = flowIds) { + PlatformThreadContextElement( + tracer = tracer, + category = category, + name = name, + flowIds = flowIds, + ) { /** * This method is called **before a coroutine is resumed** on a thread that belongs to a * dispatcher. @@ -38,27 +43,28 @@ internal class DefaultThreadContextElement( restoreThreadContextBlock(context) } - /** This method is called **after** a coroutine is suspend on the current thread. */ + /** This method is called **after** a coroutine is suspended on the current thread. */ override fun updateThreadContext(context: CoroutineContext) { updateThreadContextBlock(context) } override fun close() { close(this) - owner = EmptyTraceContext.thread } } @PublishedApi -internal actual fun buildThreadContextElement( +internal actual fun buildThreadContextElement( + tracer: T, category: String, name: String, flowIds: List, updateThreadContextBlock: (context: CoroutineContext) -> Unit, restoreThreadContextBlock: (context: CoroutineContext) -> Unit, - close: (platformThreadContextElement: PlatformThreadContextElement<*>) -> Unit, -): PlatformThreadContextElement { + close: (platformThreadContextElement: PlatformThreadContextElement<*, T>) -> Unit, +): PlatformThreadContextElement { return DefaultThreadContextElement( + tracer = tracer, category = category, name = name, flowIds = flowIds, From 43c092dc2bed16d1b2cea9f866d58583dd0f0983 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 29 Jan 2026 15:31:46 -0800 Subject: [PATCH 05/16] Add PointerEventType for pan and scale gestures starting and finishing Adding these allows denoting the start and end of pan gestures and scale gestures for trackpad events. These is useful for gesture handlers that want to know the full lifecycle of a specific gesture. Relnote: "Splits the new PointerEventType.Pan and into PointerEventType.PanStart, PointerEventType.PanMove and PointerEventType.PanEnd. Similarly, PointerEventType.Scale is split into PointerEventType.ScaleStart, PointerEventType.ScaleChange and PointerEventType.ScaleEnd. This split allows passing through additional information about the start and end of platform-interpreted pan and scale gestures, which then allows pointer input handlers in Compose to use this information to detect events appropriately, and especially know when a pan or scale is done." Bug: 479285849 Bug: 481333653 Test: Updated Change-Id: I4daf682c6cb3624bdbe935115a60fdb9edca19eb --- .../gestures/MouseWheelScrollable.kt | 4 +- .../compose/foundation/gestures/Scrollable.kt | 4 +- .../foundation/gestures/Transformable.kt | 24 ++++++++-- .../test/injectionscope/trackpad/PinchTest.kt | 36 ++++++++------- .../injectionscope/trackpad/ScrollTest.kt | 22 +++++---- compose/ui/ui/api/current.txt | 16 +++++-- compose/ui/ui/api/restricted_current.txt | 16 +++++-- compose/ui/ui/bcv/native/current.txt | 16 +++++-- .../ui/input/pointer/PointerEvent.android.kt | 32 +++++++++---- .../androidx/compose/ui/ComposeUiFlags.kt | 6 +-- .../compose/ui/input/pointer/PointerEvent.kt | 46 +++++++++++++++---- 11 files changed, 157 insertions(+), 65 deletions(-) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt index 73d6c201600b2..ebd8496382fce 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt @@ -65,7 +65,9 @@ internal class MouseWheelScrollingLogic( if ( pointerEvent.type != PointerEventType.Scroll && (!ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || - pointerEvent.type != PointerEventType.Pan) + (pointerEvent.type != PointerEventType.PanStart && + pointerEvent.type != PointerEventType.PanMove && + pointerEvent.type != PointerEventType.PanEnd)) ) return if (pointerEvent.isConsumed) return diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt index d5a1808897e45..2259512903ba2 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt @@ -555,7 +555,9 @@ internal class ScrollableNode( if ( pass == PointerEventPass.Initial && (pointerEvent.type == PointerEventType.Scroll || - pointerEvent.type == PointerEventType.Pan) + pointerEvent.type == PointerEventType.PanStart || + pointerEvent.type == PointerEventType.PanMove || + pointerEvent.type == PointerEventType.PanEnd) ) { ensureMouseWheelScrollNodeInitialized() } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt index c3668fb48214d..c64937b2d24ac 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt @@ -359,7 +359,10 @@ private fun AwaitPointerEventScope.consumePointerEventAsMouseWheelScrollOrNull( ): Offset? { if ( !pointer.keyboardModifiers.isCtrlPressed || - (pointer.type != PointerEventType.Scroll && pointer.type != PointerEventType.Pan) + (pointer.type != PointerEventType.Scroll && + pointer.type != PointerEventType.PanStart && + pointer.type != PointerEventType.PanMove && + pointer.type != PointerEventType.PanEnd) ) { return null } @@ -377,7 +380,11 @@ private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( pointer: PointerEvent, scrollConfig: ScrollConfig, ): Offset? { - if (pointer.type != PointerEventType.Pan) { + if ( + pointer.type != PointerEventType.PanStart && + pointer.type != PointerEventType.PanMove && + pointer.type != PointerEventType.PanEnd + ) { return null } val scrollDelta = with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } @@ -391,7 +398,11 @@ private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( } private fun AwaitPointerEventScope.consumePointerEventAsScaleOrNull(pointer: PointerEvent): Float? { - if (pointer.type != PointerEventType.Scale) { + if ( + pointer.type != PointerEventType.ScaleStart && + pointer.type != PointerEventType.ScaleChange && + pointer.type != PointerEventType.ScaleEnd + ) { return null } var scaleDelta = 1f @@ -426,7 +437,12 @@ private suspend fun AwaitPointerEventScope.detectZoom( val canceled = event.changes.fastAny { it.isConsumed } || (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled && - (event.type == PointerEventType.Pan || event.type == PointerEventType.Scale)) + (event.type == PointerEventType.PanStart || + event.type == PointerEventType.PanMove || + event.type == PointerEventType.PanEnd || + event.type == PointerEventType.ScaleStart || + event.type == PointerEventType.ScaleChange || + event.type == PointerEventType.ScaleEnd)) if (!canceled) { val zoomChange = event.calculateZoom() val rotationChange = event.calculateRotation() diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt index 5f5f17d4001a7..ec221ed8eaf70 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt @@ -28,7 +28,9 @@ import androidx.compose.ui.input.pointer.PointerEventType.Companion.Enter import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move import androidx.compose.ui.input.pointer.PointerEventType.Companion.Press import androidx.compose.ui.input.pointer.PointerEventType.Companion.Release -import androidx.compose.ui.input.pointer.PointerEventType.Companion.Scale +import androidx.compose.ui.input.pointer.PointerEventType.Companion.ScaleChange +import androidx.compose.ui.input.pointer.PointerEventType.Companion.ScaleEnd +import androidx.compose.ui.input.pointer.PointerEventType.Companion.ScaleStart import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.InputDispatcher @@ -97,7 +99,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleStart } else { Press }, @@ -124,7 +126,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -148,7 +150,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -167,7 +169,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -191,7 +193,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -210,7 +212,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -234,7 +236,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -253,7 +255,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleEnd } else { Release }, @@ -318,7 +320,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleStart } else { Press }, @@ -344,7 +346,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -368,7 +370,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -387,7 +389,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -411,7 +413,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -430,7 +432,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -454,7 +456,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -473,7 +475,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleEnd } else { Release }, diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt index 84c7b6f308160..3e49c356855ff 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt @@ -26,7 +26,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEventType.Companion.Enter import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move -import androidx.compose.ui.input.pointer.PointerEventType.Companion.Pan +import androidx.compose.ui.input.pointer.PointerEventType.Companion.PanEnd +import androidx.compose.ui.input.pointer.PointerEventType.Companion.PanMove +import androidx.compose.ui.input.pointer.PointerEventType.Companion.PanStart import androidx.compose.ui.input.pointer.PointerEventType.Companion.Press import androidx.compose.ui.input.pointer.PointerEventType.Companion.Release import androidx.compose.ui.input.pointer.PointerType @@ -101,7 +103,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T, PanStart, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -132,7 +134,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 2, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 2, PanMove, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -163,7 +165,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 3, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 3, PanEnd, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -241,7 +243,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T, PanStart, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -272,7 +274,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 2, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 2, PanMove, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(-10f) @@ -303,7 +305,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 3, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 3, PanEnd, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -385,7 +387,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T, PanStart, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) @@ -416,7 +418,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 2, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 2, PanMove, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(-10f) @@ -447,7 +449,7 @@ class ScrollTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - event.verifyTrackpadEvent(T * 3, Pan, false, Offset.Zero) + event.verifyTrackpadEvent(T * 3, PanEnd, false, Offset.Zero) assertThat(event.classification) .isEqualTo(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) assertThat(event.axisGestureScrollXDistance).isEqualTo(0f) diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index 9268d2c1417bb..101f37ab091ee 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt @@ -2423,19 +2423,27 @@ package androidx.compose.ui.input.pointer { method @BytecodeOnly public int getEnter-7fucELk(); method @BytecodeOnly public int getExit-7fucELk(); method @BytecodeOnly public int getMove-7fucELk(); - method @BytecodeOnly public int getPan-7fucELk(); + method @BytecodeOnly public int getPanEnd-7fucELk(); + method @BytecodeOnly public int getPanMove-7fucELk(); + method @BytecodeOnly public int getPanStart-7fucELk(); method @BytecodeOnly public int getPress-7fucELk(); method @BytecodeOnly public int getRelease-7fucELk(); - method @BytecodeOnly public int getScale-7fucELk(); + method @BytecodeOnly public int getScaleChange-7fucELk(); + method @BytecodeOnly public int getScaleEnd-7fucELk(); + method @BytecodeOnly public int getScaleStart-7fucELk(); method @BytecodeOnly public int getScroll-7fucELk(); method @BytecodeOnly public int getUnknown-7fucELk(); property public androidx.compose.ui.input.pointer.PointerEventType Enter; property public androidx.compose.ui.input.pointer.PointerEventType Exit; property public androidx.compose.ui.input.pointer.PointerEventType Move; - property public androidx.compose.ui.input.pointer.PointerEventType Pan; + property public androidx.compose.ui.input.pointer.PointerEventType PanEnd; + property public androidx.compose.ui.input.pointer.PointerEventType PanMove; + property public androidx.compose.ui.input.pointer.PointerEventType PanStart; property public androidx.compose.ui.input.pointer.PointerEventType Press; property public androidx.compose.ui.input.pointer.PointerEventType Release; - property public androidx.compose.ui.input.pointer.PointerEventType Scale; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleChange; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleEnd; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleStart; property public androidx.compose.ui.input.pointer.PointerEventType Scroll; property public androidx.compose.ui.input.pointer.PointerEventType Unknown; } diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt index 9a612aad42684..cb65611d15dc6 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt @@ -2424,19 +2424,27 @@ package androidx.compose.ui.input.pointer { method @BytecodeOnly public int getEnter-7fucELk(); method @BytecodeOnly public int getExit-7fucELk(); method @BytecodeOnly public int getMove-7fucELk(); - method @BytecodeOnly public int getPan-7fucELk(); + method @BytecodeOnly public int getPanEnd-7fucELk(); + method @BytecodeOnly public int getPanMove-7fucELk(); + method @BytecodeOnly public int getPanStart-7fucELk(); method @BytecodeOnly public int getPress-7fucELk(); method @BytecodeOnly public int getRelease-7fucELk(); - method @BytecodeOnly public int getScale-7fucELk(); + method @BytecodeOnly public int getScaleChange-7fucELk(); + method @BytecodeOnly public int getScaleEnd-7fucELk(); + method @BytecodeOnly public int getScaleStart-7fucELk(); method @BytecodeOnly public int getScroll-7fucELk(); method @BytecodeOnly public int getUnknown-7fucELk(); property public androidx.compose.ui.input.pointer.PointerEventType Enter; property public androidx.compose.ui.input.pointer.PointerEventType Exit; property public androidx.compose.ui.input.pointer.PointerEventType Move; - property public androidx.compose.ui.input.pointer.PointerEventType Pan; + property public androidx.compose.ui.input.pointer.PointerEventType PanEnd; + property public androidx.compose.ui.input.pointer.PointerEventType PanMove; + property public androidx.compose.ui.input.pointer.PointerEventType PanStart; property public androidx.compose.ui.input.pointer.PointerEventType Press; property public androidx.compose.ui.input.pointer.PointerEventType Release; - property public androidx.compose.ui.input.pointer.PointerEventType Scale; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleChange; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleEnd; + property public androidx.compose.ui.input.pointer.PointerEventType ScaleStart; property public androidx.compose.ui.input.pointer.PointerEventType Scroll; property public androidx.compose.ui.input.pointer.PointerEventType Unknown; } diff --git a/compose/ui/ui/bcv/native/current.txt b/compose/ui/ui/bcv/native/current.txt index 712b4840d91d0..7cd7b1a08e664 100644 --- a/compose/ui/ui/bcv/native/current.txt +++ b/compose/ui/ui/bcv/native/current.txt @@ -3205,14 +3205,22 @@ final value class androidx.compose.ui.input.pointer/PointerEventType { // androi final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Exit.|(){}[0] final val Move // androidx.compose.ui.input.pointer/PointerEventType.Companion.Move|{}Move[0] final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Move.|(){}[0] - final val Pan // androidx.compose.ui.input.pointer/PointerEventType.Companion.Pan|{}Pan[0] - final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Pan.|(){}[0] + final val PanEnd // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanEnd|{}PanEnd[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanEnd.|(){}[0] + final val PanMove // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanMove|{}PanMove[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanMove.|(){}[0] + final val PanStart // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanStart|{}PanStart[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.PanStart.|(){}[0] final val Press // androidx.compose.ui.input.pointer/PointerEventType.Companion.Press|{}Press[0] final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Press.|(){}[0] final val Release // androidx.compose.ui.input.pointer/PointerEventType.Companion.Release|{}Release[0] final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Release.|(){}[0] - final val Scale // androidx.compose.ui.input.pointer/PointerEventType.Companion.Scale|{}Scale[0] - final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Scale.|(){}[0] + final val ScaleChange // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleChange|{}ScaleChange[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleChange.|(){}[0] + final val ScaleEnd // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleEnd|{}ScaleEnd[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleEnd.|(){}[0] + final val ScaleStart // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleStart|{}ScaleStart[0] + final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.ScaleStart.|(){}[0] final val Scroll // androidx.compose.ui.input.pointer/PointerEventType.Companion.Scroll|{}Scroll[0] final fun (): androidx.compose.ui.input.pointer/PointerEventType // androidx.compose.ui.input.pointer/PointerEventType.Companion.Scroll.|(){}[0] final val Unknown // androidx.compose.ui.input.pointer/PointerEventType.Companion.Unknown|{}Unknown[0] diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt index 913b83cf29a3b..28b4d4eb36f0c 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt @@ -121,22 +121,38 @@ internal actual constructor( val isPinch = Build.VERSION.SDK_INT >= 29 && motionEvent.classification == CLASSIFICATION_PINCH return when (motionEvent.actionMasked) { - MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_DOWN -> { + if (isTwoFingerSwipe && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { + PointerEventType.PanStart + } else if (isPinch && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { + PointerEventType.ScaleStart + } else { + PointerEventType.Press + } + } MotionEvent.ACTION_POINTER_DOWN -> { if (isTwoFingerSwipe && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Pan + PointerEventType.PanStart } else if (isPinch && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Scale + PointerEventType.ScaleChange } else { PointerEventType.Press } } - MotionEvent.ACTION_UP, + MotionEvent.ACTION_UP -> { + if (isTwoFingerSwipe && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { + PointerEventType.PanEnd + } else if (isPinch && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { + PointerEventType.ScaleEnd + } else { + PointerEventType.Release + } + } MotionEvent.ACTION_POINTER_UP -> { if (isTwoFingerSwipe && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Pan + PointerEventType.PanEnd } else if (isPinch && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Scale + PointerEventType.ScaleChange } else { PointerEventType.Release } @@ -144,9 +160,9 @@ internal actual constructor( MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_MOVE -> { if (isTwoFingerSwipe && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Pan + PointerEventType.PanMove } else if (isPinch && ComposeUiFlags.isTrackpadGestureHandlingEnabled) { - PointerEventType.Scale + PointerEventType.ScaleChange } else { PointerEventType.Move } diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt index 650f561a5a9f1..7a7b21e6d853b 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt @@ -127,9 +127,9 @@ object ComposeUiFlags { * Enables support of trackpad gesture events. * * If enabled, [androidx.compose.ui.input.pointer.PointerEvent]s can have type of - * [androidx.compose.ui.input.pointer.PointerEventType.Pan] and - * [androidx.compose.ui.input.pointer.PointerEventType.Scale], corresponding to gestures on a - * trackpad. + * [androidx.compose.ui.input.pointer.PointerEventType.PanMove] and + * [androidx.compose.ui.input.pointer.PointerEventType.ScaleChange], corresponding to + * system-recognized gestures on a trackpad. * * These trackpad gestures will also generally be treated as mouse, with the exact behavior * depending on platform specifics. diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt index 18a25c9f04f08..2fea37cb94dc3 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt @@ -299,17 +299,41 @@ value class PointerEventType private constructor(internal val value: Int) { val Scroll = PointerEventType(6) /** - * A scale event was sent. This can happen, for example, due to a trackpad gesture. This - * event indicates that the [PointerInputChange.scaleGestureFactor]'s [Offset] is different - * from 1. + * A scale started. This can happen, for example, due to a trackpad gesture recognized by + * the platform. Such a gesture will start with an event of [ScaleStart], followed by some + * number of [ScaleChange]s, and finally a [ScaleEnd]. */ - val Scale = PointerEventType(7) + val ScaleStart = PointerEventType(7) /** - * A pan event was sent. This can happen, for example, due to a trackpad gesture. This event - * indicates that the [PointerInputChange.panGestureOffset]'s [Offset]. + * An intermediate scale move. This can happen, for example, due to a trackpad gesture + * recognized by the platform. This event indicates that the + * [PointerInputChange.scaleGestureFactor]'s [Offset] may be different from 1. */ - val Pan = PointerEventType(8) + val ScaleChange = PointerEventType(8) + + /** + * A scale ended. This can happen, for example, due to a trackpad gesture recognized by the + * platform. + */ + val ScaleEnd = PointerEventType(9) + + /** + * A pan started. This can happen, for example, due to a trackpad gesture recognized by the + * platform. Such a gesture will start with an event type of [PanStart], followed by some + * number of [PanMove]s, and finally a [PanEnd] + */ + val PanStart = PointerEventType(10) + + /** + * An intermediate pan move. This can happen, for example, due to a trackpad gesture + * recognized by the platform. This event indicates that the + * [PointerInputChange.panGestureOffset]'s [Offset] may be non-zero. + */ + val PanMove = PointerEventType(11) + + /** A pan ended. This can happen, for example, due to a trackpad gesture. */ + val PanEnd = PointerEventType(12) } override fun toString(): String = @@ -320,8 +344,12 @@ value class PointerEventType private constructor(internal val value: Int) { Enter -> "Enter" Exit -> "Exit" Scroll -> "Scroll" - Scale -> "Scale" - Pan -> "Pan" + ScaleStart -> "ScaleStart" + ScaleChange -> "ScaleChange" + ScaleEnd -> "ScaleFinish" + PanStart -> "PanStart" + PanMove -> "Pan" + PanEnd -> "PanEnd" else -> "Unknown" } } From 1520c8efa9709f48b218389fd3d0a1f6b09ff252 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 29 Jan 2026 15:33:41 -0800 Subject: [PATCH 06/16] Add trackpad testing APIs for injecting multimove pans and scales and adjust APIs This allows simulating a pan with specific velocities, similar to touchscreen swipes, as well as scales. This also adjusts the naming of the methods in response to API feedback. Relnote: "Adds more functionality for trackpad testing APIs, to simulate different curves and inputs for a trackpad gesture." Fixes: 473603145 Fixes: 479213358 Test: New tests Change-Id: I290fb4dea339278201099d0192a7f617b0aaeed0 --- .../compose/foundation/ScrollableTest.kt | 13 +- .../compose/foundation/TransformableTest.kt | 18 +- compose/ui/ui-test/api/current.txt | 18 +- compose/ui/ui-test/api/restricted_current.txt | 18 +- compose/ui/ui-test/bcv/native/current.txt | 12 +- .../samples/TrackpadInjectionScopeSamples.kt | 14 +- .../trackpad/{ScrollTest.kt => PanTest.kt} | 11 +- .../trackpad/PanWithVelocityTest.kt | 178 ++++++++++ .../trackpad/{PinchTest.kt => ScaleTest.kt} | 9 +- .../inputdispatcher/TrackpadEventsTest.kt | 30 +- .../ui/test/AndroidInputDispatcher.android.kt | 46 +-- .../compose/ui/test/InputDispatcher.kt | 83 ++++- .../compose/ui/test/TrackpadInjectionScope.kt | 306 +++++++++++++++--- 13 files changed, 640 insertions(+), 116 deletions(-) rename compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/{ScrollTest.kt => PanTest.kt} (99%) create mode 100644 compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt rename compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/{PinchTest.kt => ScaleTest.kt} (99%) diff --git a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt index f6acd9d554551..095b3c3a3c10c 100644 --- a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt +++ b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt @@ -112,6 +112,7 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.pan import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performSemanticsAction @@ -470,7 +471,7 @@ class ScrollableTest { Modifier.scrollable(state = controller, orientation = Orientation.Horizontal) } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(100f, 0f)) // only moved horizontally + this.pan(Offset(100f, 0f)) // only moved horizontally } var lastTotal = @@ -480,12 +481,12 @@ class ScrollableTest { } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(0f, 100f)) // only moved vertically + this.pan(Offset(0f, 100f)) // only moved vertically } rule.runOnIdle { assertThat(total).isEqualTo(lastTotal) } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(-100f, 0f)) // only moved horizontally + this.pan(Offset(-100f, 0f)) // only moved horizontally } rule.runOnIdle { assertThat(total).isLessThan(0.01f) } } @@ -1016,7 +1017,7 @@ class ScrollableTest { Modifier.scrollable(state = scrollableState, orientation = Orientation.Vertical) } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(0f, 100f)) // only moved vertically + this.pan(Offset(0f, 100f)) // only moved vertically } var lastTotal = @@ -1026,12 +1027,12 @@ class ScrollableTest { } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(100f, 0f)) // only moved horizontally + this.pan(Offset(100f, 0f)) // only moved horizontally } rule.runOnIdle { assertThat(total).isEqualTo(lastTotal) } rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { - this.scroll(Offset(0f, -100f)) // only moved vertically + this.pan(Offset(0f, -100f)) // only moved vertically } rule.runOnIdle { assertThat(total).isLessThan(0.01f) } } diff --git a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/TransformableTest.kt b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/TransformableTest.kt index 3448de6335045..bf9d505d52e60 100644 --- a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/TransformableTest.kt +++ b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/TransformableTest.kt @@ -49,11 +49,13 @@ import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.pan import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performMultiModalInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTrackpadInput import androidx.compose.ui.test.pinch +import androidx.compose.ui.test.scale import androidx.compose.ui.test.withKeysDown import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @@ -257,7 +259,7 @@ class TransformableTest { rule.onNodeWithTag(TEST_TAG).performTrackpadInput { moveTo(center) - scroll(expected) + pan(expected) } rule.runOnIdle { @@ -491,7 +493,7 @@ class TransformableTest { rule.onNodeWithTag(TEST_TAG).performTrackpadInput { centerOffset = center moveTo(center) - pinch(scaleFactor = 0.5f) + scale(scaleFactor = 0.5f) } rule.runOnIdle { @@ -525,7 +527,7 @@ class TransformableTest { rule.onNodeWithTag(TEST_TAG).performTrackpadInput { centerOffset = center moveTo(center) - pinch(scaleFactor = 2f) + scale(scaleFactor = 2f) } rule.runOnIdle { @@ -1026,7 +1028,7 @@ class TransformableTest { withKeysDown(listOf(Key.CtrlLeft)) { trackpad { moveTo(Offset(20f, 30f)) - scroll(Offset(0f, SCROLL_FACTOR * 1.dp.toPx())) + pan(Offset(0f, SCROLL_FACTOR * 1.dp.toPx())) } } } @@ -1071,7 +1073,7 @@ class TransformableTest { withKeysDown(listOf(Key.CtrlLeft)) { trackpad { moveTo(Offset(20f, 30f)) - scroll(Offset(0f, -SCROLL_FACTOR * 1.dp.toPx())) + pan(Offset(0f, -SCROLL_FACTOR * 1.dp.toPx())) } } } @@ -1171,7 +1173,7 @@ class TransformableTest { withKeysDown(listOf(Key.CtrlLeft)) { trackpad { moveTo(Offset(20f, 30f)) - scroll(Offset(0f, -SCROLL_FACTOR * 1.dp.toPx())) + pan(Offset(0f, -SCROLL_FACTOR * 1.dp.toPx())) } } } @@ -1224,7 +1226,7 @@ class TransformableTest { withKeysDown(listOf(Key.CtrlLeft)) { trackpad { moveTo(Offset(20f, 30f)) - scroll(Offset(0f, 100f)) + pan(Offset(0f, 100f)) } } } @@ -1293,7 +1295,7 @@ class TransformableTest { } } - rule.onNodeWithTag(TEST_TAG).performTrackpadInput { scroll(Offset(0f, 100f)) } + rule.onNodeWithTag(TEST_TAG).performTrackpadInput { pan(Offset(0f, 100f)) } rule.runOnIdle { assertWithMessage("Should not scroll").that(scrollState.value).isEqualTo(0) diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt index 6bf760234883d..20cab6e3b6c6e 100644 --- a/compose/ui/ui-test/api/current.txt +++ b/compose/ui/ui-test/api/current.txt @@ -828,15 +828,19 @@ package androidx.compose.ui.test { method @KotlinOnly public void moveTo(androidx.compose.ui.geometry.Offset position, optional long delayMillis); method @BytecodeOnly public void moveTo-3MmeM6k(long, long); method @BytecodeOnly public static void moveTo-3MmeM6k$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, long, int, Object!); - method public void pinch(float scaleFactor); + method public void panEnd(); + method @KotlinOnly public void panMoveBy(androidx.compose.ui.geometry.Offset delta); + method @BytecodeOnly public void panMoveBy-k-4lQ0M(long); + method public void panStart(); method @KotlinOnly public void press(optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public void press-SMKQcqU(int); method @BytecodeOnly public static void press-SMKQcqU$default(androidx.compose.ui.test.TrackpadInjectionScope!, int, int, Object!); method @KotlinOnly public void release(optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public void release-SMKQcqU(int); method @BytecodeOnly public static void release-SMKQcqU$default(androidx.compose.ui.test.TrackpadInjectionScope!, int, int, Object!); - method @KotlinOnly public void scroll(androidx.compose.ui.geometry.Offset offset); - method @BytecodeOnly public void scroll-k-4lQ0M(long); + method public void scaleChangeBy(@FloatRange(from=0.0, fromInclusive=false) float scaleFactor); + method public void scaleEnd(); + method public void scaleStart(); method @KotlinOnly public default void updatePointerBy(androidx.compose.ui.geometry.Offset delta); method @BytecodeOnly public default void updatePointerBy-k-4lQ0M(long); method @KotlinOnly public void updatePointerTo(androidx.compose.ui.geometry.Offset position); @@ -865,9 +869,17 @@ package androidx.compose.ui.test { method @KotlinOnly public static void longClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position, optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public static void longClick-xhG_qxo(androidx.compose.ui.test.TrackpadInjectionScope, long, int); method @BytecodeOnly public static void longClick-xhG_qxo$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, int, Object!); + method @KotlinOnly public static void pan(androidx.compose.ui.test.TrackpadInjectionScope, androidx.compose.ui.geometry.Offset offset); + method public static void pan(androidx.compose.ui.test.TrackpadInjectionScope, kotlin.jvm.functions.Function1 curve, optional long durationMillis, optional java.util.List keyTimes); + method @BytecodeOnly public static void pan$default(androidx.compose.ui.test.TrackpadInjectionScope!, kotlin.jvm.functions.Function1!, long, java.util.List!, int, Object!); + method @BytecodeOnly public static void pan-Uv8p0NA(androidx.compose.ui.test.TrackpadInjectionScope, long); + method @KotlinOnly public static void panWithVelocity(androidx.compose.ui.test.TrackpadInjectionScope, androidx.compose.ui.geometry.Offset offset, @FloatRange(from=0.0) float endVelocity, optional long durationMillis); + method @BytecodeOnly public static void panWithVelocity-ubNVwUQ(androidx.compose.ui.test.TrackpadInjectionScope, long, @FloatRange(from=0.0) float, long); + method @BytecodeOnly public static void panWithVelocity-ubNVwUQ$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, float, long, int, Object!); method @KotlinOnly public static void rightClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position); method @BytecodeOnly public static void rightClick-Uv8p0NA(androidx.compose.ui.test.TrackpadInjectionScope, long); method @BytecodeOnly public static void rightClick-Uv8p0NA$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, Object!); + method public static void scale(androidx.compose.ui.test.TrackpadInjectionScope, @FloatRange(from=0.0, fromInclusive=false) float scaleFactor); method @KotlinOnly public static void tripleClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position, optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public static void tripleClick-xhG_qxo(androidx.compose.ui.test.TrackpadInjectionScope, long, int); method @BytecodeOnly public static void tripleClick-xhG_qxo$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, int, Object!); diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt index 3ea281c08a901..f65b03db3a898 100644 --- a/compose/ui/ui-test/api/restricted_current.txt +++ b/compose/ui/ui-test/api/restricted_current.txt @@ -830,15 +830,19 @@ package androidx.compose.ui.test { method @KotlinOnly public void moveTo(androidx.compose.ui.geometry.Offset position, optional long delayMillis); method @BytecodeOnly public void moveTo-3MmeM6k(long, long); method @BytecodeOnly public static void moveTo-3MmeM6k$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, long, int, Object!); - method public void pinch(float scaleFactor); + method public void panEnd(); + method @KotlinOnly public void panMoveBy(androidx.compose.ui.geometry.Offset delta); + method @BytecodeOnly public void panMoveBy-k-4lQ0M(long); + method public void panStart(); method @KotlinOnly public void press(optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public void press-SMKQcqU(int); method @BytecodeOnly public static void press-SMKQcqU$default(androidx.compose.ui.test.TrackpadInjectionScope!, int, int, Object!); method @KotlinOnly public void release(optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public void release-SMKQcqU(int); method @BytecodeOnly public static void release-SMKQcqU$default(androidx.compose.ui.test.TrackpadInjectionScope!, int, int, Object!); - method @KotlinOnly public void scroll(androidx.compose.ui.geometry.Offset offset); - method @BytecodeOnly public void scroll-k-4lQ0M(long); + method public void scaleChangeBy(@FloatRange(from=0.0, fromInclusive=false) float scaleFactor); + method public void scaleEnd(); + method public void scaleStart(); method @KotlinOnly public default void updatePointerBy(androidx.compose.ui.geometry.Offset delta); method @BytecodeOnly public default void updatePointerBy-k-4lQ0M(long); method @KotlinOnly public void updatePointerTo(androidx.compose.ui.geometry.Offset position); @@ -867,9 +871,17 @@ package androidx.compose.ui.test { method @KotlinOnly public static void longClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position, optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public static void longClick-xhG_qxo(androidx.compose.ui.test.TrackpadInjectionScope, long, int); method @BytecodeOnly public static void longClick-xhG_qxo$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, int, Object!); + method @KotlinOnly public static void pan(androidx.compose.ui.test.TrackpadInjectionScope, androidx.compose.ui.geometry.Offset offset); + method public static void pan(androidx.compose.ui.test.TrackpadInjectionScope, kotlin.jvm.functions.Function1 curve, optional long durationMillis, optional java.util.List keyTimes); + method @BytecodeOnly public static void pan$default(androidx.compose.ui.test.TrackpadInjectionScope!, kotlin.jvm.functions.Function1!, long, java.util.List!, int, Object!); + method @BytecodeOnly public static void pan-Uv8p0NA(androidx.compose.ui.test.TrackpadInjectionScope, long); + method @KotlinOnly public static void panWithVelocity(androidx.compose.ui.test.TrackpadInjectionScope, androidx.compose.ui.geometry.Offset offset, @FloatRange(from=0.0) float endVelocity, optional long durationMillis); + method @BytecodeOnly public static void panWithVelocity-ubNVwUQ(androidx.compose.ui.test.TrackpadInjectionScope, long, @FloatRange(from=0.0) float, long); + method @BytecodeOnly public static void panWithVelocity-ubNVwUQ$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, float, long, int, Object!); method @KotlinOnly public static void rightClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position); method @BytecodeOnly public static void rightClick-Uv8p0NA(androidx.compose.ui.test.TrackpadInjectionScope, long); method @BytecodeOnly public static void rightClick-Uv8p0NA$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, Object!); + method public static void scale(androidx.compose.ui.test.TrackpadInjectionScope, @FloatRange(from=0.0, fromInclusive=false) float scaleFactor); method @KotlinOnly public static void tripleClick(androidx.compose.ui.test.TrackpadInjectionScope, optional androidx.compose.ui.geometry.Offset position, optional androidx.compose.ui.test.MouseButton button); method @BytecodeOnly public static void tripleClick-xhG_qxo(androidx.compose.ui.test.TrackpadInjectionScope, long, int); method @BytecodeOnly public static void tripleClick-xhG_qxo$default(androidx.compose.ui.test.TrackpadInjectionScope!, long, int, int, Object!); diff --git a/compose/ui/ui-test/bcv/native/current.txt b/compose/ui/ui-test/bcv/native/current.txt index 108804070feba..003ecfa4217ab 100644 --- a/compose/ui/ui-test/bcv/native/current.txt +++ b/compose/ui/ui-test/bcv/native/current.txt @@ -144,10 +144,14 @@ abstract interface androidx.compose.ui.test/TrackpadInjectionScope : androidx.co abstract fun enter(androidx.compose.ui.geometry/Offset = ..., kotlin/Long = ...) // androidx.compose.ui.test/TrackpadInjectionScope.enter|enter(androidx.compose.ui.geometry.Offset;kotlin.Long){}[0] abstract fun exit(androidx.compose.ui.geometry/Offset = ..., kotlin/Long = ...) // androidx.compose.ui.test/TrackpadInjectionScope.exit|exit(androidx.compose.ui.geometry.Offset;kotlin.Long){}[0] abstract fun moveTo(androidx.compose.ui.geometry/Offset, kotlin/Long = ...) // androidx.compose.ui.test/TrackpadInjectionScope.moveTo|moveTo(androidx.compose.ui.geometry.Offset;kotlin.Long){}[0] - abstract fun pinch(kotlin/Float) // androidx.compose.ui.test/TrackpadInjectionScope.pinch|pinch(kotlin.Float){}[0] + abstract fun panEnd() // androidx.compose.ui.test/TrackpadInjectionScope.panEnd|panEnd(){}[0] + abstract fun panMoveBy(androidx.compose.ui.geometry/Offset) // androidx.compose.ui.test/TrackpadInjectionScope.panMoveBy|panMoveBy(androidx.compose.ui.geometry.Offset){}[0] + abstract fun panStart() // androidx.compose.ui.test/TrackpadInjectionScope.panStart|panStart(){}[0] abstract fun press(androidx.compose.ui.test/MouseButton = ...) // androidx.compose.ui.test/TrackpadInjectionScope.press|press(androidx.compose.ui.test.MouseButton){}[0] abstract fun release(androidx.compose.ui.test/MouseButton = ...) // androidx.compose.ui.test/TrackpadInjectionScope.release|release(androidx.compose.ui.test.MouseButton){}[0] - abstract fun scroll(androidx.compose.ui.geometry/Offset) // androidx.compose.ui.test/TrackpadInjectionScope.scroll|scroll(androidx.compose.ui.geometry.Offset){}[0] + abstract fun scaleChangeBy(kotlin/Float) // androidx.compose.ui.test/TrackpadInjectionScope.scaleChangeBy|scaleChangeBy(kotlin.Float){}[0] + abstract fun scaleEnd() // androidx.compose.ui.test/TrackpadInjectionScope.scaleEnd|scaleEnd(){}[0] + abstract fun scaleStart() // androidx.compose.ui.test/TrackpadInjectionScope.scaleStart|scaleStart(){}[0] abstract fun updatePointerTo(androidx.compose.ui.geometry/Offset) // androidx.compose.ui.test/TrackpadInjectionScope.updatePointerTo|updatePointerTo(androidx.compose.ui.geometry.Offset){}[0] open fun moveBy(androidx.compose.ui.geometry/Offset, kotlin/Long = ...) // androidx.compose.ui.test/TrackpadInjectionScope.moveBy|moveBy(androidx.compose.ui.geometry.Offset;kotlin.Long){}[0] open fun updatePointerBy(androidx.compose.ui.geometry/Offset) // androidx.compose.ui.test/TrackpadInjectionScope.updatePointerBy|updatePointerBy(androidx.compose.ui.geometry.Offset){}[0] @@ -470,7 +474,11 @@ final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui. final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/doubleClick(androidx.compose.ui.geometry/Offset = ..., androidx.compose.ui.test/MouseButton = ...) // androidx.compose.ui.test/doubleClick|doubleClick@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset;androidx.compose.ui.test.MouseButton){}[0] final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/dragAndDrop(androidx.compose.ui.geometry/Offset, androidx.compose.ui.geometry/Offset, androidx.compose.ui.test/MouseButton = ..., kotlin/Long = ...) // androidx.compose.ui.test/dragAndDrop|dragAndDrop@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset;androidx.compose.ui.geometry.Offset;androidx.compose.ui.test.MouseButton;kotlin.Long){}[0] final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/longClick(androidx.compose.ui.geometry/Offset = ..., androidx.compose.ui.test/MouseButton = ...) // androidx.compose.ui.test/longClick|longClick@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset;androidx.compose.ui.test.MouseButton){}[0] +final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/pan(androidx.compose.ui.geometry/Offset) // androidx.compose.ui.test/pan|pan@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset){}[0] +final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/pan(kotlin/Function1, kotlin/Long = ..., kotlin.collections/List = ...) // androidx.compose.ui.test/pan|pan@androidx.compose.ui.test.TrackpadInjectionScope(kotlin.Function1;kotlin.Long;kotlin.collections.List){}[0] +final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/panWithVelocity(androidx.compose.ui.geometry/Offset, kotlin/Float, kotlin/Long = ...) // androidx.compose.ui.test/panWithVelocity|panWithVelocity@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset;kotlin.Float;kotlin.Long){}[0] final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/rightClick(androidx.compose.ui.geometry/Offset = ...) // androidx.compose.ui.test/rightClick|rightClick@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset){}[0] +final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/scale(kotlin/Float) // androidx.compose.ui.test/scale|scale@androidx.compose.ui.test.TrackpadInjectionScope(kotlin.Float){}[0] final fun (androidx.compose.ui.test/TrackpadInjectionScope).androidx.compose.ui.test/tripleClick(androidx.compose.ui.geometry/Offset = ..., androidx.compose.ui.test/MouseButton = ...) // androidx.compose.ui.test/tripleClick|tripleClick@androidx.compose.ui.test.TrackpadInjectionScope(androidx.compose.ui.geometry.Offset;androidx.compose.ui.test.MouseButton){}[0] final fun (androidx.compose.ui.unit/Dp).androidx.compose.ui.test/assertIsEqualTo(androidx.compose.ui.unit/Dp, kotlin/String, androidx.compose.ui.unit/Dp = ...) // androidx.compose.ui.test/assertIsEqualTo|assertIsEqualTo@androidx.compose.ui.unit.Dp(androidx.compose.ui.unit.Dp;kotlin.String;androidx.compose.ui.unit.Dp){}[0] final fun <#A: kotlin/Function> (androidx.compose.ui.test/SemanticsNodeInteraction).androidx.compose.ui.test/performSemanticsAction(androidx.compose.ui.semantics/SemanticsPropertyKey>, kotlin/Function1<#A, kotlin/Unit>): androidx.compose.ui.test/SemanticsNodeInteraction // androidx.compose.ui.test/performSemanticsAction|performSemanticsAction@androidx.compose.ui.test.SemanticsNodeInteraction(androidx.compose.ui.semantics.SemanticsPropertyKey>;kotlin.Function1<0:0,kotlin.Unit>){0§>}[0] diff --git a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TrackpadInjectionScopeSamples.kt b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TrackpadInjectionScopeSamples.kt index 59a7d4e6f92ff..643b6c90d7dab 100644 --- a/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TrackpadInjectionScopeSamples.kt +++ b/compose/ui/ui-test/samples/src/main/java/androidx/compose/ui/test/samples/TrackpadInjectionScopeSamples.kt @@ -22,7 +22,9 @@ import androidx.compose.ui.test.animateMoveAlong import androidx.compose.ui.test.animateMoveTo import androidx.compose.ui.test.click import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.pan import androidx.compose.ui.test.performTrackpadInput +import androidx.compose.ui.test.scale import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin @@ -65,18 +67,16 @@ fun trackpadInputAnimateMoveAlong() { } @Sampled -fun trackpadInputScroll() { +fun trackpadInputPan() { composeTestRule.onNodeWithTag("verticalScrollable").performTrackpadInput { - scroll(Offset(0f, 100f)) + pan(Offset(0f, 100f)) } } @Sampled -fun trackpadInputPinch() { +fun trackpadInputScale() { composeTestRule.onNodeWithTag("transformable").performTrackpadInput { - // Performs a pinch with a factor of 0.9f, which corresponds to a pinch - // with the fingers becoming closer together, which is commonly interpreted - // as a "zoom out" gesture - pinch(0.9f) + // Performs a scale with a factor of 0.9f, which corresponds to a "zoom out" gesture. + scale(0.9f) } } diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanTest.kt similarity index 99% rename from compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt rename to compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanTest.kt index 3e49c356855ff..98ff77ebe5c8d 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScrollTest.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanTest.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.test.injectionscope.trackpad.Common.PrimaryButton import androidx.compose.ui.test.injectionscope.trackpad.Common.verifyTrackpadEvent import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.pan import androidx.compose.ui.test.performTrackpadInput import androidx.compose.ui.test.util.ClickableTestBox import androidx.compose.ui.test.util.SinglePointerInputRecorder @@ -54,10 +55,10 @@ import org.junit.runner.RunWith @MediumTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalTestApi::class) -class ScrollTest { +class PanTest { companion object { private val T = InputDispatcher.eventPeriodMillis - private const val TAG = "SCROLL" + private const val TAG = "PAN" } @get:Rule val rule = createComposeRule(StandardTestDispatcher()) @@ -75,7 +76,7 @@ class ScrollTest { rule.onNodeWithTag(TAG).performTrackpadInput { enter() // scroll vertically - scroll(Offset(0f, 10f)) + pan(Offset(0f, 10f)) } rule.runOnIdle { @@ -215,7 +216,7 @@ class ScrollTest { rule.onNodeWithTag(TAG).performTrackpadInput { enter() // scroll horizontally - scroll(Offset(10f, 0f)) + pan(Offset(10f, 0f)) } rule.runOnIdle { @@ -357,7 +358,7 @@ class ScrollTest { // press primary button press(MouseButton.Primary) // scroll - scroll(Offset(10f, 0f)) + pan(Offset(10f, 0f)) } rule.runOnIdle { diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt new file mode 100644 index 0000000000000..bf50e40baa3a0 --- /dev/null +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2026 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.ui.test.injectionscope.trackpad + +import android.os.Build +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.ui.Alignment +import androidx.compose.ui.ComposeUiFlags +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerInputModifier +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.test.InputDispatcher.Companion.eventPeriodMillis +import androidx.compose.ui.test.TrackpadInjectionScope +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.panWithVelocity +import androidx.compose.ui.test.performTrackpadInput +import androidx.compose.ui.test.util.ClickableTestBox +import androidx.compose.ui.test.util.DataPoint +import androidx.compose.ui.test.util.RecordingFilter +import androidx.compose.ui.unit.Velocity +import androidx.test.filters.MediumTest +import com.google.common.truth.Truth.assertThat +import kotlin.math.max +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test for [TrackpadInjectionScope.panWithVelocity] to see if we can generate gestures that end + * with a specific velocity. + */ +@MediumTest +@RunWith(Parameterized::class) +class PanWithVelocityTest(private val config: TestConfig) { + data class TestConfig(val durationMillis: Long, val velocity: Float) + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun createTestSet(): List { + return mutableListOf().apply { + for (duration in listOf(100, 500, 1000)) { + for (velocity in listOf(100f, 999f, 2000f)) { + add(TestConfig(duration.toLong(), velocity)) + } + } + } + } + + private const val tag = "widget" + + private const val boxSize = 800f + private val boxCenter = Offset(boxSize, boxSize) / 2f + + private val panDelta = Offset(200f, 0f) + } + + @get:Rule val rule = createComposeRule() + + private val recorder = TrackpadPanInputRecorder() + + @Test + fun panWithVelocity() { + rule.setContent { + Box(Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) { + ClickableTestBox(recorder, boxSize, boxSize, tag = tag) + } + } + + rule.onNodeWithTag(tag).performTrackpadInput { + moveTo(center) + panWithVelocity(panDelta, config.velocity, config.durationMillis) + } + + rule.runOnIdle { + recorder.run { + // At least the last 100ms should have velocity + val minimumEventSize = max(2, (100 / eventPeriodMillis).toInt()) + assertThat(events.size).isAtLeast(minimumEventSize) + + assertSinglePointer() + assertThat(events.first().eventType).isEqualTo(PointerEventType.Enter) + + // Check timestamps + assertTimestampsAreIncreasing() + assertThat(recordedDurationMillis).isEqualTo(config.durationMillis) + + val computedVelocity: Velocity + + @OptIn(ExperimentalComposeUiApi::class) + if ( + ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 + ) { + assertThat(events.map { it.position }.toSet()).containsExactly(boxCenter) + assertThat(events[1].eventType).isEqualTo(PointerEventType.PanStart) + assertThat(events.subList(2, events.size - 1).map { it.eventType }.toSet()) + .containsExactly(PointerEventType.PanMove) + assertThat(events.last().eventType).isEqualTo(PointerEventType.PanEnd) + + computedVelocity = -panVelocityTracker.calculateVelocity() + } else { + assertThat(events[1].eventType).isEqualTo(PointerEventType.Press) + assertThat(events.subList(2, events.size - 2).map { it.eventType }.toSet()) + .containsExactly(PointerEventType.Move) + assertThat(events[events.size - 2].eventType) + .isEqualTo(PointerEventType.Release) + assertThat(events.last().eventType).isEqualTo(PointerEventType.Move) + + computedVelocity = fingerVelocityTracker.calculateVelocity() + } + + assertThat(computedVelocity.x).isWithin(0.1f).of(config.velocity) + assertThat(computedVelocity.y).isWithin(0.1f).of(0f) + } + } + } +} + +private class TrackpadPanInputRecorder : PointerInputModifier { + private val _events = mutableListOf() + val events + get() = _events as List + + val fingerVelocityTracker = VelocityTracker() + val panVelocityTracker = VelocityTracker() + private var accumulatedPan = Offset.Zero + + override val pointerInputFilter = RecordingFilter { event -> + event.changes.forEach { + _events.add(DataPoint(it, event)) + if (it.pressed) { + fingerVelocityTracker.addPosition(it.uptimeMillis, it.position) + } else if (event.type == PointerEventType.PanMove) { + accumulatedPan += it.panGestureOffset + panVelocityTracker.addPosition(it.uptimeMillis, accumulatedPan) + } + } + } +} + +private fun TrackpadPanInputRecorder.assertTimestampsAreIncreasing() { + check(events.isNotEmpty()) { "No events recorded" } + events.reduce { prev, curr -> + assertThat(curr.timestamp).isAtLeast(prev.timestamp) + curr + } +} + +private val TrackpadPanInputRecorder.recordedDurationMillis: Long + get() { + check(events.isNotEmpty()) { "No events recorded" } + return events.last().timestamp - events.first().timestamp + } + +private fun TrackpadPanInputRecorder.assertSinglePointer() { + assertThat(events.map { it.id }.distinct()).hasSize(1) +} diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScaleTest.kt similarity index 99% rename from compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt rename to compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScaleTest.kt index ec221ed8eaf70..7d086fab23492 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PinchTest.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/ScaleTest.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.test.injectionscope.trackpad.Common.verifyTrackpadEve import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTrackpadInput +import androidx.compose.ui.test.scale import androidx.compose.ui.test.util.ClickableTestBox import androidx.compose.ui.test.util.MultiPointerInputRecorder import androidx.compose.ui.test.util.assertTimestampsAreIncreasing @@ -52,10 +53,10 @@ import org.junit.runner.RunWith @MediumTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class) -class PinchTest { +class ScaleTest { companion object { private val T = InputDispatcher.eventPeriodMillis - private const val TAG = "PINCH" + private const val TAG = "SCALE" } @get:Rule val rule = createComposeRule(StandardTestDispatcher()) @@ -72,7 +73,7 @@ class PinchTest { rule.onNodeWithTag(TAG).performTrackpadInput { moveTo(center) - pinch(0.9f) + scale(0.9f) } rule.runOnIdle { @@ -293,7 +294,7 @@ class PinchTest { rule.onNodeWithTag(TAG).performTrackpadInput { moveTo(center) - pinch(1.1f) + scale(1.1f) } rule.runOnIdle { diff --git a/compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/inputdispatcher/TrackpadEventsTest.kt b/compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/inputdispatcher/TrackpadEventsTest.kt index 3f42e37faeb3f..7325f279bec65 100644 --- a/compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/inputdispatcher/TrackpadEventsTest.kt +++ b/compose/ui/ui-test/src/androidHostTest/kotlin/androidx/compose/ui/test/inputdispatcher/TrackpadEventsTest.kt @@ -470,25 +470,33 @@ class TrackpadEventsTest : InputDispatcherTest() { } @Test - fun scroll() { + fun pan() { // Scenario: // move trackpad - // scroll vertically by 10f + // pan vertically by 10f // press primary button - // scroll horizontally by 10f + // pan horizontally by 10f var expectedEvents = 0 subject.verifyTrackpadPosition(Offset.Zero) subject.enqueueTrackpadMove(position1) expectedEvents += 2 // enter + hover subject.advanceEventTime() - subject.enqueueTrackpadScroll(Offset(0f, 10f)) + subject.enqueueTrackpadPanStart() + subject.advanceEventTime() + subject.enqueueTrackpadPanMove(Offset(0f, 10f)) + subject.advanceEventTime() + subject.enqueueTrackpadPanEnd() expectedEvents += 5 // exit + down + move + up + enter subject.advanceEventTime() subject.enqueueTrackpadPress(MouseButton.Primary.buttonId) expectedEvents += 3 // exit + down + press subject.advanceEventTime() - subject.enqueueTrackpadScroll(Offset(10f, 0f)) + subject.enqueueTrackpadPanStart() + subject.advanceEventTime() + subject.enqueueTrackpadPanMove(Offset(10f, 0f)) + subject.advanceEventTime() + subject.enqueueTrackpadPanEnd() expectedEvents += 8 // release + up + enter + exit + down + move + up + enter subject.flush() @@ -643,10 +651,18 @@ class TrackpadEventsTest : InputDispatcherTest() { subject.enqueueTrackpadMove(position1) expectedEvents += 2 // enter + hover subject.advanceEventTime() - subject.enqueueTrackpadPinch(0.9f) + subject.enqueueTrackpadScaleStart() + subject.advanceEventTime() + subject.enqueueTrackpadScaleChange(0.9f) + subject.advanceEventTime() + subject.enqueueTrackpadScaleEnd() expectedEvents += 7 // exit + down + pointerDown + move + pointerUp + up + enter subject.advanceEventTime() - subject.enqueueTrackpadPinch(1.1f) + subject.enqueueTrackpadScaleStart() + subject.advanceEventTime() + subject.enqueueTrackpadScaleChange(1.1f) + subject.advanceEventTime() + subject.enqueueTrackpadScaleEnd() expectedEvents += 7 // exit + down + pointerDown + move + pointerUp + up + enter subject.flush() diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt index 38c0530c34841..4a1f96678363e 100644 --- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt +++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt @@ -262,7 +262,7 @@ internal class AndroidInputDispatcher( enqueueTrackpadEvent(ACTION_CANCEL) } - override fun CursorInputState.enqueueTrackpadScroll(offset: Offset) { + override fun CursorInputState.enqueueTrackpadPanStart() { // A two-finger trackpad scroll on Android is represented by a fake single finger, // moving like a single finger would on the touchscreen to generate a scroll. // To accomplish a full scroll for a specific offset this we need to: @@ -291,33 +291,37 @@ internal class AndroidInputDispatcher( action = ACTION_DOWN, coordinate = lastPosition, delta = Offset.Zero, - accumulatedDelta = Offset.Zero, + accumulatedDelta = cursorInputState.panAccumulatedOffset!!, metaState = keyInputState.constructMetaState(), ) - advanceEventTime() + } + + override fun CursorInputState.enqueueTrackpadPanMove(delta: Offset) { enqueueTwoFingerSwipeTrackpadEvent( - downTime = fakeFingerDownTime, + downTime = downTime, eventTime = currentTime, action = ACTION_MOVE, coordinate = lastPosition, - delta = offset, - accumulatedDelta = offset, + delta = delta, + accumulatedDelta = cursorInputState.panAccumulatedOffset!!, metaState = keyInputState.constructMetaState(), ) - advanceEventTime() + } + + override fun CursorInputState.enqueueTrackpadPanEnd() { enqueueTwoFingerSwipeTrackpadEvent( - downTime = fakeFingerDownTime, + downTime = downTime, eventTime = currentTime, action = ACTION_UP, coordinate = lastPosition, delta = Offset.Zero, - accumulatedDelta = offset, + accumulatedDelta = cursorInputState.panAccumulatedOffset!!, metaState = keyInputState.constructMetaState(), ) enqueueTrackpadEnter() } - override fun CursorInputState.enqueueTrackpadPinch(scaleFactor: Float) { + override fun CursorInputState.enqueueTrackpadScaleStart() { // A trackpad pinch on Android is represented by two fake fingers, moving like two fingers // would on a touchscreen to generate a pinch // To accomplish a full pinch for a specific scale factor we need to @@ -360,36 +364,40 @@ internal class AndroidInputDispatcher( accumulatedDelta = 1f, metaState = keyInputState.constructMetaState(), ) - advanceEventTime() + } + + override fun CursorInputState.enqueueTrackpadScaleChange(delta: Float) { enqueuePinchTrackpadEvent( - downTime = fakeFingersDownTime, + downTime = downTime, eventTime = currentTime, action = ACTION_MOVE, actionIndex = 0, coordinate = lastPosition, - delta = scaleFactor, - accumulatedDelta = scaleFactor, + delta = delta, + accumulatedDelta = cursorInputState.scaleAccumulatedFactor!!, metaState = keyInputState.constructMetaState(), ) - advanceEventTime() + } + + override fun CursorInputState.enqueueTrackpadScaleEnd() { enqueuePinchTrackpadEvent( - downTime = fakeFingersDownTime, + downTime = downTime, eventTime = currentTime, action = ACTION_POINTER_UP, actionIndex = 1, coordinate = lastPosition, delta = 1f, - accumulatedDelta = scaleFactor, + accumulatedDelta = cursorInputState.scaleAccumulatedFactor!!, metaState = keyInputState.constructMetaState(), ) enqueuePinchTrackpadEvent( - downTime = fakeFingersDownTime, + downTime = downTime, eventTime = currentTime, action = ACTION_UP, actionIndex = 0, coordinate = lastPosition, delta = 1f, - accumulatedDelta = scaleFactor, + cursorInputState.scaleAccumulatedFactor!!, metaState = keyInputState.constructMetaState(), ) enqueueTrackpadEnter() diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt index c14d6c868d6a5..59ef906e15d2a 100644 --- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt +++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt @@ -789,24 +789,79 @@ internal abstract class InputDispatcher( cursor.currentCursorInputSource = null } - fun enqueueTrackpadScroll(offset: Offset) { + fun enqueueTrackpadPanStart() { val cursor = cursorInputState cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(!cursor.isInPanGesture) { + "Cannot send trackpad pan start event, a pan gesture is already in progress" + } + cursor.panAccumulatedOffset = Offset.Zero if (isWithinRootBounds(currentCursorPosition)) { - cursor.enqueueTrackpadScroll(offset) + cursor.enqueueTrackpadPanStart() } } - fun enqueueTrackpadPinch(scaleFactor: Float) { + fun enqueueTrackpadPanMove(delta: Offset) { val cursor = cursorInputState cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(cursor.isInPanGesture) { + "Cannot send trackpad pan move event, no pan gesture is in progress" + } + cursor.panAccumulatedOffset = cursor.panAccumulatedOffset!! + delta + if (isWithinRootBounds(currentCursorPosition)) { + cursor.enqueueTrackpadPanMove(delta) + } + } + + fun enqueueTrackpadPanEnd() { + val cursor = cursorInputState + cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(cursor.isInPanGesture) { + "Cannot send trackpad pan end event, no pan gesture is in progress" + } + if (isWithinRootBounds(currentCursorPosition)) { + cursor.enqueueTrackpadPanEnd() + } + cursor.panAccumulatedOffset = null + } + fun enqueueTrackpadScaleStart() { + val cursor = cursorInputState + cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(!cursor.isInScaleGesture) { + "Cannot send trackpad scale start event, a scale gesture is already in progress" + } + cursor.scaleAccumulatedFactor = 1f if (isWithinRootBounds(currentCursorPosition)) { - cursor.enqueueTrackpadPinch(scaleFactor) + cursor.enqueueTrackpadScaleStart() } } + fun enqueueTrackpadScaleChange(scaleFactor: Float) { + val cursor = cursorInputState + cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(cursor.isInScaleGesture) { + "Cannot send trackpad scale change event, no pan gesture is in progress" + } + cursor.scaleAccumulatedFactor = cursor.scaleAccumulatedFactor!! * scaleFactor + if (isWithinRootBounds(currentCursorPosition)) { + cursor.enqueueTrackpadScaleChange(scaleFactor) + } + } + + fun enqueueTrackpadScaleEnd() { + val cursor = cursorInputState + cursor.currentCursorInputSource = CursorInputSource.Trackpad + check(cursor.isInScaleGesture) { + "Cannot send trackpad scale end event, no scale gesture is in progress" + } + if (isWithinRootBounds(currentCursorPosition)) { + cursor.enqueueTrackpadScaleEnd() + } + cursor.scaleAccumulatedFactor = null + } + /** * Generates a key down event for the given [key]. * @@ -1006,9 +1061,17 @@ internal abstract class InputDispatcher( protected abstract fun CursorInputState.enqueueMouseScroll(offset: Offset) - protected abstract fun CursorInputState.enqueueTrackpadScroll(offset: Offset) + protected abstract fun CursorInputState.enqueueTrackpadPanStart() + + protected abstract fun CursorInputState.enqueueTrackpadPanMove(delta: Offset) + + protected abstract fun CursorInputState.enqueueTrackpadPanEnd() - protected abstract fun CursorInputState.enqueueTrackpadPinch(scaleFactor: Float) + protected abstract fun CursorInputState.enqueueTrackpadScaleStart() + + protected abstract fun CursorInputState.enqueueTrackpadScaleChange(delta: Float) + + protected abstract fun CursorInputState.enqueueTrackpadScaleEnd() protected abstract fun RotaryInputState.enqueueRotaryScrollHorizontally( horizontalScrollPixels: Float @@ -1059,6 +1122,8 @@ internal class CursorInputState { var lastPosition: Offset = Offset.Zero var isEntered: Boolean = false var currentCursorInputSource: CursorInputSource? = null + var panAccumulatedOffset: Offset? = null + var scaleAccumulatedFactor: Float? = null val hasAnyButtonPressed get() = pressedButtons.isNotEmpty() @@ -1073,6 +1138,12 @@ internal class CursorInputState { return pressedButtons.contains(buttonId) } + val isInPanGesture + get() = panAccumulatedOffset != null + + val isInScaleGesture + get() = scaleAccumulatedFactor != null + fun setButtonBit(buttonId: Int) { pressedButtons.add(buttonId) } diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TrackpadInjectionScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TrackpadInjectionScope.kt index 9d8cd97c31337..52566d7bf352b 100644 --- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TrackpadInjectionScope.kt +++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TrackpadInjectionScope.kt @@ -16,20 +16,23 @@ package androidx.compose.ui.test +import androidx.annotation.FloatRange import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.lerp import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.util.lerp +import kotlin.math.ceil import kotlin.math.max import kotlin.math.roundToInt +import kotlin.math.roundToLong /** * The receiver scope of the trackpad input injection lambda from [performTrackpadInput]. * * The functions in [TrackpadInjectionScope] can roughly be divided into two groups: full gestures * and individual trackpad events. The individual trackpad events are: [press], [moveTo] and - * friends, [release], [cancel], [scroll] and [advanceEventTime]. Full gestures are all the other + * friends, [release], [cancel], [pan] and [advanceEventTime]. Full gestures are all the other * functions, like [TrackpadInjectionScope.click], [TrackpadInjectionScope.doubleClick], * [TrackpadInjectionScope.animateMoveTo], etc. These are built on top of the individual events and * serve as a good example on how you can build your own full gesture functions. @@ -41,7 +44,7 @@ import kotlin.math.roundToInt * event. Use [press] and [release] to send button pressed and button released events. This will * also send all other necessary events that keep the stream of trackpad events consistent with * actual trackpad input, such as a hover exit event. A [cancel] event can be sent at any time when - * at least one button is pressed. Use [scroll] to send a trackpad scroll event. + * at least one button is pressed. Use [pan] to send a trackpad pan event. * * The entire event injection state is shared between all `perform.*Input` methods, meaning you can * continue an unfinished trackpad gesture in a subsequent invocation of [performTrackpadInput] or @@ -75,7 +78,7 @@ interface TrackpadInjectionScope : InjectionScope { * position of the trackpad updated to [position]. The [position] is in the node's local * coordinate system, where (0, 0) is the top left corner of the node. * - * If no mouse buttons are pressed, a hover event will be sent instead of a move event. If the + * If no buttons are pressed, a hover event will be sent instead of a move event. If the * trackpad wasn't hovering yet, a hover enter event is sent as well. * * @param position The new position of the trackpad, in the node's local coordinate system @@ -88,7 +91,7 @@ interface TrackpadInjectionScope : InjectionScope { * Sends a move event [delayMillis] after the last sent event on the associated node, with the * position of the trackpad moved by the given [delta]. * - * If no mouse buttons are pressed, a hover event will be sent instead of a move event. If the + * If no buttons are pressed, a hover event will be sent instead of a move event. If the * trackpad wasn't hovering yet, a hover enter event is sent as well. * * @param delta The position for this move event, relative to the current position of the @@ -128,12 +131,10 @@ interface TrackpadInjectionScope : InjectionScope { * Sends a down and button pressed event for the given [button] on the associated node. When no * buttons were down yet, this will exit hovering mode before the button is pressed. All events * will be sent at the current event time. Trackpads behave similarly to mice, with platform - * interpreted gestures that send mouse button events, so this API takes the [MouseButton] to - * press. + * interpreted gestures that send button events. * - * Throws an [IllegalStateException] if the [button] is already pressed. - * - * @param button The mouse button that is pressed. By default the primary mouse button. + * @param button The button that is pressed. By default the primary button. + * @throws [IllegalStateException] if the [button] is already pressed. */ fun press(button: MouseButton = MouseButton.Primary) @@ -142,18 +143,17 @@ interface TrackpadInjectionScope : InjectionScope { * was the last button to be released, the trackpad will enter hovering mode and send an * accompanying trackpad move event after the button has been released. All events will be sent * at the current event time. Trackpads behave similarly to mice, with platform interpreted - * gestures that send mouse button events, so this API takes the [MouseButton] to release. - * - * Throws an [IllegalStateException] if the [button] is not pressed. + * gestures that send button events. * - * @param button The mouse button that is released. By default the primary mouse button. + * @param button The button that is released. By default the primary button. + * @throws [IllegalStateException] if the [button] is not pressed. */ fun release(button: MouseButton = MouseButton.Primary) /** * Sends a cancel event [delayMillis] after the last sent event to cancel a stream of trackpad - * events with pressed mouse buttons. All buttons will be released as a result. A trackpad - * cancel event can only be sent when mouse buttons are pressed. + * events with pressed buttons. All buttons will be released as a result. A trackpad cancel + * event can only be sent when buttons are pressed. * * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis] * by default. @@ -164,9 +164,6 @@ interface TrackpadInjectionScope : InjectionScope { * Sends a hover enter event at the given [position], [delayMillis] after the last sent event, * without sending a hover move event. * - * An [IllegalStateException] will be thrown when mouse buttons are down, or if the trackpad is - * already hovering. - * * The [position] is in the node's local coordinate system, where (0, 0) is the top left corner * of the node. * @@ -180,6 +177,7 @@ interface TrackpadInjectionScope : InjectionScope { * [currentPosition] by default. * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis] * by default. + * @throws [IllegalStateException] if buttons are down, or if the trackpad is already hovering. */ fun enter(position: Offset = currentPosition, delayMillis: Long = eventPeriodMillis) @@ -187,8 +185,6 @@ interface TrackpadInjectionScope : InjectionScope { * Sends a hover exit event at the given [position], [delayMillis] after the last sent event, * without sending a hover move event. * - * An [IllegalStateException] will be thrown if the trackpad was not hovering. - * * The [position] is in the node's local coordinate system, where (0, 0) is the top left corner * of the node. * @@ -202,32 +198,80 @@ interface TrackpadInjectionScope : InjectionScope { * [currentPosition] by default. * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis] * by default. + * @throws [IllegalStateException] if the trackpad was not hovering. */ fun exit(position: Offset = currentPosition, delayMillis: Long = eventPeriodMillis) /** - * Sends a scroll event with the given [offset]. The event will be sent at the current event - * time. + * Starts a pan gesture. The [PointerEventType.PanStart] will be sent at the current event time. + * This should be followed by any number of calls to [panMoveBy], followed by [panEnd]. + * + * The helper function [pan] allows combining these calls into a single call, to pan by a given + * offset sending the appropriate event in sequence. + * + * @throws [IllegalStateException] if the trackpad was already sending a pan gesture. + */ + fun panStart() + + /** + * Updates the ongoing pan gesture, by applying the given [delta] as part of the pan. The + * [PointerEventType.PanMove] will be sent at the current event time. + * + * The helper function [pan] allows combining these calls into a single call, to pan by a given + * offset sending the appropriate event in sequence. + * + * @param delta the incremental change in the pan offset. + * @throws [IllegalStateException] if the trackpad is not in a pan gesture started by + * [panStart]. + */ + fun panMoveBy(delta: Offset) + + /** + * Ends a pan gesture. The [PointerEventType.PanEnd] will be sent at the current event time. + * + * The helper function [pan] allows combining these calls into a single call, to pan by a given + * offset sending the appropriate event in sequence. + * + * @throws [IllegalStateException] if the trackpad is not in a pan gesture started by + * [panStart]. + */ + fun panEnd() + + /** + * Starts a scale gesture. The [PointerEventType.ScaleStart] will be sent at the current event + * time. This should be followed by any number of calls to [scaleChangeBy], followed by + * [scaleEnd]. + * + * The helper function [scale] allows combining these calls into a single call, to scale by a + * given factor sending the appropriate events in sequence. * - * @sample androidx.compose.ui.test.samples.trackpadInputScroll - * @param offset The amount of scroll + * @throws [IllegalStateException] if the trackpad was already sending a scale gesture. */ - fun scroll(offset: Offset) + fun scaleStart() /** - * Sends a pinch event with the given [scaleFactor]. The event will be sent at the current event - * time. + * Updates the ongoing scale gesture, by applying the given multiplicative [scaleFactor] as part + * of the gesture. The [PointerEventType.ScaleChange] will be sent at the current event time. * - * The [scaleFactor] is a multiplicative zoom factor. A [scaleFactor] of 1 represents no pinch - * movement. A [scaleFactor] less than 1 represents a pinch where the 2 fingers become closer - * together (often interpreted as a "zoom out" gesture), and a [scaleFactor] of more than 1 - * represents a pinch where the 2 fingers become farther apart (often interpreted as a "zoom in" - * gesture). + * The helper function [scale] allows combining these calls into a single call, to scale by a + * given factor sending the appropriate events in sequence. * - * @sample androidx.compose.ui.test.samples.trackpadInputPinch - * @param scaleFactor The amount of pinch. + * @param scaleFactor the incremental multiplicative change in the scale factor. + * @throws [IllegalStateException] if the trackpad is not in a scale gesture started by + * [scaleStart]. */ - fun pinch(scaleFactor: Float) + fun scaleChangeBy(@FloatRange(from = 0.0, fromInclusive = false) scaleFactor: Float) + + /** + * Ends a scale gesture. The [PointerEventType.ScaleEnd] will be sent at the current event time. + * + * The helper function [scale] allows combining these calls into a single call, to scale by a + * given factor sending the appropriate events in sequence. + * + * @throws [IllegalStateException] if the trackpad is not in a scale gesture started by + * [scaleStart]. + */ + fun scaleEnd() } internal class TrackpadInjectionScopeImpl(private val baseScope: MultiModalInjectionScopeImpl) : @@ -276,12 +320,28 @@ internal class TrackpadInjectionScopeImpl(private val baseScope: MultiModalInjec inputDispatcher.enqueueTrackpadCancel() } - override fun scroll(offset: Offset) { - inputDispatcher.enqueueTrackpadScroll(offset) + override fun panStart() { + inputDispatcher.enqueueTrackpadPanStart() + } + + override fun panMoveBy(delta: Offset) { + inputDispatcher.enqueueTrackpadPanMove(delta) } - override fun pinch(scaleFactor: Float) { - inputDispatcher.enqueueTrackpadPinch(scaleFactor) + override fun panEnd() { + inputDispatcher.enqueueTrackpadPanEnd() + } + + override fun scaleStart() { + inputDispatcher.enqueueTrackpadScaleStart() + } + + override fun scaleChangeBy(scaleFactor: Float) { + inputDispatcher.enqueueTrackpadScaleChange(scaleFactor) + } + + override fun scaleEnd() { + inputDispatcher.enqueueTrackpadScaleEnd() } } @@ -309,10 +369,10 @@ fun TrackpadInjectionScope.click( /** * Secondary-click on [position], or on the current cursor position if [position] is - * [unspecified][Offset.Unspecified]. While the secondary mouse button is not necessarily the right - * mouse button (e.g. on left-handed mice), this method is still called `rightClick` for it's - * widespread use. The [position] is in the node's local coordinate system, where (0, 0) is the top - * left corner of the node. + * [unspecified][Offset.Unspecified]. While the secondary button is not necessarily a physical right + * button (e.g. a multi-finger tap), this method is still called `rightClick` for it's widespread + * use. The [position] is in the node's local coordinate system, where (0, 0) is the top left corner + * of the node. * * @param position The position where to click, in the node's local coordinate system. If omitted, * the [center] of the node will be used. If [unspecified][Offset.Unspecified], clicks on the @@ -478,10 +538,10 @@ fun TrackpadInjectionScope.animateMoveAlong( * before starting the gesture. The positions defined by the [start] and [end] are in the node's * local coordinate system, where (0, 0) is the top left corner of the node. * - * @param start The position where to press the primary mouse button and initiate the drag, in the - * node's local coordinate system. - * @param end The position where to release the primary mouse button and end the drag, in the node's + * @param start The position where to press the primary button and initiate the drag, in the node's * local coordinate system. + * @param end The position where to release the primary button and end the drag, in the node's local + * coordinate system. * @param button The button to drag with. Uses the [primary][MouseButton.Primary] by default. * @param durationMillis The duration of the gesture. By default 300 milliseconds. */ @@ -497,5 +557,159 @@ fun TrackpadInjectionScope.dragAndDrop( release(button) } +/** + * Sends a pan gesture with the given total [offset]. The event will be sent starting at the current + * event time. + * + * To send a pan gesture with a curve, use `pan(curve, durationMillis, keyTimes)`. + * + * @sample androidx.compose.ui.test.samples.trackpadInputPan + * @param offset The amount of pan + */ +fun TrackpadInjectionScope.pan(offset: Offset) { + panStart() + advanceEventTime() + panMoveBy(offset) + advanceEventTime() + panEnd() +} + +/** + * Sends a pan gesture with the offsets in the panning coordinate space following the given [curve]. + * It is expected that the curve starts at [Offset.Zero], as a pan gesture starts with no delta, and + * then move outward from the origin. + * + * To send a pan with just a single amount, use `pan(offset)`. + * + * @param curve The function that describes the gesture. The argument passed to the function is the + * time in milliseconds since the start of the swipe, and the return value is the location of the + * pan from the origin at that point in time. + * @param durationMillis The duration of the gesture + * @param keyTimes An optional list of timestamps in milliseconds at which a pan move event must be + * sampled + */ +@Suppress("PrimitiveInCollection") +fun TrackpadInjectionScope.pan( + curve: (timeMillis: Long) -> Offset, + durationMillis: Long = 200, + keyTimes: List = emptyList(), +) { + val startTime = 0L + val endTime = durationMillis + + // Validate input + require(durationMillis >= 1) { "duration must be at least 1 millisecond, not $durationMillis" } + val validRange = startTime..endTime + require(keyTimes.all { it in validRange }) { + "keyTimes contains timestamps out of range [$startTime..$endTime]: $keyTimes" + } + require(keyTimes.asSequence().zipWithNext { a, b -> a <= b }.all { it }) { + "keyTimes must be sorted: $keyTimes" + } + + panStart() + + var accmulatedDelta: Offset = Offset.Zero + + // Send move events between each consecutive pair in [t0, ..keyTimes, tN] + var currTime = startTime + var key = 0 + while (currTime < endTime) { + // advance key + while (key < keyTimes.size && keyTimes[key] <= currTime) { + key++ + } + // send events between t and next keyTime + val tNext = if (key < keyTimes.size) keyTimes[key] else endTime + + val steps = max(1, ((tNext - currTime) / eventPeriodMillis.toFloat()).roundToInt()) + var step = 0 + + var tPrev = currTime + while (step++ < steps) { + val progress = step / steps.toFloat() + val t = lerp(currTime, tNext, progress) + val value = curve(t) + + val delta = value - accmulatedDelta + accmulatedDelta = value + advanceEventTime(t - tPrev) + panMoveBy(delta) + tPrev = t + } + currTime = tNext + } + + panEnd() +} + +/** + * Performs a pan gesture on the associated node such that it ends with the given [endVelocity]. + * + * The pan will go from [Offset.Zero] at t=0 to [offset] at t=[durationMillis]. In between, the pan + * will go monotonically from [Offset.Zero] and [offset], but not strictly. Due to imprecision, no + * guarantees can be made for the actual velocity at the end of the gesture, but generally it is + * within 0.1 of the desired velocity. + * + * When a pan cannot be created that results in the desired velocity (because the input is too + * restrictive), an exception will be thrown with suggestions to fix the input. + * + * The coordinates are in the pan coordinate system, which has the same scale as the display in + * pixel coordinates, where [Offset.Zero] indicates no panning. + * + * Use `pan(curve, duration, durationMillis)` to directly control the curve of the pan. + * + * @param offset The end position of the pan + * @param endVelocity The velocity of the gesture at the moment it ends in px/second. Must be + * positive. + * @param durationMillis The duration of the gesture in milliseconds. Must be long enough that at + * least 3 input events are generated, which happens with a duration of 40ms or more. If omitted, + * a duration is calculated such that a valid pan with velocity can be created. + * @throws IllegalArgumentException When no pan can be generated that will result in the desired + * velocity. The error message will suggest changes to the input parameters such that a pan will + * become feasible. + */ +fun TrackpadInjectionScope.panWithVelocity( + offset: Offset, + @FloatRange(from = 0.0) endVelocity: Float, + durationMillis: Long = + VelocityPathFinder.calculateDefaultDuration(Offset.Zero, offset, endVelocity), +) { + require(endVelocity >= 0f) { "Velocity cannot be $endVelocity, it must be positive" } + require(eventPeriodMillis < 40) { + "InputDispatcher.eventPeriod must be smaller than 40ms in order to generate velocities" + } + val minimumDuration = ceil(2.5f * eventPeriodMillis).roundToLong() + require(durationMillis >= minimumDuration) { + "Duration must be at least ${minimumDuration}ms because " + + "velocity requires at least 3 input events" + } + + val pathFinder = VelocityPathFinder(Offset.Zero, offset, endVelocity, durationMillis) + val swipeFunction: (Long) -> Offset = { pathFinder.calculateOffsetForTime(it) } + pan(swipeFunction, durationMillis) +} + +/** + * Sends a scale event with the given [scaleFactor]. The event will be sent starting at the current + * event time. + * + * The [scaleFactor] is a multiplicative zoom factor. A [scaleFactor] of 1 represents no change. A + * [scaleFactor] less than 1 represents a "zoom out" gesture, while a factor of more than one + * represents a "zoom in" gesture. + * + * @sample androidx.compose.ui.test.samples.trackpadInputScale + * @param scaleFactor The amount to scale. + */ +fun TrackpadInjectionScope.scale( + @FloatRange(from = 0.0, fromInclusive = false) scaleFactor: Float +) { + scaleStart() + advanceEventTime() + scaleChangeBy(scaleFactor) + advanceEventTime() + scaleEnd() +} + /** The default duration of trackpad gestures with configurable time (e.g. [animateMoveTo]). */ private const val DefaultTrackpadGestureDurationMillis: Long = 300L From 3e44e79ed451b1ff4248b7d698dab20a015274bc Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 29 Jan 2026 15:34:05 -0800 Subject: [PATCH 07/16] Trackpad fling with velocity This splits the gesture handling for trackpads out of MouseWheelScrollingLogic into a similar TrackpadScrollingLogic. This uses the new PointerEventTypes indicating the starting and finishing of pan gestures, which allows calculating velocity and more cleanly starting flings when the pan gesture finishes. Relnote: "Adds fling support to trackpad scrolling gestures" Fixes: 479285849 Test: New test in ScrollableTest Change-Id: I55f8a17d6c2578003c20f263ae459acfc155406a --- .../compose/foundation/ScrollableTest.kt | 36 ++++ .../gestures/AndroidScrollable.android.kt | 17 +- .../gestures/DifferentialVelocityTracker.kt | 37 ++++ ...ollable.kt => MouseWheelScrollingLogic.kt} | 94 +-------- .../gestures/NonTouchScrollingLogic.kt | 91 ++++++++ .../compose/foundation/gestures/Scrollable.kt | 40 +++- .../gestures/TrackpadScrollingLogic.kt | 199 ++++++++++++++++++ .../foundation/gestures/Transformable.kt | 78 ++++--- 8 files changed, 461 insertions(+), 131 deletions(-) create mode 100644 compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DifferentialVelocityTracker.kt rename compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/{MouseWheelScrollable.kt => MouseWheelScrollingLogic.kt} (80%) create mode 100644 compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/NonTouchScrollingLogic.kt create mode 100644 compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt diff --git a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt index 095b3c3a3c10c..2d8391f097287 100644 --- a/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt +++ b/compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/ScrollableTest.kt @@ -113,6 +113,7 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.pan +import androidx.compose.ui.test.panWithVelocity import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performSemanticsAction @@ -2943,6 +2944,41 @@ class ScrollableTest { assertThat(flingVelocity).isWithin(5f).of(1000f) } + @Test + fun scrollable_flingBehaviourCalled_withTrackpad() { + var total = 0f + val scrollableState = + ScrollableState( + consumeScrollDelta = { + total += it + it + } + ) + var flingCalled = 0 + var flingVelocity: Float = Float.MAX_VALUE + val flingBehaviour = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + flingCalled++ + flingVelocity = initialVelocity + return 0f + } + } + setScrollableContent { + Modifier.scrollable( + state = scrollableState, + flingBehavior = flingBehaviour, + orientation = Orientation.Horizontal, + ) + } + rule.onNodeWithTag(scrollableBoxTag).performTrackpadInput { + moveTo(center) + panWithVelocity(offset = Offset(115f, 0f), endVelocity = 500f) + } + assertThat(flingCalled).isEqualTo(1) + assertThat(flingVelocity).isWithin(10f).of(500f) + } + @Test fun scrollable_flingBehaviourCalled_indirectPointer() { var total = 0f diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt index 434bc3ff3b6b8..51cf172785e04 100644 --- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt +++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt @@ -19,8 +19,6 @@ package androidx.compose.foundation.gestures import android.os.Build import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.ComposeFoundationFlags -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.node.CompositionLocalConsumerModifierNode @@ -64,20 +62,7 @@ internal class AndroidConfig(val viewConfiguration: android.view.ViewConfigurati .fastFold(Offset.Zero) { acc, c -> acc + c.scrollDelta } .let { Offset(it.x * horizontalScrollFactor, it.y * verticalScrollFactor) } - @OptIn(ExperimentalFoundationApi::class) - val accumulatedGesturePanOffset = - if (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) { - event.changes.firstOrNull()?.let { - it.panGestureOffset + - it.historical.fastFold(Offset.Zero) { acc, historicalChange -> - acc + historicalChange.panGestureOffset - } - } ?: Offset.Zero - } else { - Offset.Zero - } - - return accumulatedScrollDelta - accumulatedGesturePanOffset + return accumulatedScrollDelta } } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DifferentialVelocityTracker.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DifferentialVelocityTracker.kt new file mode 100644 index 0000000000000..e4591451be3df --- /dev/null +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DifferentialVelocityTracker.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 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.foundation.gestures + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.util.VelocityTracker1D +import androidx.compose.ui.unit.Velocity + +internal class DifferentialVelocityTracker { + private val xVelocityTracker = VelocityTracker1D(isDataDifferential = true) + private val yVelocityTracker = VelocityTracker1D(isDataDifferential = true) + + fun addDelta(timeMillis: Long, delta: Offset) { + xVelocityTracker.addDataPoint(timeMillis, delta.x) + yVelocityTracker.addDataPoint(timeMillis, delta.y) + } + + fun calculateVelocity(): Velocity { + val velocityX = xVelocityTracker.calculateVelocity(Float.MAX_VALUE) + val velocityY = yVelocityTracker.calculateVelocity(Float.MAX_VALUE) + return Velocity(velocityX, velocityY) + } +} diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollingLogic.kt similarity index 80% rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollingLogic.kt index ebd8496382fce..2aa1575d594fe 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollingLogic.kt @@ -22,54 +22,38 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateTo import androidx.compose.animation.core.copy import androidx.compose.animation.core.tween -import androidx.compose.foundation.ComposeFoundationFlags import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.MutatePriority -import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.util.VelocityTracker1D import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastForEach import kotlin.math.abs import kotlin.math.roundToInt import kotlin.math.sign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withTimeoutOrNull internal class MouseWheelScrollingLogic( - private val scrollingLogic: ScrollingLogic, + scrollingLogic: ScrollingLogic, private val mouseWheelScrollConfig: ScrollConfig, - private val onScrollStopped: suspend (velocity: Velocity) -> Unit, - private var density: Density, -) { - fun updateDensity(density: Density) { - this.density = density - } - - fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize) { - @OptIn(ExperimentalFoundationApi::class) - if ( - pointerEvent.type != PointerEventType.Scroll && - (!ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || - (pointerEvent.type != PointerEventType.PanStart && - pointerEvent.type != PointerEventType.PanMove && - pointerEvent.type != PointerEventType.PanEnd)) - ) - return + onScrollStopped: suspend (velocity: Velocity) -> Unit, + density: Density, +) : NonTouchScrollingLogic(scrollingLogic, onScrollStopped, density) { + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize, + ) { + if (pointerEvent.type != PointerEventType.Scroll) return if (pointerEvent.isConsumed) return /** * If this scrollable is already scrolling from a previous interaction, consume immediately @@ -93,11 +77,6 @@ internal class MouseWheelScrollingLogic( } } - private inline val PointerEvent.isConsumed: Boolean - get() = changes.fastAny { it.isConsumed } - - private fun PointerEvent.consume() = changes.fastForEach { it.consume() } - private data class MouseWheelScrollDelta( val value: Offset, val timeMillis: Long, @@ -118,11 +97,10 @@ internal class MouseWheelScrollingLogic( } private val channel = Channel(capacity = Channel.UNLIMITED) - private var isScrolling = false private var receivingMouseWheelEventsJob: Job? = null - fun startReceivingMouseWheelEvents(coroutineScope: CoroutineScope) { + override fun startReceivingEvents(coroutineScope: CoroutineScope) { if (receivingMouseWheelEventsJob == null) { receivingMouseWheelEventsJob = coroutineScope.launch { @@ -140,13 +118,6 @@ internal class MouseWheelScrollingLogic( } } - private suspend fun ScrollingLogic.userScroll(block: suspend NestedScrollScope.() -> Unit) { - isScrolling = true - // Run it in supervisorScope to ignore cancellations from scrolls with higher MutatePriority - supervisorScope { scroll(MutatePriority.UserInput, block) } - isScrolling = false - } - private fun onMouseWheel(pointerEvent: PointerEvent, bounds: IntSize): Boolean { val scrollDelta = with(mouseWheelScrollConfig) { @@ -179,31 +150,6 @@ internal class MouseWheelScrollingLogic( return sum } - /** - * Replacement of regular [Channel.receive] that schedules an invalidation each frame. It avoids - * entering an idle state while waiting for [ScrollProgressTimeout]. It's important for tests - * that attempt to trigger another scroll after a mouse wheel event. - */ - private suspend fun Channel.busyReceive() = coroutineScope { - val job = launch { - while (coroutineContext.isActive) { - withFrameNanos {} - } - } - try { - receive() - } finally { - job.cancel() - } - } - - private fun untilNull(builderAction: () -> E?) = - sequence { - do { - val element = builderAction()?.also { yield(it) } - } while (element != null) - } - @OptIn(ExperimentalFoundationApi::class) private fun ScrollingLogic.canConsumeDelta(scrollDelta: Offset): Boolean { /** @@ -220,8 +166,6 @@ internal class MouseWheelScrollingLogic( } } - private val velocityTracker = MouseWheelVelocityTracker() - private fun trackVelocity(scrollDelta: MouseWheelScrollDelta) { velocityTracker.addDelta(scrollDelta.timeMillis, scrollDelta.value) } @@ -364,22 +308,6 @@ internal class MouseWheelScrollingLogic( } } -private class MouseWheelVelocityTracker { - private val xVelocityTracker = VelocityTracker1D(isDataDifferential = true) - private val yVelocityTracker = VelocityTracker1D(isDataDifferential = true) - - fun addDelta(timeMillis: Long, delta: Offset) { - xVelocityTracker.addDataPoint(timeMillis, delta.x) - yVelocityTracker.addDataPoint(timeMillis, delta.y) - } - - fun calculateVelocity(): Velocity { - val velocityX = xVelocityTracker.calculateVelocity(Float.MAX_VALUE) - val velocityY = yVelocityTracker.calculateVelocity(Float.MAX_VALUE) - return Velocity(velocityX, velocityY) - } -} - /* * Returns true, if the value is too low for visible change in scroll (consumed delta, animation-based change, etc), * false otherwise diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/NonTouchScrollingLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/NonTouchScrollingLogic.kt new file mode 100644 index 0000000000000..f9ee645eff83e --- /dev/null +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/NonTouchScrollingLogic.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2026 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.foundation.gestures + +import androidx.compose.foundation.MutatePriority +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope + +/** A shared base class for [TrackpadScrollingLogic] and [MouseWheelScrollingLogic]. */ +internal abstract class NonTouchScrollingLogic( + protected val scrollingLogic: ScrollingLogic, + protected val onScrollStopped: suspend (velocity: Velocity) -> Unit, + protected var density: Density, +) { + fun updateDensity(density: Density) { + this.density = density + } + + internal inline val PointerEvent.isConsumed: Boolean + get() = changes.fastAny { it.isConsumed } + + internal fun PointerEvent.consume() = changes.fastForEach { it.consume() } + + internal var isScrolling = false + + internal suspend fun userScroll(block: suspend NestedScrollScope.() -> Unit) { + isScrolling = true + // Run it in supervisorScope to ignore cancellations from scrolls with higher MutatePriority + supervisorScope { scrollingLogic.scroll(MutatePriority.UserInput, block) } + isScrolling = false + } + + internal val velocityTracker = DifferentialVelocityTracker() + + /** Forwards the given [pointerEvent] for processing by this scroll logic. */ + abstract fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize) + + /** Begins processing of events sent to [onPointerEvent] using the given [coroutineScope]. */ + abstract fun startReceivingEvents(coroutineScope: CoroutineScope) +} + +/** + * Replacement of regular [Channel.receive] that schedules an invalidation each frame. It avoids + * entering an idle state while waiting for [ScrollProgressTimeout]. It's important for tests that + * attempt to trigger another scroll after a mouse wheel event. + */ +internal suspend fun Channel.busyReceive(): T = coroutineScope { + val job = launch { + while (coroutineContext.isActive) { + withFrameNanos {} + } + } + try { + receive() + } finally { + job.cancel() + } +} + +internal fun untilNull(builderAction: () -> E?) = + sequence { + do { + val element = builderAction()?.also { yield(it) } + } while (element != null) + } diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt index 2259512903ba2..0255fe2f231a0 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt @@ -341,6 +341,7 @@ internal class ScrollableNode( private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null private var mouseWheelScrollingLogic: MouseWheelScrollingLogic? = null + private var trackpadScrollingLogic: TrackpadScrollingLogic? = null private var scrollableContainerNode: ScrollableContainerNode? = null @@ -403,11 +404,17 @@ internal class ScrollableNode( } } + private fun onTrackpadScrollStopped(velocity: Velocity) { + nestedScrollDispatcher.coroutineScope.launch { + scrollingLogic.onScrollStopped(velocity, isMouseWheel = false) + } + } + override fun startDragImmediately(): Boolean { return scrollingLogic.shouldScrollImmediately() } - private fun ensureMouseWheelScrollNodeInitialized() { + private fun ensureMouseWheelScrollingLogicInitialized() { if (mouseWheelScrollingLogic == null) { mouseWheelScrollingLogic = MouseWheelScrollingLogic( @@ -418,7 +425,20 @@ internal class ScrollableNode( ) } - mouseWheelScrollingLogic?.startReceivingMouseWheelEvents(coroutineScope) + mouseWheelScrollingLogic?.startReceivingEvents(coroutineScope) + } + + private fun ensureTrackpadScrollingLogicInitialized() { + if (trackpadScrollingLogic == null) { + trackpadScrollingLogic = + TrackpadScrollingLogic( + scrollingLogic = scrollingLogic, + onScrollStopped = ::onTrackpadScrollStopped, + density = requireDensity(), + ) + } + + trackpadScrollingLogic?.startReceivingEvents(coroutineScope) } fun update( @@ -472,6 +492,7 @@ internal class ScrollableNode( override fun onAttach() { updateDefaultFlingBehavior() mouseWheelScrollingLogic?.updateDensity(requireDensity()) + trackpadScrollingLogic?.updateDensity(requireDensity()) } private fun updateDefaultFlingBehavior() { @@ -484,6 +505,7 @@ internal class ScrollableNode( onCancelPointerInput() updateDefaultFlingBehavior() mouseWheelScrollingLogic?.updateDensity(requireDensity()) + trackpadScrollingLogic?.updateDensity(requireDensity()) } // Key handler for Page up/down scrolling behavior. @@ -552,16 +574,20 @@ internal class ScrollableNode( } initializeGestureCoordination() if (enabled) { + if (pass == PointerEventPass.Initial && pointerEvent.type == PointerEventType.Scroll) { + ensureMouseWheelScrollingLogicInitialized() + } + mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) + if ( pass == PointerEventPass.Initial && - (pointerEvent.type == PointerEventType.Scroll || - pointerEvent.type == PointerEventType.PanStart || + (pointerEvent.type == PointerEventType.PanStart || pointerEvent.type == PointerEventType.PanMove || pointerEvent.type == PointerEventType.PanEnd) ) { - ensureMouseWheelScrollNodeInitialized() + ensureTrackpadScrollingLogicInitialized() } - mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) + trackpadScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) } } @@ -600,7 +626,7 @@ object ScrollableDefaults { * Returns a remembered [OverscrollEffect] created from the current value of * [LocalOverscrollFactory]. * - * This API has been deprpecated, and replaced with [rememberOverscrollEffect] + * This API has been deprecated, and replaced with [rememberOverscrollEffect] */ @Deprecated( "This API has been replaced with rememberOverscrollEffect, which queries theme provided OverscrollFactory values instead of the 'platform default' without customization.", diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt new file mode 100644 index 0000000000000..8eb2aa7e2d5a3 --- /dev/null +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2026 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.foundation.gestures + +import androidx.compose.foundation.ComposeFoundationFlags +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastForEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +internal class TrackpadScrollingLogic( + scrollingLogic: ScrollingLogic, + onScrollStopped: suspend (velocity: Velocity) -> Unit, + density: Density, +) : NonTouchScrollingLogic(scrollingLogic, onScrollStopped, density) { + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize, + ) { + @OptIn(ExperimentalFoundationApi::class) + if ( + !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || + (pointerEvent.type != PointerEventType.PanStart && + pointerEvent.type != PointerEventType.PanMove && + pointerEvent.type != PointerEventType.PanEnd) + ) + return + if (pointerEvent.isConsumed) return + + /** + * If this scrollable is already scrolling from a previous interaction, consume immediately + * to give it priority. + */ + if (pass == PointerEventPass.Initial && isScrolling) { + onPan(pointerEvent) + pointerEvent.consume() + } + + /** + * During the main pass. If this scrollable is not scrolling, decide if it should based on + * the consumption. If the scrollable is scrolling we don't need to worry because it + * consumed during the initial pass. + */ + if (pass == PointerEventPass.Main && !isScrolling) { + val consumed = onPan(pointerEvent) + if (consumed) { + pointerEvent.consume() + } + } + } + + private class TrackpadScrollDelta(val value: Offset, val timeMillis: Long, val isEnd: Boolean) { + operator fun plus(other: TrackpadScrollDelta) = + TrackpadScrollDelta( + value = value + other.value, + // Pick time from last one + timeMillis = maxOf(timeMillis, other.timeMillis), + // Combine together the flag to end the gesture + isEnd = isEnd || other.isEnd, + ) + } + + private val channel = Channel(capacity = Channel.UNLIMITED) + + private var receivingPanEventsJob: Job? = null + + override fun startReceivingEvents(coroutineScope: CoroutineScope) { + if (receivingPanEventsJob == null) { + receivingPanEventsJob = + coroutineScope.launch { + try { + while (coroutineContext.isActive) { + scrollingLogic.dispatchTrackpadScroll(channel.receive()) + } + } finally { + receivingPanEventsJob = null + } + } + } + } + + private fun onPan(pointerEvent: PointerEvent): Boolean { + @OptIn(ExperimentalFoundationApi::class) + if (!ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) return false + + var sent = false + + pointerEvent.changes.firstOrNull()?.let { + it.historical.fastForEach { historicalChange -> + val delta = -historicalChange.panGestureOffset + if (scrollingLogic.canConsumeDelta(delta)) { + sent = + channel + .trySend( + TrackpadScrollDelta( + value = delta, + timeMillis = historicalChange.uptimeMillis, + isEnd = false, + ) + ) + .isSuccess || sent + } + } + val delta = -it.panGestureOffset + val isPanEnd = pointerEvent.type == PointerEventType.PanEnd + if (scrollingLogic.canConsumeDelta(delta) || isPanEnd) { + sent = + channel + .trySend( + TrackpadScrollDelta( + value = delta, + timeMillis = it.uptimeMillis, + isEnd = isPanEnd, + ) + ) + .isSuccess || sent + } + } + + return sent || isScrolling + } + + private fun Channel.sumOrNull(): TrackpadScrollDelta? { + var sum: TrackpadScrollDelta? = null + for (i in untilNull { tryReceive().getOrNull() }) { + sum = if (sum == null) i else sum + i + } + return sum + } + + private fun ScrollingLogic.canConsumeDelta(scrollDelta: Offset): Boolean = + scrollDelta.reverseIfNeeded().toSingleAxisDeltaFromAngle() != 0f + + private fun trackVelocity(scrollDelta: TrackpadScrollDelta) { + velocityTracker.addDelta(scrollDelta.timeMillis, scrollDelta.value) + } + + private suspend fun ScrollingLogic.dispatchTrackpadScroll(scrollDelta: TrackpadScrollDelta) { + var targetScrollDelta = scrollDelta + trackVelocity(scrollDelta) + // Sum delta from all pending events to drain the channel. + channel.sumOrNull()?.let { + trackVelocity(it) + targetScrollDelta += it + } + + userScroll { + dispatchTrackpadScroll( + targetScrollDelta.value.reverseIfNeeded().toSingleAxisDeltaFromAngle() + ) + while (!targetScrollDelta.isEnd) { + targetScrollDelta = channel.busyReceive() + trackVelocity(targetScrollDelta) + channel.sumOrNull()?.let { + trackVelocity(it) + targetScrollDelta += it + } + dispatchTrackpadScroll( + targetScrollDelta.value.reverseIfNeeded().toSingleAxisDeltaFromAngle() + ) + } + } + + onScrollStopped(velocityTracker.calculateVelocity()) + } + + private fun NestedScrollScope.dispatchTrackpadScroll(delta: Float) = + with(scrollingLogic) { + val offset = delta.reverseIfNeeded().toOffset() + val consumed = scrollByWithOverscroll(offset, NestedScrollSource.UserInput) + consumed.reverseIfNeeded().toFloat() + } +} diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt index c64937b2d24ac..1292d21f7012b 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastFold import androidx.compose.ui.util.fastForEach import kotlin.math.PI import kotlin.math.abs @@ -263,6 +264,14 @@ private class TransformableNode( // curve fitting the ChromeOS's zoom factors. internal const val SCROLL_FACTOR = 545f +/** + * Convert non touch events into the appropriate transform events. There are 3 cases, where order of + * determination matters: + * - If Ctrl is pressed, and we get a scroll, either from a mouse wheel or a trackpad pan, we + * convert that scroll into an equivalent zoom + * - If we get a trackpad pan, we convert that into a pan + * - If we get a trackpad scale, we convert that into a zoom + */ private suspend fun PointerInputScope.detectNonTouchGestures( channel: Channel, scrollConfig: ScrollConfig, @@ -277,15 +286,16 @@ private suspend fun PointerInputScope.detectNonTouchGestures( var pointer: PointerEvent do { pointer = awaitPointerEvent() - zoomOffset = consumePointerEventAsMouseWheelScrollOrNull(pointer, scrollConfig) - @OptIn(ExperimentalFoundationApi::class) - if (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) { - panOffset = consumePointerEventAsPanOrNull(pointer, scrollConfig) - scale = consumePointerEventAsScaleOrNull(pointer) - } else { - panOffset = null - scale = null - } + + // Convert non touch events into the appropriate transform events. + // There are 3 cases, where order of determination matters: + // - If Ctrl is pressed, and we get a scroll, either from a mouse wheel or a + // trackpad pan, we convert that scroll into an equivalent zoom + // - If we get a trackpad pan, we convert that into a pan + // - If we get a trackpad scale, we convert that into a zoom + zoomOffset = consumePointerEventAsCtrlScrollOrNull(pointer, scrollConfig) + panOffset = consumePointerEventAsPanOrNull(pointer) + scale = consumePointerEventAsScaleOrNull(pointer) } while (zoomOffset == null && panOffset == null && scale == null) if (zoomOffset != null) { var scrollDelta: Offset = zoomOffset @@ -304,8 +314,7 @@ private suspend fun PointerInputScope.detectNonTouchGestures( ) pointer = awaitPointerEvent() scrollDelta = - consumePointerEventAsMouseWheelScrollOrNull(pointer, scrollConfig) - ?: break + consumePointerEventAsCtrlScrollOrNull(pointer, scrollConfig) ?: break } } else if (panOffset != null) { var panDelta: Offset = panOffset @@ -320,7 +329,7 @@ private suspend fun PointerInputScope.detectNonTouchGestures( ) ) pointer = awaitPointerEvent() - panDelta = consumePointerEventAsPanOrNull(pointer, scrollConfig) ?: break + panDelta = consumePointerEventAsPanOrNull(pointer) ?: break } } else { var scaleDelta: Float = @@ -353,7 +362,7 @@ private suspend fun PointerInputScope.detectNonTouchGestures( * pressed, its scrollDelta is returned. Otherwise, null is returned. The event is consumed when it * detects ctrl + mouse scroll. */ -private fun AwaitPointerEventScope.consumePointerEventAsMouseWheelScrollOrNull( +private fun AwaitPointerEventScope.consumePointerEventAsCtrlScrollOrNull( pointer: PointerEvent, scrollConfig: ScrollConfig, ): Offset? { @@ -366,7 +375,19 @@ private fun AwaitPointerEventScope.consumePointerEventAsMouseWheelScrollOrNull( ) { return null } - val scrollDelta = with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } + @OptIn(ExperimentalFoundationApi::class) + val scrollDelta = + with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } + + if (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) { + (pointer.changes.firstOrNull()?.let { + -it.panGestureOffset + + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> + acc - historicalChange.panGestureOffset + } + } ?: Offset.Zero) + } else { + Offset.Zero + } if (scrollDelta == Offset.Zero) { return null @@ -376,18 +397,23 @@ private fun AwaitPointerEventScope.consumePointerEventAsMouseWheelScrollOrNull( return scrollDelta } -private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( - pointer: PointerEvent, - scrollConfig: ScrollConfig, -): Offset? { +private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull(pointer: PointerEvent): Offset? { + @OptIn(ExperimentalFoundationApi::class) if ( - pointer.type != PointerEventType.PanStart && - pointer.type != PointerEventType.PanMove && - pointer.type != PointerEventType.PanEnd + !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || + (pointer.type != PointerEventType.PanStart && + pointer.type != PointerEventType.PanMove && + pointer.type != PointerEventType.PanEnd) ) { return null } - val scrollDelta = with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } + val scrollDelta = + pointer.changes.firstOrNull()?.let { + -it.panGestureOffset + + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> + acc - historicalChange.panGestureOffset + } + } ?: Offset.Zero if (scrollDelta == Offset.Zero) { return null @@ -398,10 +424,12 @@ private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( } private fun AwaitPointerEventScope.consumePointerEventAsScaleOrNull(pointer: PointerEvent): Float? { + @OptIn(ExperimentalFoundationApi::class) if ( - pointer.type != PointerEventType.ScaleStart && - pointer.type != PointerEventType.ScaleChange && - pointer.type != PointerEventType.ScaleEnd + !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || + (pointer.type != PointerEventType.ScaleStart && + pointer.type != PointerEventType.ScaleChange && + pointer.type != PointerEventType.ScaleEnd) ) { return null } From cf725453134615cae4be2052778585904ec99e04 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 12 Feb 2026 15:10:59 -0800 Subject: [PATCH 08/16] Rename to scaleFactor and panOffset Renames the new properties in PointerInputChange and HistoricalChange to be scaleFactor and panOffset. This is in response to API feedback, and also avoids confusion over which gesture they refer to. Relnote: "Renames PointerInputChange and HistoricalChange properties for retrieving scale and pan values." Fixes: 481333653 Test: Updated Change-Id: I989a80bdac8c64ce1faacc04bc9ff560fcd963e3 --- .../gestures/TrackpadScrollingLogic.kt | 4 +- .../foundation/gestures/Transformable.kt | 12 +-- .../trackpad/PanWithVelocityTest.kt | 2 +- .../compose/ui/test/util/PointerInputs.kt | 2 +- compose/ui/ui/api/current.txt | 20 ++--- compose/ui/ui/api/restricted_current.txt | 20 ++--- compose/ui/ui/bcv/native/current.txt | 16 ++-- .../ui/input/pointer/HitPathTrackerTest.kt | 4 +- .../compose/ui/input/pointer/TestUtils.kt | 4 +- .../ui/input/pointer/PointerEvent.android.kt | 4 +- .../ui/input/pointer/HitPathTracker.kt | 4 +- .../compose/ui/input/pointer/PointerEvent.kt | 78 ++++++++++--------- 12 files changed, 86 insertions(+), 84 deletions(-) diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt index 8eb2aa7e2d5a3..64280f59d7587 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TrackpadScrollingLogic.kt @@ -113,7 +113,7 @@ internal class TrackpadScrollingLogic( pointerEvent.changes.firstOrNull()?.let { it.historical.fastForEach { historicalChange -> - val delta = -historicalChange.panGestureOffset + val delta = -historicalChange.panOffset if (scrollingLogic.canConsumeDelta(delta)) { sent = channel @@ -127,7 +127,7 @@ internal class TrackpadScrollingLogic( .isSuccess || sent } } - val delta = -it.panGestureOffset + val delta = -it.panOffset val isPanEnd = pointerEvent.type == PointerEventType.PanEnd if (scrollingLogic.canConsumeDelta(delta) || isPanEnd) { sent = diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt index 1292d21f7012b..14e729894deb8 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt @@ -380,9 +380,9 @@ private fun AwaitPointerEventScope.consumePointerEventAsCtrlScrollOrNull( with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } + if (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) { (pointer.changes.firstOrNull()?.let { - -it.panGestureOffset + + -it.panOffset + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> - acc - historicalChange.panGestureOffset + acc - historicalChange.panOffset } } ?: Offset.Zero) } else { @@ -409,9 +409,9 @@ private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull(pointer: Point } val scrollDelta = pointer.changes.firstOrNull()?.let { - -it.panGestureOffset + + -it.panOffset + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> - acc - historicalChange.panGestureOffset + acc - historicalChange.panOffset } } ?: Offset.Zero @@ -435,8 +435,8 @@ private fun AwaitPointerEventScope.consumePointerEventAsScaleOrNull(pointer: Poi } var scaleDelta = 1f pointer.changes.fastForEach { - scaleDelta *= it.scaleGestureFactor - it.historical.fastForEach { scaleDelta *= it.scaleGestureFactor } + scaleDelta *= it.scaleFactor + it.historical.fastForEach { scaleDelta *= it.scaleFactor } } if (scaleDelta == 1f) { diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt index bf50e40baa3a0..6f5031a2a86eb 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/injectionscope/trackpad/PanWithVelocityTest.kt @@ -152,7 +152,7 @@ private class TrackpadPanInputRecorder : PointerInputModifier { if (it.pressed) { fingerVelocityTracker.addPosition(it.uptimeMillis, it.position) } else if (event.type == PointerEventType.PanMove) { - accumulatedPan += it.panGestureOffset + accumulatedPan += it.panOffset panVelocityTracker.addPosition(it.uptimeMillis, accumulatedPan) } } diff --git a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt index bbba37b1d9031..2ee0220aabaf9 100644 --- a/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt +++ b/compose/ui/ui-test/src/androidDeviceTest/kotlin/androidx/compose/ui/test/util/PointerInputs.kt @@ -59,7 +59,7 @@ data class DataPoint( change.uptimeMillis, change.position, change.scrollDelta, - change.panGestureOffset, + change.panOffset, change.pressed, change.type, event.type, diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index 101f37ab091ee..9336382a93866 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt @@ -2342,17 +2342,17 @@ package androidx.compose.ui.input.pointer { } @androidx.compose.runtime.Immutable public final class HistoricalChange { - ctor @KotlinOnly public HistoricalChange(long uptimeMillis, androidx.compose.ui.geometry.Offset position, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); + ctor @KotlinOnly public HistoricalChange(long uptimeMillis, androidx.compose.ui.geometry.Offset position, optional float scaleFactor, optional androidx.compose.ui.geometry.Offset panOffset); ctor @BytecodeOnly public HistoricalChange(long, long, float, long, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly public HistoricalChange(long, long, float, long, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly @Deprecated public HistoricalChange(long, long, kotlin.jvm.internal.DefaultConstructorMarker!); - method @BytecodeOnly public long getPanGestureOffset-F1C5BW0(); + method @BytecodeOnly public long getPanOffset-F1C5BW0(); method @BytecodeOnly public long getPosition-F1C5BW0(); - method @InaccessibleFromKotlin public float getScaleGestureFactor(); + method @InaccessibleFromKotlin public float getScaleFactor(); method @InaccessibleFromKotlin public long getUptimeMillis(); - property public androidx.compose.ui.geometry.Offset panGestureOffset; + property public androidx.compose.ui.geometry.Offset panOffset; property public androidx.compose.ui.geometry.Offset position; - property public float scaleGestureFactor; + property public float scaleFactor; property public long uptimeMillis; } @@ -2526,7 +2526,7 @@ package androidx.compose.ui.input.pointer { } @androidx.compose.runtime.Immutable public final class PointerInputChange { - ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, float pressure, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); + ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, float pressure, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleFactor, optional androidx.compose.ui.geometry.Offset panOffset); ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); ctor @BytecodeOnly public PointerInputChange(long, long, long, boolean, float, long, long, boolean, boolean, int, long, float, long, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly public PointerInputChange(long, long, long, boolean, float, long, long, boolean, boolean, int, long, float, long, kotlin.jvm.internal.DefaultConstructorMarker!); @@ -2562,14 +2562,14 @@ package androidx.compose.ui.input.pointer { method @InaccessibleFromKotlin @Deprecated public androidx.compose.ui.input.pointer.ConsumedData getConsumed(); method @InaccessibleFromKotlin public java.util.List getHistorical(); method @BytecodeOnly public long getId-J3iCeTQ(); - method @BytecodeOnly public long getPanGestureOffset-F1C5BW0(); + method @BytecodeOnly public long getPanOffset-F1C5BW0(); method @BytecodeOnly public long getPosition-F1C5BW0(); method @InaccessibleFromKotlin public boolean getPressed(); method @InaccessibleFromKotlin public float getPressure(); method @BytecodeOnly public long getPreviousPosition-F1C5BW0(); method @InaccessibleFromKotlin public boolean getPreviousPressed(); method @InaccessibleFromKotlin public long getPreviousUptimeMillis(); - method @InaccessibleFromKotlin public float getScaleGestureFactor(); + method @InaccessibleFromKotlin public float getScaleFactor(); method @BytecodeOnly public long getScrollDelta-F1C5BW0(); method @BytecodeOnly public int getType-T8wyACA(); method @InaccessibleFromKotlin public long getUptimeMillis(); @@ -2578,14 +2578,14 @@ package androidx.compose.ui.input.pointer { property public java.util.List historical; property public androidx.compose.ui.input.pointer.PointerId id; property public boolean isConsumed; - property public androidx.compose.ui.geometry.Offset panGestureOffset; + property public androidx.compose.ui.geometry.Offset panOffset; property public androidx.compose.ui.geometry.Offset position; property public boolean pressed; property public float pressure; property public androidx.compose.ui.geometry.Offset previousPosition; property public boolean previousPressed; property public long previousUptimeMillis; - property public float scaleGestureFactor; + property public float scaleFactor; property public androidx.compose.ui.geometry.Offset scrollDelta; property public androidx.compose.ui.input.pointer.PointerType type; property public long uptimeMillis; diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt index cb65611d15dc6..050d114bd4791 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt @@ -2343,17 +2343,17 @@ package androidx.compose.ui.input.pointer { } @androidx.compose.runtime.Immutable public final class HistoricalChange { - ctor @KotlinOnly public HistoricalChange(long uptimeMillis, androidx.compose.ui.geometry.Offset position, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); + ctor @KotlinOnly public HistoricalChange(long uptimeMillis, androidx.compose.ui.geometry.Offset position, optional float scaleFactor, optional androidx.compose.ui.geometry.Offset panOffset); ctor @BytecodeOnly public HistoricalChange(long, long, float, long, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly public HistoricalChange(long, long, float, long, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly @Deprecated public HistoricalChange(long, long, kotlin.jvm.internal.DefaultConstructorMarker!); - method @BytecodeOnly public long getPanGestureOffset-F1C5BW0(); + method @BytecodeOnly public long getPanOffset-F1C5BW0(); method @BytecodeOnly public long getPosition-F1C5BW0(); - method @InaccessibleFromKotlin public float getScaleGestureFactor(); + method @InaccessibleFromKotlin public float getScaleFactor(); method @InaccessibleFromKotlin public long getUptimeMillis(); - property public androidx.compose.ui.geometry.Offset panGestureOffset; + property public androidx.compose.ui.geometry.Offset panOffset; property public androidx.compose.ui.geometry.Offset position; - property public float scaleGestureFactor; + property public float scaleFactor; property public long uptimeMillis; } @@ -2527,7 +2527,7 @@ package androidx.compose.ui.input.pointer { } @androidx.compose.runtime.Immutable public final class PointerInputChange { - ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, float pressure, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); + ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, float pressure, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleFactor, optional androidx.compose.ui.geometry.Offset panOffset); ctor @KotlinOnly public PointerInputChange(androidx.compose.ui.input.pointer.PointerId id, long uptimeMillis, androidx.compose.ui.geometry.Offset position, boolean pressed, long previousUptimeMillis, androidx.compose.ui.geometry.Offset previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional androidx.compose.ui.input.pointer.PointerType type, optional androidx.compose.ui.geometry.Offset scrollDelta, optional float scaleGestureFactor, optional androidx.compose.ui.geometry.Offset panGestureOffset); ctor @BytecodeOnly public PointerInputChange(long, long, long, boolean, float, long, long, boolean, boolean, int, long, float, long, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor @BytecodeOnly public PointerInputChange(long, long, long, boolean, float, long, long, boolean, boolean, int, long, float, long, kotlin.jvm.internal.DefaultConstructorMarker!); @@ -2563,14 +2563,14 @@ package androidx.compose.ui.input.pointer { method @InaccessibleFromKotlin @Deprecated public androidx.compose.ui.input.pointer.ConsumedData getConsumed(); method @InaccessibleFromKotlin public java.util.List getHistorical(); method @BytecodeOnly public long getId-J3iCeTQ(); - method @BytecodeOnly public long getPanGestureOffset-F1C5BW0(); + method @BytecodeOnly public long getPanOffset-F1C5BW0(); method @BytecodeOnly public long getPosition-F1C5BW0(); method @InaccessibleFromKotlin public boolean getPressed(); method @InaccessibleFromKotlin public float getPressure(); method @BytecodeOnly public long getPreviousPosition-F1C5BW0(); method @InaccessibleFromKotlin public boolean getPreviousPressed(); method @InaccessibleFromKotlin public long getPreviousUptimeMillis(); - method @InaccessibleFromKotlin public float getScaleGestureFactor(); + method @InaccessibleFromKotlin public float getScaleFactor(); method @BytecodeOnly public long getScrollDelta-F1C5BW0(); method @BytecodeOnly public int getType-T8wyACA(); method @InaccessibleFromKotlin public long getUptimeMillis(); @@ -2579,14 +2579,14 @@ package androidx.compose.ui.input.pointer { property public java.util.List historical; property public androidx.compose.ui.input.pointer.PointerId id; property public boolean isConsumed; - property public androidx.compose.ui.geometry.Offset panGestureOffset; + property public androidx.compose.ui.geometry.Offset panOffset; property public androidx.compose.ui.geometry.Offset position; property public boolean pressed; property public float pressure; property public androidx.compose.ui.geometry.Offset previousPosition; property public boolean previousPressed; property public long previousUptimeMillis; - property public float scaleGestureFactor; + property public float scaleFactor; property public androidx.compose.ui.geometry.Offset scrollDelta; property public androidx.compose.ui.input.pointer.PointerType type; property public long uptimeMillis; diff --git a/compose/ui/ui/bcv/native/current.txt b/compose/ui/ui/bcv/native/current.txt index 7cd7b1a08e664..61c9cddbc7ea8 100644 --- a/compose/ui/ui/bcv/native/current.txt +++ b/compose/ui/ui/bcv/native/current.txt @@ -1801,12 +1801,12 @@ final class androidx.compose.ui.input.pointer/HistoricalChange { // androidx.com constructor (kotlin/Long, androidx.compose.ui.geometry/Offset) // androidx.compose.ui.input.pointer/HistoricalChange.|(kotlin.Long;androidx.compose.ui.geometry.Offset){}[0] constructor (kotlin/Long, androidx.compose.ui.geometry/Offset, kotlin/Float = ..., androidx.compose.ui.geometry/Offset = ...) // androidx.compose.ui.input.pointer/HistoricalChange.|(kotlin.Long;androidx.compose.ui.geometry.Offset;kotlin.Float;androidx.compose.ui.geometry.Offset){}[0] - final val panGestureOffset // androidx.compose.ui.input.pointer/HistoricalChange.panGestureOffset|{}panGestureOffset[0] - final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/HistoricalChange.panGestureOffset.|(){}[0] + final val panOffset // androidx.compose.ui.input.pointer/HistoricalChange.panOffset|{}panOffset[0] + final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/HistoricalChange.panOffset.|(){}[0] final val position // androidx.compose.ui.input.pointer/HistoricalChange.position|{}position[0] final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/HistoricalChange.position.|(){}[0] - final val scaleGestureFactor // androidx.compose.ui.input.pointer/HistoricalChange.scaleGestureFactor|{}scaleGestureFactor[0] - final fun (): kotlin/Float // androidx.compose.ui.input.pointer/HistoricalChange.scaleGestureFactor.|(){}[0] + final val scaleFactor // androidx.compose.ui.input.pointer/HistoricalChange.scaleFactor|{}scaleFactor[0] + final fun (): kotlin/Float // androidx.compose.ui.input.pointer/HistoricalChange.scaleFactor.|(){}[0] final val uptimeMillis // androidx.compose.ui.input.pointer/HistoricalChange.uptimeMillis|{}uptimeMillis[0] final fun (): kotlin/Long // androidx.compose.ui.input.pointer/HistoricalChange.uptimeMillis.|(){}[0] @@ -1853,8 +1853,8 @@ final class androidx.compose.ui.input.pointer/PointerInputChange { // androidx.c final fun (): androidx.compose.ui.input.pointer/PointerId // androidx.compose.ui.input.pointer/PointerInputChange.id.|(){}[0] final val isConsumed // androidx.compose.ui.input.pointer/PointerInputChange.isConsumed|{}isConsumed[0] final fun (): kotlin/Boolean // androidx.compose.ui.input.pointer/PointerInputChange.isConsumed.|(){}[0] - final val panGestureOffset // androidx.compose.ui.input.pointer/PointerInputChange.panGestureOffset|{}panGestureOffset[0] - final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/PointerInputChange.panGestureOffset.|(){}[0] + final val panOffset // androidx.compose.ui.input.pointer/PointerInputChange.panOffset|{}panOffset[0] + final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/PointerInputChange.panOffset.|(){}[0] final val position // androidx.compose.ui.input.pointer/PointerInputChange.position|{}position[0] final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/PointerInputChange.position.|(){}[0] final val pressed // androidx.compose.ui.input.pointer/PointerInputChange.pressed|{}pressed[0] @@ -1867,8 +1867,8 @@ final class androidx.compose.ui.input.pointer/PointerInputChange { // androidx.c final fun (): kotlin/Boolean // androidx.compose.ui.input.pointer/PointerInputChange.previousPressed.|(){}[0] final val previousUptimeMillis // androidx.compose.ui.input.pointer/PointerInputChange.previousUptimeMillis|{}previousUptimeMillis[0] final fun (): kotlin/Long // androidx.compose.ui.input.pointer/PointerInputChange.previousUptimeMillis.|(){}[0] - final val scaleGestureFactor // androidx.compose.ui.input.pointer/PointerInputChange.scaleGestureFactor|{}scaleGestureFactor[0] - final fun (): kotlin/Float // androidx.compose.ui.input.pointer/PointerInputChange.scaleGestureFactor.|(){}[0] + final val scaleFactor // androidx.compose.ui.input.pointer/PointerInputChange.scaleFactor|{}scaleFactor[0] + final fun (): kotlin/Float // androidx.compose.ui.input.pointer/PointerInputChange.scaleFactor.|(){}[0] final val scrollDelta // androidx.compose.ui.input.pointer/PointerInputChange.scrollDelta|{}scrollDelta[0] final fun (): androidx.compose.ui.geometry/Offset // androidx.compose.ui.input.pointer/PointerInputChange.scrollDelta.|(){}[0] final val type // androidx.compose.ui.input.pointer/PointerInputChange.type|{}type[0] diff --git a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt index 90274ef068626..fd9596418261b 100644 --- a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt +++ b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt @@ -3302,8 +3302,8 @@ class HitPathTrackerTest { HistoricalChange( uptimeMillis = 1L, position = Offset.Unspecified, - scaleGestureFactor = 0f, - panGestureOffset = Offset.Zero, + scaleFactor = 0f, + panOffset = Offset.Zero, ) ), ) diff --git a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt index b2560d5c53f29..0b7c29fe1e230 100644 --- a/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt +++ b/compose/ui/ui/src/androidDeviceTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt @@ -272,8 +272,8 @@ internal fun InternalPointerEvent( down = data.pressed, pressure = data.pressure, type = data.type, - scaleGestureFactor = data.scaleGestureFactor, - panGestureOffset = data.panGestureOffset, + scaleGestureFactor = data.scaleFactor, + panGestureOffset = data.panOffset, ) ) } diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt index 28b4d4eb36f0c..f64351f9a6426 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt @@ -210,8 +210,8 @@ internal actual constructor( type = change.type, activeHover = this.internalPointerEvent?.activeHoverEvent(change.id) == true, - scaleGestureFactor = change.scaleGestureFactor, - panGestureOffset = change.panGestureOffset, + scaleGestureFactor = change.scaleFactor, + panGestureOffset = change.panOffset, ) } diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt index 853b040d8fdcf..45526ea648244 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt @@ -530,8 +530,8 @@ internal class Node(val modifierNode: Modifier.Node) : NodeParent() { parentCoordinates, historicalPosition, ), - scaleGestureFactor = it.scaleGestureFactor, - panGestureOffset = it.panGestureOffset, + scaleGestureFactor = it.scaleFactor, + panGestureOffset = it.panOffset, originalEventPosition = it.originalEventPosition, ) ) diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt index 2fea37cb94dc3..99159e51dbfc1 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt @@ -308,7 +308,7 @@ value class PointerEventType private constructor(internal val value: Int) { /** * An intermediate scale move. This can happen, for example, due to a trackpad gesture * recognized by the platform. This event indicates that the - * [PointerInputChange.scaleGestureFactor]'s [Offset] may be different from 1. + * [PointerInputChange.scaleFactor]'s [Offset] may be different from 1. */ val ScaleChange = PointerEventType(8) @@ -328,7 +328,7 @@ value class PointerEventType private constructor(internal val value: Int) { /** * An intermediate pan move. This can happen, for example, due to a trackpad gesture * recognized by the platform. This event indicates that the - * [PointerInputChange.panGestureOffset]'s [Offset] may be non-zero. + * [PointerInputChange.panOffset]'s [Offset] may be non-zero. */ val PanMove = PointerEventType(11) @@ -408,13 +408,15 @@ value class PointerEventType private constructor(internal val value: Int) { * @param type The device type that produced the event, such as [mouse][PointerType.Mouse], or * [touch][PointerType.Touch]. * @param scrollDelta The amount of scroll wheel movement in the horizontal and vertical directions. - * Note that this is not an offset in pixel coordinates. Also consider [panGestureOffset]. - * @param scaleGestureFactor A multiplicative scale factor indicating the amount of scale to perform - * as part of this pointer input change. A value of `1f` indicates no scale, a value less than - * `1f` indicates a scale down, commonly causing a zoom out, and a value greater than `1f` - * indicates a scale up, commonly causing a zoom in. - * @param panGestureOffset An [Offset] in pixel coordinates indicating an amount of scrolling. Also - * consider [scrollDelta]. + * Note that this is not an offset in pixel coordinates, but is instead an offset in a platform + * specific mouse wheel tick units. Also consider handling [panOffset], which represents a similar + * action from trackpads. + * @param scaleFactor A multiplicative scale factor indicating the amount of scale to perform as + * part of this pointer input change. A value of `1f` indicates no scale, a value less than `1f` + * indicates a scale down, commonly causing a zoom out, and a value greater than `1f` indicates a + * scale up, commonly causing a zoom in. + * @param panOffset An [Offset] in pixel coordinates indicating an amount of panning. Also consider + * handling [scrollDelta], which represents a similar action from mouse wheels. */ @Immutable class PointerInputChange( @@ -429,8 +431,8 @@ class PointerInputChange( isInitiallyConsumed: Boolean, val type: PointerType = PointerType.Touch, val scrollDelta: Offset = Offset.Zero, - val scaleGestureFactor: Float = 1f, - val panGestureOffset: Offset = Offset.Zero, + val scaleFactor: Float = 1f, + val panOffset: Offset = Offset.Zero, ) { constructor( id: PointerId, @@ -457,8 +459,8 @@ class PointerInputChange( isInitiallyConsumed = isInitiallyConsumed, type = type, scrollDelta = scrollDelta, - scaleGestureFactor = scaleGestureFactor, - panGestureOffset = panGestureOffset, + scaleFactor = scaleGestureFactor, + panOffset = panGestureOffset, ) @Deprecated(message = "Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @@ -576,8 +578,8 @@ class PointerInputChange( isInitiallyConsumed = isInitiallyConsumed, type = type, scrollDelta = scrollDelta, - scaleGestureFactor = scaleGestureFactor, - panGestureOffset = panGestureOffset, + scaleFactor = scaleGestureFactor, + panOffset = panGestureOffset, ) { _historical = historical this.originalEventPosition = originalEventPosition @@ -672,8 +674,8 @@ class PointerInputChange( type = type, historical = this.historical, scrollDelta = this.scrollDelta, - scaleGestureFactor = this.scaleGestureFactor, - panGestureOffset = this.panGestureOffset, + scaleGestureFactor = this.scaleFactor, + panGestureOffset = this.panOffset, originalEventPosition = this.originalEventPosition, ) .also { @@ -758,8 +760,8 @@ class PointerInputChange( type = type, historical = this.historical, scrollDelta = scrollDelta, - scaleGestureFactor = this.scaleGestureFactor, - panGestureOffset = this.panGestureOffset, + scaleGestureFactor = this.scaleFactor, + panGestureOffset = this.panOffset, originalEventPosition = this.originalEventPosition, ) .also { @@ -802,8 +804,8 @@ class PointerInputChange( type = type, historical = this.historical, scrollDelta = scrollDelta, - scaleGestureFactor = this.scaleGestureFactor, - panGestureOffset = this.panGestureOffset, + scaleGestureFactor = this.scaleFactor, + panGestureOffset = this.panOffset, originalEventPosition = this.originalEventPosition, ) .also { @@ -888,8 +890,8 @@ class PointerInputChange( type = type, historical = historical, scrollDelta = scrollDelta, - scaleGestureFactor = this.scaleGestureFactor, - panGestureOffset = this.panGestureOffset, + scaleGestureFactor = this.scaleFactor, + panGestureOffset = this.panOffset, originalEventPosition = this.originalEventPosition, ) .also { @@ -919,8 +921,8 @@ class PointerInputChange( type: PointerType = this.type, historical: List = this.historical, scrollDelta: Offset = this.scrollDelta, - scaleGestureFactor: Float = this.scaleGestureFactor, - panGestureOffset: Offset = this.panGestureOffset, + scaleGestureFactor: Float = this.scaleFactor, + panGestureOffset: Offset = this.panOffset, ): PointerInputChange = PointerInputChange( id = id, @@ -959,8 +961,8 @@ class PointerInputChange( "type=$type, " + "historical=$historical, " + "scrollDelta=$scrollDelta, " + - "scaleGestureFactor=$scaleGestureFactor, " + - "panGestureOffset=$panGestureOffset)" + "scaleFactor=$scaleFactor, " + + "panOffset=$panOffset)" } } @@ -973,18 +975,18 @@ class PointerInputChange( * @param uptimeMillis The time of the historical pointer event, in milliseconds. In between the * current and previous pointer event times. * @param position The [Offset] of the historical pointer event, relative to the containing element. - * @param scaleGestureFactor A multiplicative scale factor indicating the amount of scale to perform - * as part of this pointer input change. A value of `1f` indicates no scale, a value less than - * `1f` indicates a scale down, commonly causing a zoom out, and a value greater than `1f` - * indicates a scale up, commonly causing a zoom in. - * @param panGestureOffset An [Offset] in pixel coordinates indicating an amount of scrolling. + * @param scaleFactor A multiplicative scale factor indicating the amount of scale to perform as + * part of this pointer input change. A value of `1f` indicates no scale, a value less than `1f` + * indicates a scale down, commonly causing a zoom out, and a value greater than `1f` indicates a + * scale up, commonly causing a zoom in. + * @param panOffset An [Offset] in pixel coordinates indicating an amount of scrolling. */ @Immutable class HistoricalChange( val uptimeMillis: Long, val position: Offset, - val scaleGestureFactor: Float = 1f, - val panGestureOffset: Offset = Offset.Zero, + val scaleFactor: Float = 1f, + val panOffset: Offset = Offset.Zero, ) { internal var originalEventPosition: Offset = Offset.Zero private set @@ -996,8 +998,8 @@ class HistoricalChange( ) : this( uptimeMillis = uptimeMillis, position = position, - scaleGestureFactor = 1f, - panGestureOffset = Offset.Zero, + scaleFactor = 1f, + panOffset = Offset.Zero, ) internal constructor( @@ -1013,8 +1015,8 @@ class HistoricalChange( override fun toString(): String { return "HistoricalChange(uptimeMillis=$uptimeMillis, " + "position=$position, " + - "scaleGestureFactor=$scaleGestureFactor, " + - "panGestureOffset=$panGestureOffset)" + "scaleFactor=$scaleFactor, " + + "panOffset=$panOffset)" } } From 4456e0104884f4180494b5e8cb94240c33c00eda Mon Sep 17 00:00:00 2001 From: Bob Wang Date: Tue, 24 Feb 2026 09:35:19 -0800 Subject: [PATCH 09/16] Bump the version of core.uwb to 1.0.0-beta01. Test: N/A Bug: 435257668 Change-Id: I79f39589ac1fa282eb94dc8cbd92713e29427591 --- core/uwb/uwb-rxjava3/api/1.0.0-beta01.txt | 21 ++ core/uwb/uwb-rxjava3/api/res-1.0.0-beta01.txt | 0 .../api/restricted_1.0.0-beta01.txt | 21 ++ core/uwb/uwb/api/1.0.0-beta01.txt | 280 ++++++++++++++++++ core/uwb/uwb/api/res-1.0.0-beta01.txt | 0 core/uwb/uwb/api/restricted_1.0.0-beta01.txt | 280 ++++++++++++++++++ libraryversions.toml | 2 +- 7 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 core/uwb/uwb-rxjava3/api/1.0.0-beta01.txt create mode 100644 core/uwb/uwb-rxjava3/api/res-1.0.0-beta01.txt create mode 100644 core/uwb/uwb-rxjava3/api/restricted_1.0.0-beta01.txt create mode 100644 core/uwb/uwb/api/1.0.0-beta01.txt create mode 100644 core/uwb/uwb/api/res-1.0.0-beta01.txt create mode 100644 core/uwb/uwb/api/restricted_1.0.0-beta01.txt diff --git a/core/uwb/uwb-rxjava3/api/1.0.0-beta01.txt b/core/uwb/uwb-rxjava3/api/1.0.0-beta01.txt new file mode 100644 index 0000000000000..9f9d29f8e0352 --- /dev/null +++ b/core/uwb/uwb-rxjava3/api/1.0.0-beta01.txt @@ -0,0 +1,21 @@ +// Signature format: 4.0 +package androidx.core.uwb.rxjava3 { + + public final class UwbClientSessionScopeRx { + method public static io.reactivex.rxjava3.core.Flowable rangingResultsFlowable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters); + method public static io.reactivex.rxjava3.core.Observable rangingResultsObservable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters); + } + + public final class UwbControllerSessionScopeRx { + method public static io.reactivex.rxjava3.core.Single addControleeSingle(androidx.core.uwb.UwbControllerSessionScope, androidx.core.uwb.UwbAddress address); + method public static io.reactivex.rxjava3.core.Single removeControleeSingle(androidx.core.uwb.UwbControllerSessionScope, androidx.core.uwb.UwbAddress address); + } + + public final class UwbManagerRx { + method @Deprecated public static io.reactivex.rxjava3.core.Single clientSessionScopeSingle(androidx.core.uwb.UwbManager); + method public static io.reactivex.rxjava3.core.Single controleeSessionScopeSingle(androidx.core.uwb.UwbManager); + method public static io.reactivex.rxjava3.core.Single controllerSessionScopeSingle(androidx.core.uwb.UwbManager); + } + +} + diff --git a/core/uwb/uwb-rxjava3/api/res-1.0.0-beta01.txt b/core/uwb/uwb-rxjava3/api/res-1.0.0-beta01.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/core/uwb/uwb-rxjava3/api/restricted_1.0.0-beta01.txt b/core/uwb/uwb-rxjava3/api/restricted_1.0.0-beta01.txt new file mode 100644 index 0000000000000..9f9d29f8e0352 --- /dev/null +++ b/core/uwb/uwb-rxjava3/api/restricted_1.0.0-beta01.txt @@ -0,0 +1,21 @@ +// Signature format: 4.0 +package androidx.core.uwb.rxjava3 { + + public final class UwbClientSessionScopeRx { + method public static io.reactivex.rxjava3.core.Flowable rangingResultsFlowable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters); + method public static io.reactivex.rxjava3.core.Observable rangingResultsObservable(androidx.core.uwb.UwbClientSessionScope, androidx.core.uwb.RangingParameters parameters); + } + + public final class UwbControllerSessionScopeRx { + method public static io.reactivex.rxjava3.core.Single addControleeSingle(androidx.core.uwb.UwbControllerSessionScope, androidx.core.uwb.UwbAddress address); + method public static io.reactivex.rxjava3.core.Single removeControleeSingle(androidx.core.uwb.UwbControllerSessionScope, androidx.core.uwb.UwbAddress address); + } + + public final class UwbManagerRx { + method @Deprecated public static io.reactivex.rxjava3.core.Single clientSessionScopeSingle(androidx.core.uwb.UwbManager); + method public static io.reactivex.rxjava3.core.Single controleeSessionScopeSingle(androidx.core.uwb.UwbManager); + method public static io.reactivex.rxjava3.core.Single controllerSessionScopeSingle(androidx.core.uwb.UwbManager); + } + +} + diff --git a/core/uwb/uwb/api/1.0.0-beta01.txt b/core/uwb/uwb/api/1.0.0-beta01.txt new file mode 100644 index 0000000000000..be4d8820f1504 --- /dev/null +++ b/core/uwb/uwb/api/1.0.0-beta01.txt @@ -0,0 +1,280 @@ +// Signature format: 4.0 +package androidx.core.uwb { + + public final class RangingCapabilities { + method @InaccessibleFromKotlin public int getMinRangingInterval(); + method @InaccessibleFromKotlin public java.util.Set getSupportedChannels(); + method @InaccessibleFromKotlin public java.util.Set getSupportedConfigIds(); + method @InaccessibleFromKotlin public java.util.Set getSupportedNtfConfigs(); + method @InaccessibleFromKotlin public java.util.Set getSupportedRangingUpdateRates(); + method @InaccessibleFromKotlin public java.util.Set getSupportedSlotDurations(); + method @InaccessibleFromKotlin public boolean isAzimuthalAngleSupported(); + method @InaccessibleFromKotlin public boolean isBackgroundRangingSupported(); + method @InaccessibleFromKotlin public boolean isDistanceSupported(); + method @InaccessibleFromKotlin public boolean isElevationAngleSupported(); + method @InaccessibleFromKotlin public boolean isRangingIntervalReconfigureSupported(); + property public boolean isAzimuthalAngleSupported; + property public boolean isBackgroundRangingSupported; + property public boolean isDistanceSupported; + property public boolean isElevationAngleSupported; + property public boolean isRangingIntervalReconfigureSupported; + property public int minRangingInterval; + property public java.util.Set supportedChannels; + property public java.util.Set supportedConfigIds; + property public java.util.Set supportedNtfConfigs; + property public java.util.Set supportedRangingUpdateRates; + property public java.util.Set supportedSlotDurations; + } + + public final class RangingControleeParameters { + ctor public RangingControleeParameters(int subSessionId, optional byte[]? subSessionKey); + ctor @BytecodeOnly public RangingControleeParameters(int, byte[]!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + method @InaccessibleFromKotlin public int getSubSessionId(); + method @InaccessibleFromKotlin public byte[]? getSubSessionKey(); + property public int subSessionId; + property public byte[]? subSessionKey; + } + + public final class RangingMeasurement { + ctor public RangingMeasurement(float value); + method @InaccessibleFromKotlin public float getValue(); + property public float value; + } + + public final class RangingParameters { + ctor @BytecodeOnly public RangingParameters(int, int, int, byte[]!, byte[]!, androidx.core.uwb.UwbComplexChannel!, java.util.List!, int, androidx.core.uwb.UwbRangeDataNtfConfig!, long, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig, optional @IntRange(from=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_1_MILLIS, to=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_2_MILLIS) long slotDurationMillis); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig, optional @IntRange(from=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_1_MILLIS, to=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_2_MILLIS) long slotDurationMillis, optional boolean isAoaDisabled); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbComplexChannel? getComplexChannel(); + method @InaccessibleFromKotlin public java.util.List getPeerDevices(); + method @InaccessibleFromKotlin public int getSessionId(); + method @InaccessibleFromKotlin public byte[]? getSessionKeyInfo(); + method @InaccessibleFromKotlin public long getSlotDurationMillis(); + method @InaccessibleFromKotlin public int getSubSessionId(); + method @InaccessibleFromKotlin public byte[]? getSubSessionKeyInfo(); + method @InaccessibleFromKotlin public int getUpdateRateType(); + method @InaccessibleFromKotlin public int getUwbConfigType(); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbRangeDataNtfConfig? getUwbRangeDataNtfConfig(); + method @InaccessibleFromKotlin public boolean isAoaDisabled(); + property public androidx.core.uwb.UwbComplexChannel? complexChannel; + property public boolean isAoaDisabled; + property public java.util.List peerDevices; + property public int sessionId; + property public byte[]? sessionKeyInfo; + property public long slotDurationMillis; + property public int subSessionId; + property public byte[]? subSessionKeyInfo; + property public int updateRateType; + property public int uwbConfigType; + property public androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig; + field public static final int CONFIG_MULTICAST_DS_TWR = 2; // 0x2 + field public static final int CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR = 7; // 0x7 + field public static final int CONFIG_PROVISIONED_MULTICAST_DS_TWR = 5; // 0x5 + field public static final int CONFIG_PROVISIONED_UNICAST_DS_TWR = 4; // 0x4 + field public static final int CONFIG_UNICAST_DS_TWR = 1; // 0x1 + field public static final androidx.core.uwb.RangingParameters.Companion Companion; + field public static final long RANGING_SLOT_DURATION_1_MILLIS = 1L; // 0x1L + field public static final long RANGING_SLOT_DURATION_2_MILLIS = 2L; // 0x2L + field public static final int RANGING_UPDATE_RATE_AUTOMATIC = 1; // 0x1 + field public static final int RANGING_UPDATE_RATE_FREQUENT = 3; // 0x3 + field public static final int RANGING_UPDATE_RATE_INFREQUENT = 2; // 0x2 + } + + public static final class RangingParameters.Companion { + property public static int CONFIG_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_UNICAST_DS_TWR; + property public static int CONFIG_UNICAST_DS_TWR; + property public static long RANGING_SLOT_DURATION_1_MILLIS; + property public static long RANGING_SLOT_DURATION_2_MILLIS; + property public static int RANGING_UPDATE_RATE_AUTOMATIC; + property public static int RANGING_UPDATE_RATE_FREQUENT; + property public static int RANGING_UPDATE_RATE_INFREQUENT; + } + + public final class RangingPosition { + ctor public RangingPosition(androidx.core.uwb.RangingMeasurement? distance, androidx.core.uwb.RangingMeasurement? azimuth, androidx.core.uwb.RangingMeasurement? elevation, long elapsedRealtimeNanos); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getAzimuth(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getDistance(); + method @InaccessibleFromKotlin public long getElapsedRealtimeNanos(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getElevation(); + property public androidx.core.uwb.RangingMeasurement? azimuth; + property public androidx.core.uwb.RangingMeasurement? distance; + property public long elapsedRealtimeNanos; + property public androidx.core.uwb.RangingMeasurement? elevation; + } + + public abstract sealed exhaustive class RangingResult { + method @InaccessibleFromKotlin public abstract androidx.core.uwb.UwbDevice getDevice(); + property public abstract androidx.core.uwb.UwbDevice device; + field public static final androidx.core.uwb.RangingResult.Companion Companion; + field public static final int RANGING_FAILURE_REASON_BAD_PARAMETERS = 1; // 0x1 + field public static final int RANGING_FAILURE_REASON_FAILED_TO_START = 2; // 0x2 + field public static final int RANGING_FAILURE_REASON_MAX_RR_RETRY_REACHED = 6; // 0x6 + field public static final int RANGING_FAILURE_REASON_STOPPED_BY_LOCAL = 4; // 0x4 + field public static final int RANGING_FAILURE_REASON_STOPPED_BY_PEER = 3; // 0x3 + field public static final int RANGING_FAILURE_REASON_SYSTEM_POLICY = 5; // 0x5 + field public static final int RANGING_FAILURE_REASON_UNKNOWN = 0; // 0x0 + } + + public static final class RangingResult.Companion { + property public static int RANGING_FAILURE_REASON_BAD_PARAMETERS; + property public static int RANGING_FAILURE_REASON_FAILED_TO_START; + property public static int RANGING_FAILURE_REASON_MAX_RR_RETRY_REACHED; + property public static int RANGING_FAILURE_REASON_STOPPED_BY_LOCAL; + property public static int RANGING_FAILURE_REASON_STOPPED_BY_PEER; + property public static int RANGING_FAILURE_REASON_SYSTEM_POLICY; + property public static int RANGING_FAILURE_REASON_UNKNOWN; + } + + public static final class RangingResult.RangingResultFailure extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultFailure(androidx.core.uwb.UwbDevice device, int reason); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public int getReason(); + property public androidx.core.uwb.UwbDevice device; + property public int reason; + } + + public static final class RangingResult.RangingResultInitialized extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultInitialized(androidx.core.uwb.UwbDevice device); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + property public androidx.core.uwb.UwbDevice device; + } + + public static final class RangingResult.RangingResultPeerDisconnected extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultPeerDisconnected(androidx.core.uwb.UwbDevice device, int reason); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public int getReason(); + property public androidx.core.uwb.UwbDevice device; + property public int reason; + } + + public static final class RangingResult.RangingResultPosition extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultPosition(androidx.core.uwb.UwbDevice device, androidx.core.uwb.RangingPosition position); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingPosition getPosition(); + property public androidx.core.uwb.UwbDevice device; + property public androidx.core.uwb.RangingPosition position; + } + + public final class UwbAddress { + ctor public UwbAddress(byte[] address); + ctor public UwbAddress(String address); + method @InaccessibleFromKotlin public byte[] getAddress(); + property public byte[] address; + field public static final androidx.core.uwb.UwbAddress.Companion Companion; + } + + public static final class UwbAddress.Companion { + } + + public interface UwbAvailabilityCallback { + method public void onUwbStateChanged(boolean isAvailable, int reason); + field public static final androidx.core.uwb.UwbAvailabilityCallback.Companion Companion; + field public static final int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR = 2; // 0x2 + field public static final int STATE_CHANGE_REASON_SYSTEM_POLICY = 1; // 0x1 + field public static final int STATE_CHANGE_REASON_UNKNOWN = 0; // 0x0 + } + + public static final class UwbAvailabilityCallback.Companion { + property public static int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR; + property public static int STATE_CHANGE_REASON_SYSTEM_POLICY; + property public static int STATE_CHANGE_REASON_UNKNOWN; + field public static final int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR = 2; // 0x2 + field public static final int STATE_CHANGE_REASON_SYSTEM_POLICY = 1; // 0x1 + field public static final int STATE_CHANGE_REASON_UNKNOWN = 0; // 0x0 + } + + public interface UwbClientSessionScope { + method @InaccessibleFromKotlin public androidx.core.uwb.UwbAddress getLocalAddress(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingCapabilities getRangingCapabilities(); + method public kotlinx.coroutines.flow.Flow prepareSession(androidx.core.uwb.RangingParameters parameters); + method public suspend Object? reconfigureRangeDataNtf(int configType, int proximityNear, int proximityFar, kotlin.coroutines.Continuation); + property public abstract androidx.core.uwb.UwbAddress localAddress; + property public abstract androidx.core.uwb.RangingCapabilities rangingCapabilities; + } + + public final class UwbComplexChannel { + ctor public UwbComplexChannel(int channel, int preambleIndex); + method @InaccessibleFromKotlin public int getChannel(); + method @InaccessibleFromKotlin public int getPreambleIndex(); + property public int channel; + property public int preambleIndex; + } + + public interface UwbControleeSessionScope extends androidx.core.uwb.UwbClientSessionScope { + } + + public interface UwbControllerSessionScope extends androidx.core.uwb.UwbClientSessionScope { + method public suspend Object? addControlee(androidx.core.uwb.UwbAddress address, androidx.core.uwb.RangingControleeParameters parameters, kotlin.coroutines.Continuation); + method public suspend Object? addControlee(androidx.core.uwb.UwbAddress address, kotlin.coroutines.Continuation); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbComplexChannel getUwbComplexChannel(); + method public suspend Object? reconfigureRangingInterval(int intervalSkipCount, kotlin.coroutines.Continuation); + method public suspend Object? removeControlee(androidx.core.uwb.UwbAddress address, kotlin.coroutines.Continuation); + property public abstract androidx.core.uwb.UwbComplexChannel uwbComplexChannel; + } + + public final class UwbDevice { + ctor public UwbDevice(androidx.core.uwb.UwbAddress address); + method public static androidx.core.uwb.UwbDevice createForAddress(byte[] address); + method public static androidx.core.uwb.UwbDevice createForAddress(String address); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbAddress getAddress(); + property public androidx.core.uwb.UwbAddress address; + field public static final androidx.core.uwb.UwbDevice.Companion Companion; + } + + public static final class UwbDevice.Companion { + method public androidx.core.uwb.UwbDevice createForAddress(byte[] address); + method public androidx.core.uwb.UwbDevice createForAddress(String address); + } + + @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager { + method public void clearUwbAvailabilityCallback(); + method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation); + method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation); + method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation); + method public static androidx.core.uwb.UwbManager createInstance(android.content.Context context); + method public suspend Object? isAvailable(kotlin.coroutines.Continuation); + method public void setUwbAvailabilityCallback(java.util.concurrent.Executor executor, androidx.core.uwb.UwbAvailabilityCallback observer); + field public static final androidx.core.uwb.UwbManager.Companion Companion; + } + + public static final class UwbManager.Companion { + method public androidx.core.uwb.UwbManager createInstance(android.content.Context context); + } + + public final class UwbRangeDataNtfConfig { + ctor public UwbRangeDataNtfConfig(int configType, int ntfProximityNearCm, int ntfProximityFarCm); + method @InaccessibleFromKotlin public int getConfigType(); + method @InaccessibleFromKotlin public int getNtfProximityFarCm(); + method @InaccessibleFromKotlin public int getNtfProximityNearCm(); + property public int configType; + property public int ntfProximityFarCm; + property public int ntfProximityNearCm; + } + +} + +package androidx.core.uwb.exceptions { + + public class UwbApiException extends java.lang.Exception { + ctor public UwbApiException(String message); + } + + public final class UwbHardwareNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbHardwareNotAvailableException(String message); + } + + public final class UwbServiceNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbServiceNotAvailableException(String message); + } + + public final class UwbSystemCallbackException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbSystemCallbackException(String message); + } + +} + diff --git a/core/uwb/uwb/api/res-1.0.0-beta01.txt b/core/uwb/uwb/api/res-1.0.0-beta01.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/core/uwb/uwb/api/restricted_1.0.0-beta01.txt b/core/uwb/uwb/api/restricted_1.0.0-beta01.txt new file mode 100644 index 0000000000000..be4d8820f1504 --- /dev/null +++ b/core/uwb/uwb/api/restricted_1.0.0-beta01.txt @@ -0,0 +1,280 @@ +// Signature format: 4.0 +package androidx.core.uwb { + + public final class RangingCapabilities { + method @InaccessibleFromKotlin public int getMinRangingInterval(); + method @InaccessibleFromKotlin public java.util.Set getSupportedChannels(); + method @InaccessibleFromKotlin public java.util.Set getSupportedConfigIds(); + method @InaccessibleFromKotlin public java.util.Set getSupportedNtfConfigs(); + method @InaccessibleFromKotlin public java.util.Set getSupportedRangingUpdateRates(); + method @InaccessibleFromKotlin public java.util.Set getSupportedSlotDurations(); + method @InaccessibleFromKotlin public boolean isAzimuthalAngleSupported(); + method @InaccessibleFromKotlin public boolean isBackgroundRangingSupported(); + method @InaccessibleFromKotlin public boolean isDistanceSupported(); + method @InaccessibleFromKotlin public boolean isElevationAngleSupported(); + method @InaccessibleFromKotlin public boolean isRangingIntervalReconfigureSupported(); + property public boolean isAzimuthalAngleSupported; + property public boolean isBackgroundRangingSupported; + property public boolean isDistanceSupported; + property public boolean isElevationAngleSupported; + property public boolean isRangingIntervalReconfigureSupported; + property public int minRangingInterval; + property public java.util.Set supportedChannels; + property public java.util.Set supportedConfigIds; + property public java.util.Set supportedNtfConfigs; + property public java.util.Set supportedRangingUpdateRates; + property public java.util.Set supportedSlotDurations; + } + + public final class RangingControleeParameters { + ctor public RangingControleeParameters(int subSessionId, optional byte[]? subSessionKey); + ctor @BytecodeOnly public RangingControleeParameters(int, byte[]!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + method @InaccessibleFromKotlin public int getSubSessionId(); + method @InaccessibleFromKotlin public byte[]? getSubSessionKey(); + property public int subSessionId; + property public byte[]? subSessionKey; + } + + public final class RangingMeasurement { + ctor public RangingMeasurement(float value); + method @InaccessibleFromKotlin public float getValue(); + property public float value; + } + + public final class RangingParameters { + ctor @BytecodeOnly public RangingParameters(int, int, int, byte[]!, byte[]!, androidx.core.uwb.UwbComplexChannel!, java.util.List!, int, androidx.core.uwb.UwbRangeDataNtfConfig!, long, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig, optional @IntRange(from=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_1_MILLIS, to=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_2_MILLIS) long slotDurationMillis); + ctor public RangingParameters(int uwbConfigType, int sessionId, int subSessionId, byte[]? sessionKeyInfo, byte[]? subSessionKeyInfo, androidx.core.uwb.UwbComplexChannel? complexChannel, java.util.List peerDevices, int updateRateType, optional androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig, optional @IntRange(from=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_1_MILLIS, to=androidx.core.uwb.RangingParameters.RANGING_SLOT_DURATION_2_MILLIS) long slotDurationMillis, optional boolean isAoaDisabled); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbComplexChannel? getComplexChannel(); + method @InaccessibleFromKotlin public java.util.List getPeerDevices(); + method @InaccessibleFromKotlin public int getSessionId(); + method @InaccessibleFromKotlin public byte[]? getSessionKeyInfo(); + method @InaccessibleFromKotlin public long getSlotDurationMillis(); + method @InaccessibleFromKotlin public int getSubSessionId(); + method @InaccessibleFromKotlin public byte[]? getSubSessionKeyInfo(); + method @InaccessibleFromKotlin public int getUpdateRateType(); + method @InaccessibleFromKotlin public int getUwbConfigType(); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbRangeDataNtfConfig? getUwbRangeDataNtfConfig(); + method @InaccessibleFromKotlin public boolean isAoaDisabled(); + property public androidx.core.uwb.UwbComplexChannel? complexChannel; + property public boolean isAoaDisabled; + property public java.util.List peerDevices; + property public int sessionId; + property public byte[]? sessionKeyInfo; + property public long slotDurationMillis; + property public int subSessionId; + property public byte[]? subSessionKeyInfo; + property public int updateRateType; + property public int uwbConfigType; + property public androidx.core.uwb.UwbRangeDataNtfConfig? uwbRangeDataNtfConfig; + field public static final int CONFIG_MULTICAST_DS_TWR = 2; // 0x2 + field public static final int CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR = 7; // 0x7 + field public static final int CONFIG_PROVISIONED_MULTICAST_DS_TWR = 5; // 0x5 + field public static final int CONFIG_PROVISIONED_UNICAST_DS_TWR = 4; // 0x4 + field public static final int CONFIG_UNICAST_DS_TWR = 1; // 0x1 + field public static final androidx.core.uwb.RangingParameters.Companion Companion; + field public static final long RANGING_SLOT_DURATION_1_MILLIS = 1L; // 0x1L + field public static final long RANGING_SLOT_DURATION_2_MILLIS = 2L; // 0x2L + field public static final int RANGING_UPDATE_RATE_AUTOMATIC = 1; // 0x1 + field public static final int RANGING_UPDATE_RATE_FREQUENT = 3; // 0x3 + field public static final int RANGING_UPDATE_RATE_INFREQUENT = 2; // 0x2 + } + + public static final class RangingParameters.Companion { + property public static int CONFIG_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_INDIVIDUAL_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_MULTICAST_DS_TWR; + property public static int CONFIG_PROVISIONED_UNICAST_DS_TWR; + property public static int CONFIG_UNICAST_DS_TWR; + property public static long RANGING_SLOT_DURATION_1_MILLIS; + property public static long RANGING_SLOT_DURATION_2_MILLIS; + property public static int RANGING_UPDATE_RATE_AUTOMATIC; + property public static int RANGING_UPDATE_RATE_FREQUENT; + property public static int RANGING_UPDATE_RATE_INFREQUENT; + } + + public final class RangingPosition { + ctor public RangingPosition(androidx.core.uwb.RangingMeasurement? distance, androidx.core.uwb.RangingMeasurement? azimuth, androidx.core.uwb.RangingMeasurement? elevation, long elapsedRealtimeNanos); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getAzimuth(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getDistance(); + method @InaccessibleFromKotlin public long getElapsedRealtimeNanos(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingMeasurement? getElevation(); + property public androidx.core.uwb.RangingMeasurement? azimuth; + property public androidx.core.uwb.RangingMeasurement? distance; + property public long elapsedRealtimeNanos; + property public androidx.core.uwb.RangingMeasurement? elevation; + } + + public abstract sealed exhaustive class RangingResult { + method @InaccessibleFromKotlin public abstract androidx.core.uwb.UwbDevice getDevice(); + property public abstract androidx.core.uwb.UwbDevice device; + field public static final androidx.core.uwb.RangingResult.Companion Companion; + field public static final int RANGING_FAILURE_REASON_BAD_PARAMETERS = 1; // 0x1 + field public static final int RANGING_FAILURE_REASON_FAILED_TO_START = 2; // 0x2 + field public static final int RANGING_FAILURE_REASON_MAX_RR_RETRY_REACHED = 6; // 0x6 + field public static final int RANGING_FAILURE_REASON_STOPPED_BY_LOCAL = 4; // 0x4 + field public static final int RANGING_FAILURE_REASON_STOPPED_BY_PEER = 3; // 0x3 + field public static final int RANGING_FAILURE_REASON_SYSTEM_POLICY = 5; // 0x5 + field public static final int RANGING_FAILURE_REASON_UNKNOWN = 0; // 0x0 + } + + public static final class RangingResult.Companion { + property public static int RANGING_FAILURE_REASON_BAD_PARAMETERS; + property public static int RANGING_FAILURE_REASON_FAILED_TO_START; + property public static int RANGING_FAILURE_REASON_MAX_RR_RETRY_REACHED; + property public static int RANGING_FAILURE_REASON_STOPPED_BY_LOCAL; + property public static int RANGING_FAILURE_REASON_STOPPED_BY_PEER; + property public static int RANGING_FAILURE_REASON_SYSTEM_POLICY; + property public static int RANGING_FAILURE_REASON_UNKNOWN; + } + + public static final class RangingResult.RangingResultFailure extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultFailure(androidx.core.uwb.UwbDevice device, int reason); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public int getReason(); + property public androidx.core.uwb.UwbDevice device; + property public int reason; + } + + public static final class RangingResult.RangingResultInitialized extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultInitialized(androidx.core.uwb.UwbDevice device); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + property public androidx.core.uwb.UwbDevice device; + } + + public static final class RangingResult.RangingResultPeerDisconnected extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultPeerDisconnected(androidx.core.uwb.UwbDevice device, int reason); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public int getReason(); + property public androidx.core.uwb.UwbDevice device; + property public int reason; + } + + public static final class RangingResult.RangingResultPosition extends androidx.core.uwb.RangingResult { + ctor public RangingResult.RangingResultPosition(androidx.core.uwb.UwbDevice device, androidx.core.uwb.RangingPosition position); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbDevice getDevice(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingPosition getPosition(); + property public androidx.core.uwb.UwbDevice device; + property public androidx.core.uwb.RangingPosition position; + } + + public final class UwbAddress { + ctor public UwbAddress(byte[] address); + ctor public UwbAddress(String address); + method @InaccessibleFromKotlin public byte[] getAddress(); + property public byte[] address; + field public static final androidx.core.uwb.UwbAddress.Companion Companion; + } + + public static final class UwbAddress.Companion { + } + + public interface UwbAvailabilityCallback { + method public void onUwbStateChanged(boolean isAvailable, int reason); + field public static final androidx.core.uwb.UwbAvailabilityCallback.Companion Companion; + field public static final int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR = 2; // 0x2 + field public static final int STATE_CHANGE_REASON_SYSTEM_POLICY = 1; // 0x1 + field public static final int STATE_CHANGE_REASON_UNKNOWN = 0; // 0x0 + } + + public static final class UwbAvailabilityCallback.Companion { + property public static int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR; + property public static int STATE_CHANGE_REASON_SYSTEM_POLICY; + property public static int STATE_CHANGE_REASON_UNKNOWN; + field public static final int STATE_CHANGE_REASON_COUNTRY_CODE_ERROR = 2; // 0x2 + field public static final int STATE_CHANGE_REASON_SYSTEM_POLICY = 1; // 0x1 + field public static final int STATE_CHANGE_REASON_UNKNOWN = 0; // 0x0 + } + + public interface UwbClientSessionScope { + method @InaccessibleFromKotlin public androidx.core.uwb.UwbAddress getLocalAddress(); + method @InaccessibleFromKotlin public androidx.core.uwb.RangingCapabilities getRangingCapabilities(); + method public kotlinx.coroutines.flow.Flow prepareSession(androidx.core.uwb.RangingParameters parameters); + method public suspend Object? reconfigureRangeDataNtf(int configType, int proximityNear, int proximityFar, kotlin.coroutines.Continuation); + property public abstract androidx.core.uwb.UwbAddress localAddress; + property public abstract androidx.core.uwb.RangingCapabilities rangingCapabilities; + } + + public final class UwbComplexChannel { + ctor public UwbComplexChannel(int channel, int preambleIndex); + method @InaccessibleFromKotlin public int getChannel(); + method @InaccessibleFromKotlin public int getPreambleIndex(); + property public int channel; + property public int preambleIndex; + } + + public interface UwbControleeSessionScope extends androidx.core.uwb.UwbClientSessionScope { + } + + public interface UwbControllerSessionScope extends androidx.core.uwb.UwbClientSessionScope { + method public suspend Object? addControlee(androidx.core.uwb.UwbAddress address, androidx.core.uwb.RangingControleeParameters parameters, kotlin.coroutines.Continuation); + method public suspend Object? addControlee(androidx.core.uwb.UwbAddress address, kotlin.coroutines.Continuation); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbComplexChannel getUwbComplexChannel(); + method public suspend Object? reconfigureRangingInterval(int intervalSkipCount, kotlin.coroutines.Continuation); + method public suspend Object? removeControlee(androidx.core.uwb.UwbAddress address, kotlin.coroutines.Continuation); + property public abstract androidx.core.uwb.UwbComplexChannel uwbComplexChannel; + } + + public final class UwbDevice { + ctor public UwbDevice(androidx.core.uwb.UwbAddress address); + method public static androidx.core.uwb.UwbDevice createForAddress(byte[] address); + method public static androidx.core.uwb.UwbDevice createForAddress(String address); + method @InaccessibleFromKotlin public androidx.core.uwb.UwbAddress getAddress(); + property public androidx.core.uwb.UwbAddress address; + field public static final androidx.core.uwb.UwbDevice.Companion Companion; + } + + public static final class UwbDevice.Companion { + method public androidx.core.uwb.UwbDevice createForAddress(byte[] address); + method public androidx.core.uwb.UwbDevice createForAddress(String address); + } + + @kotlin.jvm.JvmDefaultWithCompatibility public interface UwbManager { + method public void clearUwbAvailabilityCallback(); + method @Deprecated public suspend Object? clientSessionScope(kotlin.coroutines.Continuation); + method public suspend Object? controleeSessionScope(kotlin.coroutines.Continuation); + method public suspend Object? controllerSessionScope(kotlin.coroutines.Continuation); + method public static androidx.core.uwb.UwbManager createInstance(android.content.Context context); + method public suspend Object? isAvailable(kotlin.coroutines.Continuation); + method public void setUwbAvailabilityCallback(java.util.concurrent.Executor executor, androidx.core.uwb.UwbAvailabilityCallback observer); + field public static final androidx.core.uwb.UwbManager.Companion Companion; + } + + public static final class UwbManager.Companion { + method public androidx.core.uwb.UwbManager createInstance(android.content.Context context); + } + + public final class UwbRangeDataNtfConfig { + ctor public UwbRangeDataNtfConfig(int configType, int ntfProximityNearCm, int ntfProximityFarCm); + method @InaccessibleFromKotlin public int getConfigType(); + method @InaccessibleFromKotlin public int getNtfProximityFarCm(); + method @InaccessibleFromKotlin public int getNtfProximityNearCm(); + property public int configType; + property public int ntfProximityFarCm; + property public int ntfProximityNearCm; + } + +} + +package androidx.core.uwb.exceptions { + + public class UwbApiException extends java.lang.Exception { + ctor public UwbApiException(String message); + } + + public final class UwbHardwareNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbHardwareNotAvailableException(String message); + } + + public final class UwbServiceNotAvailableException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbServiceNotAvailableException(String message); + } + + public final class UwbSystemCallbackException extends androidx.core.uwb.exceptions.UwbApiException { + ctor public UwbSystemCallbackException(String message); + } + +} + diff --git a/libraryversions.toml b/libraryversions.toml index b72a8502a1534..84c879c0be247 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -49,7 +49,7 @@ CORE_REMOTEVIEWS = "1.1.0-rc01" CORE_ROLE = "1.2.0-alpha01" CORE_SPLASHSCREEN = "1.2.0" CORE_TELECOM = "1.1.0-alpha03" -CORE_UWB = "1.0.0-alpha11" +CORE_UWB = "1.0.0-beta01" CORE_VIEWTREE = "1.1.0-alpha01" CREDENTIALS = "1.6.0-rc01" CREDENTIALS_E2EE_QUARANTINE = "1.0.0-alpha02" From 6e8fc9fc851a273e68b7c03c0849147aa8fc1096 Mon Sep 17 00:00:00 2001 From: Julia McClellan Date: Tue, 24 Feb 2026 16:13:02 -0500 Subject: [PATCH 10/16] Temporarily disable androidx_with_metalava To pull in the fix for b/487195353. Test: presubmit Change-Id: I30f3fb0c86069756e9833c6f656dfa39e9f70915 --- busytown/androidx_with_metalava.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/busytown/androidx_with_metalava.sh b/busytown/androidx_with_metalava.sh index fa039819e1f56..98623e8d43e5a 100755 --- a/busytown/androidx_with_metalava.sh +++ b/busytown/androidx_with_metalava.sh @@ -4,7 +4,7 @@ SCRIPT_PATH="$(cd $(dirname $0) && pwd)" # Use this flag to temporarily disable `checkApi` # while landing Metalava w/ breaking API changes -METALAVA_INTEGRATION_ENFORCED=true +METALAVA_INTEGRATION_ENFORCED=false # The default targets to build if no arguments # are provided on the command line. From acca02ccee86430cf8c0812e918aeabb10dd803f Mon Sep 17 00:00:00 2001 From: Paul Rohde Date: Fri, 6 Feb 2026 14:25:08 -0800 Subject: [PATCH 11/16] Add DiscreteRotation and DiscreteRotationMath This change adds utility functions for working with "discrete" rotation values of 0, 90, 180, 270. These utilities greatly simplify and reduce the likelihood of introducing errors when computing rotations or transforms when working with primitive camera types. For Kotlin, this change also introduces a DiscreteRotation value class with dedicated add, subtract, and rounding functions. Change-Id: Ib1ad876b540152a14674bac9bb97b99cf7009fee --- camera/camera-common/api/current.txt | 39 +++ .../camera-common/api/restricted_current.txt | 41 ++++ .../camera/common/DiscreteRotation.kt | 74 ++++++ .../camera/common/DiscreteRotationMath.kt | 85 +++++++ .../camera/common/DiscreteRotationMathTest.kt | 171 ++++++++++++++ .../camera/common/DiscreteRotationTest.kt | 222 ++++++++++++++++++ 6 files changed, 632 insertions(+) create mode 100644 camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotation.kt create mode 100644 camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotationMath.kt create mode 100644 camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationMathTest.kt create mode 100644 camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationTest.kt diff --git a/camera/camera-common/api/current.txt b/camera/camera-common/api/current.txt index e6f50d0d0fd11..3b1e1a1ea740e 100644 --- a/camera/camera-common/api/current.txt +++ b/camera/camera-common/api/current.txt @@ -1 +1,40 @@ // Signature format: 4.0 +package androidx.camera.common { + + @kotlin.jvm.JvmInline public final value class DiscreteRotation { + method @BytecodeOnly public static androidx.camera.common.DiscreteRotation! box-impl(int); + method @InaccessibleFromKotlin public int getDegrees(); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation minus(androidx.camera.common.DiscreteRotation other); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation minus(int degrees); + method @BytecodeOnly public static int minus-4Qa-4Hw(int, int); + method @BytecodeOnly public static int minus-REjC-m4(int, int); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation plus(androidx.camera.common.DiscreteRotation other); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation plus(int degrees); + method @BytecodeOnly public static int plus-4Qa-4Hw(int, int); + method @BytecodeOnly public static int plus-REjC-m4(int, int); + method @BytecodeOnly public int unbox-impl(); + property public int degrees; + field public static final androidx.camera.common.DiscreteRotation.Companion Companion; + } + + public static final class DiscreteRotation.Companion { + method @KotlinOnly public androidx.camera.common.DiscreteRotation from(int degrees); + method @BytecodeOnly public int from-REjC-m4(int); + method @KotlinOnly public androidx.camera.common.DiscreteRotation fromSurfaceRotation(int surfaceRotation); + method @BytecodeOnly public int fromSurfaceRotation-REjC-m4(int); + method @KotlinOnly public androidx.camera.common.DiscreteRotation round(float degrees); + method @KotlinOnly public androidx.camera.common.DiscreteRotation round(int degrees); + method @BytecodeOnly public int round-REjC-m4(float); + method @BytecodeOnly public int round-REjC-m4(int); + } + + public final class DiscreteRotationMath { + method public static int fromSurfaceRotation(int surfaceRotation); + method public static void requireDiscreteRotation(int degrees); + method public static int round(float degrees); + method public static int round(int degrees); + field public static final androidx.camera.common.DiscreteRotationMath INSTANCE; + } + +} + diff --git a/camera/camera-common/api/restricted_current.txt b/camera/camera-common/api/restricted_current.txt index e6f50d0d0fd11..20295d169fe25 100644 --- a/camera/camera-common/api/restricted_current.txt +++ b/camera/camera-common/api/restricted_current.txt @@ -1 +1,42 @@ // Signature format: 4.0 +package androidx.camera.common { + + @kotlin.jvm.JvmInline public final value class DiscreteRotation { + ctor @KotlinOnly @kotlin.PublishedApi internal DiscreteRotation(int degrees); + method @BytecodeOnly public static androidx.camera.common.DiscreteRotation! box-impl(int); + method @BytecodeOnly @kotlin.PublishedApi internal static int constructor-impl(int); + method @InaccessibleFromKotlin public int getDegrees(); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation minus(androidx.camera.common.DiscreteRotation other); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation minus(int degrees); + method @BytecodeOnly public static int minus-4Qa-4Hw(int, int); + method @BytecodeOnly public static int minus-REjC-m4(int, int); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation plus(androidx.camera.common.DiscreteRotation other); + method @KotlinOnly public inline operator androidx.camera.common.DiscreteRotation plus(int degrees); + method @BytecodeOnly public static int plus-4Qa-4Hw(int, int); + method @BytecodeOnly public static int plus-REjC-m4(int, int); + method @BytecodeOnly public int unbox-impl(); + property public int degrees; + field public static final androidx.camera.common.DiscreteRotation.Companion Companion; + } + + public static final class DiscreteRotation.Companion { + method @KotlinOnly public androidx.camera.common.DiscreteRotation from(int degrees); + method @BytecodeOnly public int from-REjC-m4(int); + method @KotlinOnly public androidx.camera.common.DiscreteRotation fromSurfaceRotation(int surfaceRotation); + method @BytecodeOnly public int fromSurfaceRotation-REjC-m4(int); + method @KotlinOnly public androidx.camera.common.DiscreteRotation round(float degrees); + method @KotlinOnly public androidx.camera.common.DiscreteRotation round(int degrees); + method @BytecodeOnly public int round-REjC-m4(float); + method @BytecodeOnly public int round-REjC-m4(int); + } + + public final class DiscreteRotationMath { + method public static int fromSurfaceRotation(int surfaceRotation); + method public static void requireDiscreteRotation(int degrees); + method public static int round(float degrees); + method public static int round(int degrees); + field public static final androidx.camera.common.DiscreteRotationMath INSTANCE; + } + +} + diff --git a/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotation.kt b/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotation.kt new file mode 100644 index 0000000000000..f7f60450e1572 --- /dev/null +++ b/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotation.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2026 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.camera.common + +import android.view.Surface + +/** + * Kotlin value class that represents fixed 0, 90, 180, 270 degree rotations with utility functions + * for adding and subtracting discrete rotations from each other. + * + * A [DiscreteRotation] represents integer degrees in fixed 90 degree increments. + */ +@JvmInline +@Suppress("NOTHING_TO_INLINE", "ValueClassDefinition") +public value class DiscreteRotation @PublishedApi internal constructor(public val degrees: Int) { + /** Add a [DiscreteRotation] from this, modding the result by 360. */ + @Suppress("ValueClassUsageWithoutJvmName") + public inline operator fun plus(other: DiscreteRotation): DiscreteRotation = + DiscreteRotation((this.degrees + other.degrees) % 360) + + /** Add a [DiscreteRotation] from this, modding the result by 360. */ + @Suppress("ValueClassUsageWithoutJvmName") + public inline operator fun plus(degrees: Int): DiscreteRotation = this.plus(from(degrees)) + + /** Subtract a [DiscreteRotation] from this, modding the result by 360. */ + @Suppress("ValueClassUsageWithoutJvmName") + public inline operator fun minus(other: DiscreteRotation): DiscreteRotation = + DiscreteRotation((this.degrees - other.degrees + 360) % 360) + + /** Subtract a [DiscreteRotation] from this, modding the result by 360. */ + @Suppress("ValueClassUsageWithoutJvmName") + public inline operator fun minus(degrees: Int): DiscreteRotation = this.minus(from(degrees)) + + override fun toString(): String = "$degrees°" + + public companion object { + + /** Convert integer [degrees] to a [DiscreteRotation] */ + @Suppress("ValueClassUsageWithoutJvmName", "MissingJvmstatic") + public fun from(degrees: Int): DiscreteRotation { + DiscreteRotationMath.requireDiscreteRotation(degrees) + return DiscreteRotation(degrees) + } + + /** Round integer [degrees] to a [DiscreteRotation]. */ + @Suppress("ValueClassUsageWithoutJvmName", "MissingJvmstatic") + public fun round(degrees: Int): DiscreteRotation = + DiscreteRotation(DiscreteRotationMath.round(degrees)) + + /** Round floating point [degrees] to a [DiscreteRotation]. */ + @Suppress("ValueClassUsageWithoutJvmName", "MissingJvmstatic") + public fun round(degrees: Float): DiscreteRotation = + DiscreteRotation(DiscreteRotationMath.round(degrees)) + + /** Get a [DiscreteRotation] from [Surface] rotation values. */ + @Suppress("ValueClassUsageWithoutJvmName", "MissingJvmstatic") + public fun fromSurfaceRotation(surfaceRotation: Int): DiscreteRotation = + DiscreteRotation(DiscreteRotationMath.fromSurfaceRotation(surfaceRotation)) + } +} diff --git a/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotationMath.kt b/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotationMath.kt new file mode 100644 index 0000000000000..288f1c4ba30a9 --- /dev/null +++ b/camera/camera-common/src/main/kotlin/androidx/camera/common/DiscreteRotationMath.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 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.camera.common + +import android.view.Surface + +/** Utility functions for working with discrete rotations values [0, 90, 180, 270]. */ +public object DiscreteRotationMath { + /** + * Throws an [IllegalArgumentException] if the given [degrees] is not one of [0, 90, 180, 270]. + */ + @JvmStatic + public fun requireDiscreteRotation(degrees: Int) { + require(degrees == 0 || degrees == 90 || degrees == 180 || degrees == 270) { + "Unexpected rotation: $degrees. Value must be one of 0, 90, 180, 270" + } + } + + /** + * Round [degrees] to the nearest discrete rotation (0, 90, 180, 270). Negative values are + * rounded to the nearest positive discrete rotation value. + * + * Example(s): + * - `40 => 0°` + * - `50 => 90°` + * - `-40 => 0°` (Equivalent to -40 + 360 => round(320) => 0) + * - `-50 => 270°` (Equivalent to -50 + 360 => round(310) => 270°) + */ + @JvmStatic + public fun round(degrees: Int): Int { + // 1. Given an integer (positive or negative) constrain it to -359...359 using % 360 + // 2. Offset the value to a positive range by adding 360. + // 3. When rounding to a multiple of 90 we use integer division to discard the remainder, + // which effectively rounds down for positive integers. Adding 45 causes this to round to + // the nearest discrete value. + // 4. Multiply by 90 to convert the value back to degrees. + // 5. % 360, giving a one of 0, 90, 180, 270 + return ((degrees % 360 + (360 + 45)) / 90) * 90 % 360 + } + + /** + * Round [degrees] to the nearest discrete rotation (0, 90, 180, 270). Negative values are + * rounded to the nearest positive discrete rotation value. + * + * Example(s): + * - `40.000f => 0°` + * - `44.990f => 0°` + * - `45.001f => 90°` + * - `-40.00f° => 0°` (Equivalent to -40.000f + 360 => round(320) => 0) + * - `-50.00f° => 270°` (Equivalent to -50.000f + 360 => round(310) => 270) + */ + @JvmStatic + public fun round(degrees: Float): Int { + // 1. Constrain to -360 < d < 360 using % 360 + // 2. Divide d by 90 and round giving an integer value between -4 < d < 4 + // 3. Multiply by 90, and add 360 to convert to a positive integer degree range. + // 4. % 360, giving a one of 0, 90, 180, 270 + return (Math.round(degrees % 360 / 90) * 90 + 360) % 360 + } + + /** Get a [DiscreteRotation] from [Surface] rotation values. */ + @JvmStatic + public fun fromSurfaceRotation(surfaceRotation: Int): Int = + when (surfaceRotation) { + Surface.ROTATION_0 -> 0 + Surface.ROTATION_90 -> 90 + Surface.ROTATION_180 -> 180 + Surface.ROTATION_270 -> 270 + else -> throw IllegalArgumentException("Unexpected Surface rotation $surfaceRotation!") + } +} diff --git a/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationMathTest.kt b/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationMathTest.kt new file mode 100644 index 0000000000000..63d2b47508371 --- /dev/null +++ b/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationMathTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2026 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.camera.common + +import android.view.Surface +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class DiscreteRotationMathTest { + + @Test + fun discreteRotationCanRoundIntDegrees() { + // Standard degrees should map exactly to their corresponding values. + assertThat(DiscreteRotationMath.round(0)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(90)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(180)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(270)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(360)).isEqualTo(0) + + // Negative (CCW) + assertThat(DiscreteRotationMath.round(-90)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-180)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-270)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-360)).isEqualTo(0) + + // 15 degrees off + assertThat(DiscreteRotationMath.round(15)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(105)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(195)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(285)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(375)).isEqualTo(0) + + // -15 degrees off + assertThat(DiscreteRotationMath.round(-15)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(75)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(165)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(255)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(345)).isEqualTo(0) + + // +44 degree increments + assertThat(DiscreteRotationMath.round(44)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(134)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(224)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(314)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(404)).isEqualTo(0) + + // +45 degree increments + assertThat(DiscreteRotationMath.round(45)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(135)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(225)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(315)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(405)).isEqualTo(90) + + // -45 degree increments + assertThat(DiscreteRotationMath.round(-45)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-135)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-225)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-315)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-405)).isEqualTo(0) + + // -46 degree increments + assertThat(DiscreteRotationMath.round(-46)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-136)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-226)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-316)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-406)).isEqualTo(270) + } + + @Test + fun discreteRotationCanRoundFloatDegrees() { + // Standard degrees should map exactly to their corresponding values. + assertThat(DiscreteRotationMath.round(0f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(90f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(180f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(270f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(360f)).isEqualTo(0) + + // Negative (CCW) + assertThat(DiscreteRotationMath.round(-90f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-180f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-270f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-360f)).isEqualTo(0) + + // 15 degrees off + assertThat(DiscreteRotationMath.round(15f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(105f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(195f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(285f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(375f)).isEqualTo(0) + + // -15 degrees off + assertThat(DiscreteRotationMath.round(-15f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(75f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(165f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(255f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(345f)).isEqualTo(0) + + // +44 degree increments + assertThat(DiscreteRotationMath.round(44f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(134f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(224f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(314f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(404f)).isEqualTo(0) + + // +45 degree increments + assertThat(DiscreteRotationMath.round(45f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(135f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(225f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(315f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(405f)).isEqualTo(90) + + // -45 degree increments + assertThat(DiscreteRotationMath.round(-45f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-135f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-225f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-315f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-405f)).isEqualTo(0) + + // -46 degree increments + assertThat(DiscreteRotationMath.round(-46f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-136f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-226f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-316f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-406f)).isEqualTo(270) + + // Floating point specific rounding tests + assertThat(DiscreteRotationMath.round(44.9999f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(45.0001f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(134.9999f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(135.0001f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(224.9999f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(225.0001f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(314.9999f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(315.0001f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(404.9999f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(405.0001f)).isEqualTo(90) + + assertThat(DiscreteRotationMath.round(-44.9999f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-45.0001f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-134.9999f)).isEqualTo(270) + assertThat(DiscreteRotationMath.round(-135.0001f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-224.9999f)).isEqualTo(180) + assertThat(DiscreteRotationMath.round(-225.0001f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-314.9999f)).isEqualTo(90) + assertThat(DiscreteRotationMath.round(-315.0001f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-404.9999f)).isEqualTo(0) + assertThat(DiscreteRotationMath.round(-405.0001f)).isEqualTo(270) + } + + @Test + fun discreteRotationCanBeCreatedFromSurfaceRotation() { + assertThat(DiscreteRotationMath.fromSurfaceRotation(Surface.ROTATION_0)).isEqualTo(0) + assertThat(DiscreteRotationMath.fromSurfaceRotation(Surface.ROTATION_90)).isEqualTo(90) + assertThat(DiscreteRotationMath.fromSurfaceRotation(Surface.ROTATION_180)).isEqualTo(180) + assertThat(DiscreteRotationMath.fromSurfaceRotation(Surface.ROTATION_270)).isEqualTo(270) + } +} diff --git a/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationTest.kt b/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationTest.kt new file mode 100644 index 0000000000000..6e1b424de217e --- /dev/null +++ b/camera/camera-common/src/test/kotlin/androidx/camera/common/DiscreteRotationTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2026 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.camera.common + +import android.view.Surface +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class DiscreteRotationTests { + private val ROTATION_0 = DiscreteRotation.from(0) + private val ROTATION_90 = DiscreteRotation.from(90) + private val ROTATION_180 = DiscreteRotation.from(180) + private val ROTATION_270 = DiscreteRotation.from(270) + + @Test + fun discreteRotationCanRoundIntDegrees() { + // Standard degrees should map exactly to their corresponding values. + assertThat(DiscreteRotation.round(0).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(90).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(180).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(270).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(360).degrees).isEqualTo(0) + + // Negative (CCW) + assertThat(DiscreteRotation.round(-90).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-180).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-270).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-360).degrees).isEqualTo(0) + + // 15 degrees off + assertThat(DiscreteRotation.round(15).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(105).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(195).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(285).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(375).degrees).isEqualTo(0) + + // -15 degrees off + assertThat(DiscreteRotation.round(-15).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(75).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(165).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(255).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(345).degrees).isEqualTo(0) + + // +44 degree increments + assertThat(DiscreteRotation.round(44).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(134).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(224).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(314).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(404).degrees).isEqualTo(0) + + // +45 degree increments + assertThat(DiscreteRotation.round(45).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(135).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(225).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(315).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(405).degrees).isEqualTo(90) + + // -45 degree increments + assertThat(DiscreteRotation.round(-45).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-135).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-225).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-315).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-405).degrees).isEqualTo(0) + + // -46 degree increments + assertThat(DiscreteRotation.round(-46).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-136).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-226).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-316).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-406).degrees).isEqualTo(270) + } + + @Test + fun discreteRotationCanRoundFloatDegrees() { + // Standard degrees should map exactly to their corresponding values. + assertThat(DiscreteRotation.round(0f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(90f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(180f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(270f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(360f).degrees).isEqualTo(0) + + // Negative (CCW) + assertThat(DiscreteRotation.round(-90f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-180f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-270f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-360f).degrees).isEqualTo(0) + + // 15 degrees off + assertThat(DiscreteRotation.round(15f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(105f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(195f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(285f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(375f).degrees).isEqualTo(0) + + // -15 degrees off + assertThat(DiscreteRotation.round(-15f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(75f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(165f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(255f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(345f).degrees).isEqualTo(0) + + // +44 degree increments + assertThat(DiscreteRotation.round(44f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(134f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(224f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(314f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(404f).degrees).isEqualTo(0) + + // +45 degree increments + assertThat(DiscreteRotation.round(45f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(135f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(225f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(315f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(405f).degrees).isEqualTo(90) + + // -45 degree increments + assertThat(DiscreteRotation.round(-45f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-135f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-225f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-315f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-405f).degrees).isEqualTo(0) + + // -46 degree increments + assertThat(DiscreteRotation.round(-46f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-136f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-226f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-316f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-406f).degrees).isEqualTo(270) + + // Floating point specific rounding tests + assertThat(DiscreteRotation.round(44.9999f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(45.0001f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(134.9999f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(135.0001f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(224.9999f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(225.0001f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(314.9999f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(315.0001f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(404.9999f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(405.0001f).degrees).isEqualTo(90) + + assertThat(DiscreteRotation.round(-44.9999f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-45.0001f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-134.9999f).degrees).isEqualTo(270) + assertThat(DiscreteRotation.round(-135.0001f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-224.9999f).degrees).isEqualTo(180) + assertThat(DiscreteRotation.round(-225.0001f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-314.9999f).degrees).isEqualTo(90) + assertThat(DiscreteRotation.round(-315.0001f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-404.9999f).degrees).isEqualTo(0) + assertThat(DiscreteRotation.round(-405.0001f).degrees).isEqualTo(270) + } + + @Test + fun discreteRotationCanBeCreatedFromSurfaceRotation() { + assertThat(DiscreteRotation.fromSurfaceRotation(Surface.ROTATION_0)) + .isEqualTo(DiscreteRotation.from(0)) + assertThat(DiscreteRotation.fromSurfaceRotation(Surface.ROTATION_90)) + .isEqualTo(DiscreteRotation.from(90)) + assertThat(DiscreteRotation.fromSurfaceRotation(Surface.ROTATION_180)) + .isEqualTo(DiscreteRotation.from(180)) + assertThat(DiscreteRotation.fromSurfaceRotation(Surface.ROTATION_270)) + .isEqualTo(DiscreteRotation.from(270)) + } + + @Test(expected = IllegalArgumentException::class) + fun discreteRotationThrowsOnInvalidSurfaceRotation() { + DiscreteRotation.fromSurfaceRotation(42) + } + + @Test + fun discreteRotationCanBeAdded() { + assertThat(ROTATION_0 + ROTATION_90).isEqualTo(ROTATION_90) + assertThat(ROTATION_90 + ROTATION_90).isEqualTo(ROTATION_180) + assertThat(ROTATION_180 + ROTATION_90).isEqualTo(ROTATION_270) + assertThat(ROTATION_270 + ROTATION_90).isEqualTo(ROTATION_0) + + assertThat(ROTATION_0 + 90).isEqualTo(ROTATION_90) + assertThat(ROTATION_90 + 90).isEqualTo(ROTATION_180) + } + + @Test + fun discreteRotationCanBeSubtracted() { + assertThat(ROTATION_90 - ROTATION_90).isEqualTo(ROTATION_0) + assertThat(ROTATION_180 - ROTATION_90).isEqualTo(ROTATION_90) + assertThat(ROTATION_270 - ROTATION_90).isEqualTo(ROTATION_180) + assertThat(ROTATION_0 - ROTATION_90).isEqualTo(ROTATION_270) + + assertThat(ROTATION_90 - 90).isEqualTo(ROTATION_0) + assertThat(ROTATION_0 - 90).isEqualTo(ROTATION_270) + } + + @Test(expected = IllegalArgumentException::class) + fun discreteRotationThrowsOnInvalidInt() { + DiscreteRotation.from(45) + } + + @Test + fun discreteRotationHasToString() { + assertThat(ROTATION_0.toString()).isEqualTo("0°") + assertThat(ROTATION_90.toString()).isEqualTo("90°") + assertThat(ROTATION_180.toString()).isEqualTo("180°") + assertThat(ROTATION_270.toString()).isEqualTo("270°") + } +} From 88556a317ab6a4776c9fbdf6ceaf1459ff838cde Mon Sep 17 00:00:00 2001 From: Aurimas Liutikas Date: Tue, 24 Feb 2026 14:21:38 -0800 Subject: [PATCH 12/16] Correctly set compileSdk for various projects AGP now enforces that minorApiLevel and sdkExtension are set correctly when your dependency requires that value. This check was added in ag/38072823 and caused androidx-studio-integration branch to fail. This fixes these failing projects. Test: ./gradlew assembleAndroidTest Change-Id: I3b84253d9e7ae51e5a3a3d613c3f95a52df5e6cb Merged-In: I3b84253d9e7ae51e5a3a3d613c3f95a52df5e6cb --- core/core-ktx/build.gradle | 2 +- core/core-testing/build.gradle | 2 +- core/core/samples/build.gradle | 2 +- datastore/integration-tests/macrobenchmark-target/build.gradle | 2 +- health/connect/connect-client/samples/build.gradle | 2 +- health/connect/connect-testing/build.gradle | 2 +- health/connect/connect-testing/samples/build.gradle | 2 +- lint-checks/integration-tests/build.gradle | 2 +- samples/AndroidXDemos/build.gradle | 2 +- samples/Support4Demos/build.gradle | 2 +- samples/SupportWearDemos/build.gradle | 2 +- test/uiautomator/integration-tests/testapp/build.gradle | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/core-ktx/build.gradle b/core/core-ktx/build.gradle index e724692ba951e..8e6839cde68ab 100644 --- a/core/core-ktx/build.gradle +++ b/core/core-ktx/build.gradle @@ -50,6 +50,6 @@ androidx { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.core.ktx" } diff --git a/core/core-testing/build.gradle b/core/core-testing/build.gradle index 05c894d18738e..e232c51a109f3 100644 --- a/core/core-testing/build.gradle +++ b/core/core-testing/build.gradle @@ -41,7 +41,7 @@ dependencies { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.core.testing" } diff --git a/core/core/samples/build.gradle b/core/core/samples/build.gradle index 39e8a1cc5f8c2..d1b411cfee2de 100644 --- a/core/core/samples/build.gradle +++ b/core/core/samples/build.gradle @@ -29,7 +29,7 @@ dependencies { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.core.samples" } diff --git a/datastore/integration-tests/macrobenchmark-target/build.gradle b/datastore/integration-tests/macrobenchmark-target/build.gradle index 63a805fdf2417..9e0fb27d7f138 100644 --- a/datastore/integration-tests/macrobenchmark-target/build.gradle +++ b/datastore/integration-tests/macrobenchmark-target/build.gradle @@ -23,7 +23,7 @@ plugins { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.datastore.macrobenchmark.target" } diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle index 521652af09045..97e0ea5d087b0 100644 --- a/health/connect/connect-client/samples/build.gradle +++ b/health/connect/connect-client/samples/build.gradle @@ -47,5 +47,5 @@ android { defaultConfig { minSdk { version = release(26) } } - compileSdk { version = release(36) } + compileSdk { version = release(36) { sdkExtension = 19 }} } diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle index 9753dfe6ddeca..febc11f3a6030 100644 --- a/health/connect/connect-testing/build.gradle +++ b/health/connect/connect-testing/build.gradle @@ -44,7 +44,7 @@ android { } namespace = "androidx.health.connect.testing" testOptions.unitTests.includeAndroidResources = true - compileSdk { version = release(36) } + compileSdk { version = release(36) { sdkExtension = 19 }} } androidx { diff --git a/health/connect/connect-testing/samples/build.gradle b/health/connect/connect-testing/samples/build.gradle index a1fc2328a47ef..5bcda4ec27021 100644 --- a/health/connect/connect-testing/samples/build.gradle +++ b/health/connect/connect-testing/samples/build.gradle @@ -48,5 +48,5 @@ android { defaultConfig { minSdk { version = release(26) } } - compileSdk { version = release(36) } + compileSdk { version = release(36) { sdkExtension = 19 }} } diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle index 0f305ea175caf..24495d2386dcb 100644 --- a/lint-checks/integration-tests/build.gradle +++ b/lint-checks/integration-tests/build.gradle @@ -46,7 +46,7 @@ android { abortOnError = false } namespace = "androidx.lint.integration.tests" - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} } diff --git a/samples/AndroidXDemos/build.gradle b/samples/AndroidXDemos/build.gradle index c7926581b1627..7676bc09480c9 100644 --- a/samples/AndroidXDemos/build.gradle +++ b/samples/AndroidXDemos/build.gradle @@ -39,7 +39,7 @@ android { proguardFiles getDefaultProguardFile("proguard-android-optimize.txt") } } - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} defaultConfig { vectorDrawables.useSupportLibrary = true } diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle index d3103930783e9..f943bdc01f537 100644 --- a/samples/Support4Demos/build.gradle +++ b/samples/Support4Demos/build.gradle @@ -30,6 +30,6 @@ dependencies { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "com.example.android.supportv4" } diff --git a/samples/SupportWearDemos/build.gradle b/samples/SupportWearDemos/build.gradle index c64eae367c0fd..d2eaa35403a55 100644 --- a/samples/SupportWearDemos/build.gradle +++ b/samples/SupportWearDemos/build.gradle @@ -28,7 +28,7 @@ dependencies { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} defaultConfig { minSdk { version = release(24) } } diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle index efeacaf44b469..28f8061627b2e 100644 --- a/test/uiautomator/integration-tests/testapp/build.gradle +++ b/test/uiautomator/integration-tests/testapp/build.gradle @@ -52,6 +52,6 @@ dependencies { } android { - compileSdk { version = release(36) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.test.uiautomator.testapp" } From c04069d6d26fce038de80fde35f452f6b358dbd8 Mon Sep 17 00:00:00 2001 From: Moonwon Lee Date: Tue, 24 Feb 2026 15:03:43 -0800 Subject: [PATCH 13/16] Bump the XR library group in preparation for JXR March release. Fix: 487384986 Test: N/A Change-Id: I308d9f3356b224996c6177e5ee4142831c9b04f5 --- libraryversions.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraryversions.toml b/libraryversions.toml index b72a8502a1534..33faf321aea53 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -27,7 +27,7 @@ COLLECTION = "1.7.0-alpha01" COMPOSE = "1.11.0-alpha06" COMPOSE_MATERIAL3 = "1.5.0-alpha15" COMPOSE_MATERIAL3_ADAPTIVE = "1.3.0-alpha09" -COMPOSE_MATERIAL3_XR = "1.0.0-alpha15" +COMPOSE_MATERIAL3_XR = "1.0.0-alpha16" COMPOSE_MATERIAL3_XR_ADAPTIVE = "1.0.0-alpha01" COMPOSE_RUNTIME = "1.11.0-alpha06" CONSTRAINTLAYOUT = "2.3.0-alpha01" @@ -181,12 +181,12 @@ WINDOW_EXTENSIONS = "1.6.0-alpha01" WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01" WINDOW_SIDECAR = "1.0.0-rc01" WORK = "2.12.0-alpha01" -XR_ARCORE = "1.0.0-alpha11" -XR_COMPOSE = "1.0.0-alpha11" +XR_ARCORE = "1.0.0-alpha12" +XR_COMPOSE = "1.0.0-alpha12" XR_GLIMMER = "1.0.0-alpha07" -XR_PROJECTED = "1.0.0-alpha05" -XR_RUNTIME = "1.0.0-alpha11" -XR_SCENECORE = "1.0.0-alpha12" +XR_PROJECTED = "1.0.0-alpha06" +XR_RUNTIME = "1.0.0-alpha12" +XR_SCENECORE = "1.0.0-alpha13" [groups] ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" } From 346d793f18670789910aae41cde0e5fdcfc8f43c Mon Sep 17 00:00:00 2001 From: Pranav Sheokand Date: Tue, 24 Feb 2026 14:55:21 -0800 Subject: [PATCH 14/16] Add permission mapping for EV_CURRENT_BATTERY_CAPACITY in PERMISSION_READ_PROPERTY. Map VehiclePropertyIds.EV_CURRENT_BATTERY_CAPACITY to Car.PERMISSION_ENERGY, so that the host properly requests permission for this property before attempting to access. Bug: 487385630 Fix: 487385630 Change-Id: I2593f10b128c987a2c1e2e1f7273ee676a67b869 --- .../java/androidx/car/app/hardware/common/PropertyUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java index bcdb3066cec9b..11ba0c60e9f5c 100644 --- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java +++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java @@ -91,7 +91,7 @@ public final class PropertyUtils { append(VehiclePropertyIds.INFO_MODEL_YEAR, Car.PERMISSION_CAR_INFO); append(VehiclePropertyIds.INFO_FUEL_CAPACITY, Car.PERMISSION_CAR_INFO); append(VehiclePropertyIds.INFO_FUEL_TYPE, Car.PERMISSION_CAR_INFO); - append(VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY, Car.PERMISSION_CAR_INFO); + append(VehiclePropertyIds.EV_CURRENT_BATTERY_CAPACITY, Car.PERMISSION_ENERGY); append(VehiclePropertyIds.INFO_EV_CONNECTOR_TYPE, Car.PERMISSION_CAR_INFO); append(VehiclePropertyIds.INFO_DRIVER_SEAT, Car.PERMISSION_CAR_INFO); // VehiclePropertyId added in SDK 31 From 26867503b6a89416a5a11d1dffbeecd2de60e424 Mon Sep 17 00:00:00 2001 From: Julia McClellan Date: Tue, 24 Feb 2026 16:11:07 -0500 Subject: [PATCH 15/16] Add DefaultConstructorMarker overloads which were previously skipped Changes associated with aosp/3963946: these compiler-generated constructors were previously not tracked due to a bug in metalava, but are important for compatibility because they are how the source constructors with optional parameters are referenced when the default parameter values are used. Bug: 487195353 Relnote: N/A Test: ./gradlew checkApi Change-Id: Icd2e01ba792c1fac85d699f28f2516c9394f6c39 --- busytown/androidx_with_metalava.sh | 2 +- credentials/credentials/api/1.6.0-rc01.txt | 1 + credentials/credentials/api/current.ignore | 3 +++ credentials/credentials/api/current.txt | 1 + credentials/credentials/api/restricted_1.6.0-rc01.txt | 1 + credentials/credentials/api/restricted_current.ignore | 3 +++ credentials/credentials/api/restricted_current.txt | 1 + glance/glance/api/current.ignore | 4 ++++ glance/glance/api/current.txt | 3 ++- glance/glance/api/restricted_current.ignore | 4 ++++ glance/glance/api/restricted_current.txt | 3 ++- health/connect/connect-client/api/current.txt | 1 + health/connect/connect-client/api/restricted_current.txt | 1 + playground-common/playground.properties | 2 +- 14 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 credentials/credentials/api/current.ignore create mode 100644 credentials/credentials/api/restricted_current.ignore diff --git a/busytown/androidx_with_metalava.sh b/busytown/androidx_with_metalava.sh index 98623e8d43e5a..fa039819e1f56 100755 --- a/busytown/androidx_with_metalava.sh +++ b/busytown/androidx_with_metalava.sh @@ -4,7 +4,7 @@ SCRIPT_PATH="$(cd $(dirname $0) && pwd)" # Use this flag to temporarily disable `checkApi` # while landing Metalava w/ breaking API changes -METALAVA_INTEGRATION_ENFORCED=false +METALAVA_INTEGRATION_ENFORCED=true # The default targets to build if no arguments # are provided on the command line. diff --git a/credentials/credentials/api/1.6.0-rc01.txt b/credentials/credentials/api/1.6.0-rc01.txt index 4ec4ab3db3098..4e2ad9881214f 100644 --- a/credentials/credentials/api/1.6.0-rc01.txt +++ b/credentials/credentials/api/1.6.0-rc01.txt @@ -333,6 +333,7 @@ package androidx.credentials { ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed); ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint); } diff --git a/credentials/credentials/api/current.ignore b/credentials/credentials/api/current.ignore new file mode 100644 index 0000000000000..dc416ca32c8ea --- /dev/null +++ b/credentials/credentials/api/current.ignore @@ -0,0 +1,3 @@ +// Baseline format: 1.0 +AddedMethod: androidx.credentials.GetCustomCredentialOption#GetCustomCredentialOption(String, android.os.Bundle, android.os.Bundle, boolean, boolean, java.util.Set, int, kotlin.jvm.internal.DefaultConstructorMarker): + Added constructor androidx.credentials.GetCustomCredentialOption(String,android.os.Bundle,android.os.Bundle,boolean,boolean,java.util.Set,int,kotlin.jvm.internal.DefaultConstructorMarker) diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt index 4ec4ab3db3098..4e2ad9881214f 100644 --- a/credentials/credentials/api/current.txt +++ b/credentials/credentials/api/current.txt @@ -333,6 +333,7 @@ package androidx.credentials { ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed); ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint); } diff --git a/credentials/credentials/api/restricted_1.6.0-rc01.txt b/credentials/credentials/api/restricted_1.6.0-rc01.txt index 4ec4ab3db3098..4e2ad9881214f 100644 --- a/credentials/credentials/api/restricted_1.6.0-rc01.txt +++ b/credentials/credentials/api/restricted_1.6.0-rc01.txt @@ -333,6 +333,7 @@ package androidx.credentials { ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed); ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint); } diff --git a/credentials/credentials/api/restricted_current.ignore b/credentials/credentials/api/restricted_current.ignore new file mode 100644 index 0000000000000..dc416ca32c8ea --- /dev/null +++ b/credentials/credentials/api/restricted_current.ignore @@ -0,0 +1,3 @@ +// Baseline format: 1.0 +AddedMethod: androidx.credentials.GetCustomCredentialOption#GetCustomCredentialOption(String, android.os.Bundle, android.os.Bundle, boolean, boolean, java.util.Set, int, kotlin.jvm.internal.DefaultConstructorMarker): + Added constructor androidx.credentials.GetCustomCredentialOption(String,android.os.Bundle,android.os.Bundle,boolean,boolean,java.util.Set,int,kotlin.jvm.internal.DefaultConstructorMarker) diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt index 4ec4ab3db3098..4e2ad9881214f 100644 --- a/credentials/credentials/api/restricted_current.txt +++ b/credentials/credentials/api/restricted_current.txt @@ -333,6 +333,7 @@ package androidx.credentials { ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed); ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public GetCustomCredentialOption(String!, android.os.Bundle!, android.os.Bundle!, boolean, boolean, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders); ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint); } diff --git a/glance/glance/api/current.ignore b/glance/glance/api/current.ignore index 5d0400b49bbda..5aab194a4688c 100644 --- a/glance/glance/api/current.ignore +++ b/glance/glance/api/current.ignore @@ -1,3 +1,7 @@ // Baseline format: 1.0 +RemovedFromBytecode: androidx.glance.text.TextStyle#TextStyle(androidx.glance.unit.ColorProvider, androidx.compose.ui.unit.TextUnit, androidx.glance.text.FontWeight, androidx.glance.text.FontStyle, androidx.glance.text.TextAlign, androidx.glance.text.TextDecoration, androidx.glance.text.FontFamily): + Binary breaking change: constructor androidx.glance.text.TextStyle(androidx.glance.unit.ColorProvider,androidx.compose.ui.unit.TextUnit,androidx.glance.text.FontWeight,androidx.glance.text.FontStyle,androidx.glance.text.TextAlign,androidx.glance.text.TextDecoration,androidx.glance.text.FontFamily) has been removed from bytecode + + RemovedMethod: androidx.glance.text.TextStyle#TextStyle(): Binary breaking change: Removed constructor androidx.glance.text.TextStyle() diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt index 09eee05d85b95..05b3b43cb5df4 100644 --- a/glance/glance/api/current.txt +++ b/glance/glance/api/current.txt @@ -618,8 +618,9 @@ package androidx.glance.text { } @androidx.compose.runtime.Immutable public final class TextStyle { - ctor @InaccessibleFromJava public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); + ctor @KotlinOnly public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); ctor @BytecodeOnly public TextStyle(androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public TextStyle(androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, kotlin.jvm.internal.DefaultConstructorMarker!); method @KotlinOnly public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); method @BytecodeOnly public androidx.glance.text.TextStyle copy-KmPxOYk(androidx.glance.unit.ColorProvider, androidx.compose.ui.unit.TextUnit?, androidx.glance.text.FontWeight?, androidx.glance.text.FontStyle?, androidx.glance.text.TextAlign?, androidx.glance.text.TextDecoration?, androidx.glance.text.FontFamily?); method @BytecodeOnly public static androidx.glance.text.TextStyle! copy-KmPxOYk$default(androidx.glance.text.TextStyle!, androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, int, Object!); diff --git a/glance/glance/api/restricted_current.ignore b/glance/glance/api/restricted_current.ignore index 5d0400b49bbda..5aab194a4688c 100644 --- a/glance/glance/api/restricted_current.ignore +++ b/glance/glance/api/restricted_current.ignore @@ -1,3 +1,7 @@ // Baseline format: 1.0 +RemovedFromBytecode: androidx.glance.text.TextStyle#TextStyle(androidx.glance.unit.ColorProvider, androidx.compose.ui.unit.TextUnit, androidx.glance.text.FontWeight, androidx.glance.text.FontStyle, androidx.glance.text.TextAlign, androidx.glance.text.TextDecoration, androidx.glance.text.FontFamily): + Binary breaking change: constructor androidx.glance.text.TextStyle(androidx.glance.unit.ColorProvider,androidx.compose.ui.unit.TextUnit,androidx.glance.text.FontWeight,androidx.glance.text.FontStyle,androidx.glance.text.TextAlign,androidx.glance.text.TextDecoration,androidx.glance.text.FontFamily) has been removed from bytecode + + RemovedMethod: androidx.glance.text.TextStyle#TextStyle(): Binary breaking change: Removed constructor androidx.glance.text.TextStyle() diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt index 09eee05d85b95..05b3b43cb5df4 100644 --- a/glance/glance/api/restricted_current.txt +++ b/glance/glance/api/restricted_current.txt @@ -618,8 +618,9 @@ package androidx.glance.text { } @androidx.compose.runtime.Immutable public final class TextStyle { - ctor @InaccessibleFromJava public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); + ctor @KotlinOnly public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); ctor @BytecodeOnly public TextStyle(androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public TextStyle(androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, kotlin.jvm.internal.DefaultConstructorMarker!); method @KotlinOnly public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily); method @BytecodeOnly public androidx.glance.text.TextStyle copy-KmPxOYk(androidx.glance.unit.ColorProvider, androidx.compose.ui.unit.TextUnit?, androidx.glance.text.FontWeight?, androidx.glance.text.FontStyle?, androidx.glance.text.TextAlign?, androidx.glance.text.TextDecoration?, androidx.glance.text.FontFamily?); method @BytecodeOnly public static androidx.glance.text.TextStyle! copy-KmPxOYk$default(androidx.glance.text.TextStyle!, androidx.glance.unit.ColorProvider!, androidx.compose.ui.unit.TextUnit!, androidx.glance.text.FontWeight!, androidx.glance.text.FontStyle!, androidx.glance.text.TextAlign!, androidx.glance.text.TextDecoration!, androidx.glance.text.FontFamily!, int, Object!); diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt index 07f59d0c20591..05b6235d8c91c 100644 --- a/health/connect/connect-client/api/current.txt +++ b/health/connect/connect-client/api/current.txt @@ -2400,6 +2400,7 @@ package androidx.health.connect.client.request { } public final class ReadRecordsRequest { + ctor @BytecodeOnly public ReadRecordsRequest(kotlin.reflect.KClass!, androidx.health.connect.client.time.TimeRangeFilter!, java.util.Set!, boolean, int, String!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken); } diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt index 8d7d16dc94ba7..5c70cbff2a172 100644 --- a/health/connect/connect-client/api/restricted_current.txt +++ b/health/connect/connect-client/api/restricted_current.txt @@ -2423,6 +2423,7 @@ package androidx.health.connect.client.request { } public final class ReadRecordsRequest { + ctor @BytecodeOnly public ReadRecordsRequest(kotlin.reflect.KClass!, androidx.health.connect.client.time.TimeRangeFilter!, java.util.Set!, boolean, int, String!, int, kotlin.jvm.internal.DefaultConstructorMarker!); ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken); } diff --git a/playground-common/playground.properties b/playground-common/playground.properties index eee145de2bb3b..b91e2b80f886a 100644 --- a/playground-common/playground.properties +++ b/playground-common/playground.properties @@ -26,5 +26,5 @@ kotlin.code.style=official # Disable docs androidx.enableDocumentation=false androidx.playground.snapshotBuildId=13706940 -androidx.playground.metalavaBuildId=14884342 +androidx.playground.metalavaBuildId=14932853 androidx.studio.type=playground \ No newline at end of file From dda155bcf213285e49984782ecf3362169a6bb73 Mon Sep 17 00:00:00 2001 From: Cary Haynie Date: Wed, 25 Feb 2026 00:13:13 +0000 Subject: [PATCH 16/16] [jxr] Change Config.augmentedObjectCategories from List to Set RelNote: Changed Config.augmentedObjectCategories from a List to a Set, reflecting the expectation that a given category should only be provided once. Bug: 487376359 Change-Id: I25a64eb05fa641c80185dbff62b7d140a2d4cac0 --- .../xr/arcore/guava/GeospatialGuavaTest.java | 2 +- .../openxr/OpenXrAugmentedObjectTest.kt | 2 +- .../xr/arcore/openxr/OpenXrManagerTest.kt | 2 +- .../androidx/xr/arcore/AugmentedObjectTest.kt | 4 ++-- .../testapp/helloar/HelloArObjectActivity.kt | 2 +- xr/runtime/runtime/api/current.txt | 12 +++++------ xr/runtime/runtime/api/restricted_current.txt | 20 +++++++++---------- .../main/kotlin/androidx/xr/runtime/Config.kt | 20 +++++++++---------- .../androidx/xr/runtime/XrCapabilities.kt | 4 ++-- .../kotlin/androidx/xr/runtime/SessionTest.kt | 2 +- 10 files changed, 35 insertions(+), 35 deletions(-) diff --git a/xr/arcore/arcore-guava/src/test/java/androidx/xr/arcore/guava/GeospatialGuavaTest.java b/xr/arcore/arcore-guava/src/test/java/androidx/xr/arcore/guava/GeospatialGuavaTest.java index 33236ae8a75db..70241050d1b79 100644 --- a/xr/arcore/arcore-guava/src/test/java/androidx/xr/arcore/guava/GeospatialGuavaTest.java +++ b/xr/arcore/arcore-guava/src/test/java/androidx/xr/arcore/guava/GeospatialGuavaTest.java @@ -286,7 +286,7 @@ private void createTestSessionAndRunTest(Runnable testBody) { AnchorPersistenceMode.DISABLED, FaceTrackingMode.DISABLED, GeospatialMode.VPS_AND_GPS, - java.util.Collections.emptyList(), + java.util.Collections.emptySet(), EyeTrackingMode.DISABLED, CameraFacingDirection.WORLD)); diff --git a/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrAugmentedObjectTest.kt b/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrAugmentedObjectTest.kt index 1a4eb7842d688..2d810eb2beeee 100644 --- a/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrAugmentedObjectTest.kt +++ b/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrAugmentedObjectTest.kt @@ -113,7 +113,7 @@ class OpenXrAugmentedObjectTest { openXrManager.configure( Config( augmentedObjectCategories = - listOf( + setOf( AugmentedObjectCategory.KEYBOARD, AugmentedObjectCategory.MOUSE, AugmentedObjectCategory.LAPTOP, diff --git a/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrManagerTest.kt b/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrManagerTest.kt index 20360104c593e..750527fc104eb 100644 --- a/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrManagerTest.kt +++ b/xr/arcore/arcore-openxr/src/androidTest/kotlin/androidx/xr/arcore/openxr/OpenXrManagerTest.kt @@ -337,7 +337,7 @@ class OpenXrManagerTest { check(perceptionManager.xrResources.updatables.isEmpty()) underTest.configure( - Config(augmentedObjectCategories = listOf(AugmentedObjectCategory.KEYBOARD)) + Config(augmentedObjectCategories = setOf(AugmentedObjectCategory.KEYBOARD)) ) underTest.update() diff --git a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AugmentedObjectTest.kt b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AugmentedObjectTest.kt index 5149211998a23..7a53e028a5080 100644 --- a/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AugmentedObjectTest.kt +++ b/xr/arcore/arcore/src/test/kotlin/androidx/xr/arcore/AugmentedObjectTest.kt @@ -79,7 +79,7 @@ class AugmentedObjectTest { session.configure( Config( augmentedObjectCategories = - listOf( + setOf( AugmentedObjectCategory.KEYBOARD, AugmentedObjectCategory.MOUSE, AugmentedObjectCategory.LAPTOP, @@ -116,7 +116,7 @@ class AugmentedObjectTest { @Test fun subscribe_augmentedObjectTrackingDisabled_throwsIllegalStateException() { - val configureResult = session.configure(Config(augmentedObjectCategories = listOf())) + val configureResult = session.configure(Config(augmentedObjectCategories = setOf())) assertFailsWith { AugmentedObject.subscribe(session) } } diff --git a/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/helloar/HelloArObjectActivity.kt b/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/helloar/HelloArObjectActivity.kt index b0c9e222b529e..d2bbc194f7a5d 100644 --- a/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/helloar/HelloArObjectActivity.kt +++ b/xr/arcore/integration-tests/testapp/src/main/kotlin/androidx/xr/arcore/testapp/helloar/HelloArObjectActivity.kt @@ -71,7 +71,7 @@ class HelloArObjectActivity : ComponentActivity() { Config( deviceTracking = DeviceTrackingMode.LAST_KNOWN, augmentedObjectCategories = - listOf( + setOf( AugmentedObjectCategory.KEYBOARD, AugmentedObjectCategory.MOUSE, AugmentedObjectCategory.LAPTOP, diff --git a/xr/runtime/runtime/api/current.txt b/xr/runtime/runtime/api/current.txt index cf8b997bf034a..10b0dd68b1290 100644 --- a/xr/runtime/runtime/api/current.txt +++ b/xr/runtime/runtime/api/current.txt @@ -14,7 +14,7 @@ package androidx.xr.runtime { public final class AugmentedObjectCategory { method @Deprecated public static java.util.List all(); - method public static java.util.List allSupported(); + method public static java.util.Set allSupported(); field public static final androidx.xr.runtime.AugmentedObjectCategory.Companion Companion; field public static final androidx.xr.runtime.AugmentedObjectCategory KEYBOARD; field public static final androidx.xr.runtime.AugmentedObjectCategory LAPTOP; @@ -24,7 +24,7 @@ package androidx.xr.runtime { public static final class AugmentedObjectCategory.Companion { method @Deprecated public java.util.List all(); - method public java.util.List allSupported(); + method public java.util.Set allSupported(); property public androidx.xr.runtime.AugmentedObjectCategory KEYBOARD; property public androidx.xr.runtime.AugmentedObjectCategory LAPTOP; property public androidx.xr.runtime.AugmentedObjectCategory MOUSE; @@ -47,8 +47,8 @@ package androidx.xr.runtime { ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence); ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking); ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial); - ctor @BytecodeOnly public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.List!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.List augmentedObjectCategories); + ctor @BytecodeOnly public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.Set augmentedObjectCategories); method public androidx.xr.runtime.Config copy(); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking); @@ -57,7 +57,7 @@ package androidx.xr.runtime { method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence); method @BytecodeOnly public static androidx.xr.runtime.Config! copy$default(androidx.xr.runtime.Config!, androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, int, Object!); method @InaccessibleFromKotlin public androidx.xr.runtime.AnchorPersistenceMode getAnchorPersistence(); - method @InaccessibleFromKotlin public java.util.List getAugmentedObjectCategories(); + method @InaccessibleFromKotlin public java.util.Set getAugmentedObjectCategories(); method @InaccessibleFromKotlin public androidx.xr.runtime.DepthEstimationMode getDepthEstimation(); method @InaccessibleFromKotlin public androidx.xr.runtime.DeviceTrackingMode getDeviceTracking(); method @InaccessibleFromKotlin public androidx.xr.runtime.FaceTrackingMode getFaceTracking(); @@ -65,7 +65,7 @@ package androidx.xr.runtime { method @InaccessibleFromKotlin public androidx.xr.runtime.HandTrackingMode getHandTracking(); method @InaccessibleFromKotlin public androidx.xr.runtime.PlaneTrackingMode getPlaneTracking(); property public androidx.xr.runtime.AnchorPersistenceMode anchorPersistence; - property public java.util.List augmentedObjectCategories; + property public java.util.Set augmentedObjectCategories; property public androidx.xr.runtime.DepthEstimationMode depthEstimation; property public androidx.xr.runtime.DeviceTrackingMode deviceTracking; property public androidx.xr.runtime.FaceTrackingMode faceTracking; diff --git a/xr/runtime/runtime/api/restricted_current.txt b/xr/runtime/runtime/api/restricted_current.txt index 410f7aa3c8993..33c11b868650a 100644 --- a/xr/runtime/runtime/api/restricted_current.txt +++ b/xr/runtime/runtime/api/restricted_current.txt @@ -16,7 +16,7 @@ package androidx.xr.runtime { public final class AugmentedObjectCategory { method @Deprecated public static java.util.List all(); - method public static java.util.List allSupported(); + method public static java.util.Set allSupported(); field public static final androidx.xr.runtime.AugmentedObjectCategory.Companion Companion; field public static final androidx.xr.runtime.AugmentedObjectCategory KEYBOARD; field public static final androidx.xr.runtime.AugmentedObjectCategory LAPTOP; @@ -26,7 +26,7 @@ package androidx.xr.runtime { public static final class AugmentedObjectCategory.Companion { method @Deprecated public java.util.List all(); - method public java.util.List allSupported(); + method public java.util.Set allSupported(); property public androidx.xr.runtime.AugmentedObjectCategory KEYBOARD; property public androidx.xr.runtime.AugmentedObjectCategory LAPTOP; property public androidx.xr.runtime.AugmentedObjectCategory MOUSE; @@ -62,21 +62,21 @@ package androidx.xr.runtime { ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence); ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking); ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial); - ctor @BytecodeOnly @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.List!, androidx.xr.runtime.EyeTrackingMode!, androidx.xr.runtime.CameraFacingDirection!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor @BytecodeOnly public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.List!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.List augmentedObjectCategories); - ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.List augmentedObjectCategories, optional androidx.xr.runtime.EyeTrackingMode eyeTracking, optional androidx.xr.runtime.CameraFacingDirection cameraFacingDirection); + ctor @BytecodeOnly @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.Set!, androidx.xr.runtime.EyeTrackingMode!, androidx.xr.runtime.CameraFacingDirection!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor @BytecodeOnly public Config(androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.Set!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.Set augmentedObjectCategories); + ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public Config(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.Set augmentedObjectCategories, optional androidx.xr.runtime.EyeTrackingMode eyeTracking, optional androidx.xr.runtime.CameraFacingDirection cameraFacingDirection); method public androidx.xr.runtime.Config copy(); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation); method public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence); - method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.List augmentedObjectCategories, optional androidx.xr.runtime.EyeTrackingMode eyeTracking, optional androidx.xr.runtime.CameraFacingDirection cameraFacingDirection); - method @BytecodeOnly @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.Config! copy$default(androidx.xr.runtime.Config!, androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.List!, androidx.xr.runtime.EyeTrackingMode!, androidx.xr.runtime.CameraFacingDirection!, int, Object!); + method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.Config copy(optional androidx.xr.runtime.PlaneTrackingMode planeTracking, optional androidx.xr.runtime.HandTrackingMode handTracking, optional androidx.xr.runtime.DeviceTrackingMode deviceTracking, optional androidx.xr.runtime.DepthEstimationMode depthEstimation, optional androidx.xr.runtime.AnchorPersistenceMode anchorPersistence, optional androidx.xr.runtime.FaceTrackingMode faceTracking, optional androidx.xr.runtime.GeospatialMode geospatial, optional java.util.Set augmentedObjectCategories, optional androidx.xr.runtime.EyeTrackingMode eyeTracking, optional androidx.xr.runtime.CameraFacingDirection cameraFacingDirection); + method @BytecodeOnly @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.xr.runtime.Config! copy$default(androidx.xr.runtime.Config!, androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, androidx.xr.runtime.FaceTrackingMode!, androidx.xr.runtime.GeospatialMode!, java.util.Set!, androidx.xr.runtime.EyeTrackingMode!, androidx.xr.runtime.CameraFacingDirection!, int, Object!); method @BytecodeOnly public static androidx.xr.runtime.Config! copy$default(androidx.xr.runtime.Config!, androidx.xr.runtime.PlaneTrackingMode!, androidx.xr.runtime.HandTrackingMode!, androidx.xr.runtime.DeviceTrackingMode!, androidx.xr.runtime.DepthEstimationMode!, androidx.xr.runtime.AnchorPersistenceMode!, int, Object!); method @InaccessibleFromKotlin public androidx.xr.runtime.AnchorPersistenceMode getAnchorPersistence(); - method @InaccessibleFromKotlin public java.util.List getAugmentedObjectCategories(); + method @InaccessibleFromKotlin public java.util.Set getAugmentedObjectCategories(); method @InaccessibleFromKotlin @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.CameraFacingDirection getCameraFacingDirection(); method @InaccessibleFromKotlin public androidx.xr.runtime.DepthEstimationMode getDepthEstimation(); method @InaccessibleFromKotlin public androidx.xr.runtime.DeviceTrackingMode getDeviceTracking(); @@ -86,7 +86,7 @@ package androidx.xr.runtime { method @InaccessibleFromKotlin public androidx.xr.runtime.HandTrackingMode getHandTracking(); method @InaccessibleFromKotlin public androidx.xr.runtime.PlaneTrackingMode getPlaneTracking(); property public androidx.xr.runtime.AnchorPersistenceMode anchorPersistence; - property public java.util.List augmentedObjectCategories; + property public java.util.Set augmentedObjectCategories; property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.CameraFacingDirection cameraFacingDirection; property public androidx.xr.runtime.DepthEstimationMode depthEstimation; property public androidx.xr.runtime.DeviceTrackingMode deviceTracking; diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Config.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Config.kt index 666c1274a1827..888170756389f 100644 --- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Config.kt +++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/Config.kt @@ -56,7 +56,7 @@ constructor( androidx.xr.runtime.FaceTrackingMode.DISABLED, public val geospatial: androidx.xr.runtime.GeospatialMode = androidx.xr.runtime.GeospatialMode.DISABLED, - public val augmentedObjectCategories: List = listOf(), + public val augmentedObjectCategories: Set = setOf(), @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public val eyeTracking: EyeTrackingMode = EyeTrackingMode.DISABLED, @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @@ -103,7 +103,7 @@ constructor( androidx.xr.runtime.FaceTrackingMode.DISABLED, geospatial: androidx.xr.runtime.GeospatialMode = androidx.xr.runtime.GeospatialMode.DISABLED, - augmentedObjectCategories: List = listOf(), + augmentedObjectCategories: Set = setOf(), ) : this( planeTracking, handTracking, @@ -133,7 +133,7 @@ constructor( @Suppress("DEPRECATION") public constructor( planeTracking: Config.PlaneTrackingMode - ) : this(planeTracking = planeTracking.toNewType(), augmentedObjectCategories = listOf()) + ) : this(planeTracking = planeTracking.toNewType(), augmentedObjectCategories = setOf()) /** * Defines a configuration state of all available features to be set at runtime. @@ -163,7 +163,7 @@ constructor( handTracking: Config.HandTrackingMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), ) @@ -199,7 +199,7 @@ constructor( deviceTracking: Config.DeviceTrackingMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), deviceTracking = deviceTracking.toNewType(), ) @@ -240,7 +240,7 @@ constructor( depthEstimation: Config.DepthEstimationMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), deviceTracking = deviceTracking.toNewType(), depthEstimation = depthEstimation.toNewType(), @@ -286,7 +286,7 @@ constructor( anchorPersistence: Config.AnchorPersistenceMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), deviceTracking = deviceTracking.toNewType(), depthEstimation = depthEstimation.toNewType(), @@ -337,7 +337,7 @@ constructor( faceTracking: Config.FaceTrackingMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), deviceTracking = deviceTracking.toNewType(), depthEstimation = depthEstimation.toNewType(), @@ -393,7 +393,7 @@ constructor( geospatial: Config.GeospatialMode, ) : this( planeTracking = planeTracking.toNewType(), - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = handTracking.toNewType(), deviceTracking = deviceTracking.toNewType(), depthEstimation = depthEstimation.toNewType(), @@ -466,7 +466,7 @@ constructor( anchorPersistence: androidx.xr.runtime.AnchorPersistenceMode = this.anchorPersistence, faceTracking: androidx.xr.runtime.FaceTrackingMode = this.faceTracking, geospatial: androidx.xr.runtime.GeospatialMode = this.geospatial, - augmentedObjectCategories: List = this.augmentedObjectCategories, + augmentedObjectCategories: Set = this.augmentedObjectCategories, eyeTracking: EyeTrackingMode = this.eyeTracking, cameraFacingDirection: CameraFacingDirection = this.cameraFacingDirection, ): Config { diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/XrCapabilities.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/XrCapabilities.kt index e545a674907f4..cfd2f136c132d 100644 --- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/XrCapabilities.kt +++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/XrCapabilities.kt @@ -59,8 +59,8 @@ public class AugmentedObjectCategory private constructor(private val value: Int) * the device. */ @JvmStatic - public fun allSupported(): List = - listOf( + public fun allSupported(): Set = + setOf( // TODO b/483728983 determine contents of this list dynamically based on device // capability KEYBOARD, diff --git a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt index 1c4caa4aaf0fd..5be17bc354f53 100644 --- a/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt +++ b/xr/runtime/runtime/src/test/kotlin/androidx/xr/runtime/SessionTest.kt @@ -218,7 +218,7 @@ class SessionTest { val newConfig = Config( planeTracking = PlaneTrackingMode.DISABLED, - augmentedObjectCategories = listOf(), + augmentedObjectCategories = setOf(), handTracking = HandTrackingMode.DISABLED, deviceTracking = DeviceTrackingMode.DISABLED, depthEstimation = DepthEstimationMode.DISABLED,