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°") + } +} 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 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..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 @@ -112,6 +112,8 @@ 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.panWithVelocity import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performSemanticsAction @@ -470,7 +472,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 +482,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 +1018,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 +1028,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) } } @@ -2942,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/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/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 73d6c201600b2..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,52 +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.Pan) - ) - 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 @@ -91,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, @@ -116,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 { @@ -138,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) { @@ -177,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 { /** @@ -218,8 +166,6 @@ internal class MouseWheelScrollingLogic( } } - private val velocityTracker = MouseWheelVelocityTracker() - private fun trackVelocity(scrollDelta: MouseWheelScrollDelta) { velocityTracker.addDelta(scrollDelta.timeMillis, scrollDelta.value) } @@ -362,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 d5a1808897e45..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,14 +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.Pan) + (pointerEvent.type == PointerEventType.PanStart || + pointerEvent.type == PointerEventType.PanMove || + pointerEvent.type == PointerEventType.PanEnd) ) { - ensureMouseWheelScrollNodeInitialized() + ensureTrackpadScrollingLogicInitialized() } - mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) + trackpadScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) } } @@ -598,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..64280f59d7587 --- /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.panOffset + if (scrollingLogic.canConsumeDelta(delta)) { + sent = + channel + .trySend( + TrackpadScrollDelta( + value = delta, + timeMillis = historicalChange.uptimeMillis, + isEnd = false, + ) + ) + .isSuccess || sent + } + } + val delta = -it.panOffset + 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 c3668fb48214d..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 @@ -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,17 +362,32 @@ 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? { 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 } - 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.panOffset + + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> + acc - historicalChange.panOffset + } + } ?: Offset.Zero) + } else { + Offset.Zero + } if (scrollDelta == Offset.Zero) { return null @@ -373,14 +397,23 @@ private fun AwaitPointerEventScope.consumePointerEventAsMouseWheelScrollOrNull( return scrollDelta } -private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( - pointer: PointerEvent, - scrollConfig: ScrollConfig, -): Offset? { - if (pointer.type != PointerEventType.Pan) { +private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull(pointer: PointerEvent): Offset? { + @OptIn(ExperimentalFoundationApi::class) + if ( + !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.panOffset + + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> + acc - historicalChange.panOffset + } + } ?: Offset.Zero if (scrollDelta == Offset.Zero) { return null @@ -391,13 +424,19 @@ private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull( } private fun AwaitPointerEventScope.consumePointerEventAsScaleOrNull(pointer: PointerEvent): Float? { - if (pointer.type != PointerEventType.Scale) { + @OptIn(ExperimentalFoundationApi::class) + if ( + !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || + (pointer.type != PointerEventType.ScaleStart && + pointer.type != PointerEventType.ScaleChange && + pointer.type != PointerEventType.ScaleEnd) + ) { return null } 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) { @@ -426,7 +465,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/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 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) 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 95% 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 84c7b6f308160..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 @@ -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 @@ -37,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 @@ -52,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()) @@ -73,7 +76,7 @@ class ScrollTest { rule.onNodeWithTag(TAG).performTrackpadInput { enter() // scroll vertically - scroll(Offset(0f, 10f)) + pan(Offset(0f, 10f)) } rule.runOnIdle { @@ -101,7 +104,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 +135,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 +166,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) @@ -213,7 +216,7 @@ class ScrollTest { rule.onNodeWithTag(TAG).performTrackpadInput { enter() // scroll horizontally - scroll(Offset(10f, 0f)) + pan(Offset(10f, 0f)) } rule.runOnIdle { @@ -241,7 +244,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 +275,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 +306,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) @@ -355,7 +358,7 @@ class ScrollTest { // press primary button press(MouseButton.Primary) // scroll - scroll(Offset(10f, 0f)) + pan(Offset(10f, 0f)) } rule.runOnIdle { @@ -385,7 +388,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 +419,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 +450,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-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..6f5031a2a86eb --- /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.panOffset + 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 95% 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 5f5f17d4001a7..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 @@ -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 @@ -36,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 @@ -50,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()) @@ -70,7 +73,7 @@ class PinchTest { rule.onNodeWithTag(TAG).performTrackpadInput { moveTo(center) - pinch(0.9f) + scale(0.9f) } rule.runOnIdle { @@ -97,7 +100,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleStart } else { Press }, @@ -124,7 +127,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -148,7 +151,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -167,7 +170,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -191,7 +194,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -210,7 +213,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -234,7 +237,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -253,7 +256,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleEnd } else { Release }, @@ -291,7 +294,7 @@ class PinchTest { rule.onNodeWithTag(TAG).performTrackpadInput { moveTo(center) - pinch(1.1f) + scale(1.1f) } rule.runOnIdle { @@ -318,7 +321,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleStart } else { Press }, @@ -344,7 +347,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -368,7 +371,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Press }, @@ -387,7 +390,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -411,7 +414,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Move }, @@ -430,7 +433,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -454,7 +457,7 @@ class PinchTest { ComposeUiFlags.isTrackpadGestureHandlingEnabled && Build.VERSION.SDK_INT >= 34 ) { - Scale + ScaleChange } else { Release }, @@ -473,7 +476,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/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-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 diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index 43b660a1033a2..2362ecd2ebe8e 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt @@ -2436,17 +2436,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; } @@ -2517,19 +2517,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; } @@ -2612,7 +2620,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!); @@ -2648,14 +2656,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(); @@ -2664,14 +2672,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 ce36908709e6c..2295caf07eb10 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt @@ -2437,17 +2437,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; } @@ -2518,19 +2518,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; } @@ -2613,7 +2621,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!); @@ -2649,14 +2657,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(); @@ -2665,14 +2673,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 7eee6159f3cf9..843b0b92b9e61 100644 --- a/compose/ui/ui/bcv/native/current.txt +++ b/compose/ui/ui/bcv/native/current.txt @@ -1882,12 +1882,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] @@ -1934,8 +1934,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] @@ -1948,8 +1948,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] @@ -3286,14 +3286,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/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 913b83cf29a3b..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 @@ -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 } @@ -194,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/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt index 6f4eba7a1ea4e..51076b36373ea 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/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 18a25c9f04f08..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 @@ -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.scaleFactor]'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.panOffset]'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" } } @@ -380,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( @@ -401,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, @@ -429,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) @@ -548,8 +578,8 @@ class PointerInputChange( isInitiallyConsumed = isInitiallyConsumed, type = type, scrollDelta = scrollDelta, - scaleGestureFactor = scaleGestureFactor, - panGestureOffset = panGestureOffset, + scaleFactor = scaleGestureFactor, + panOffset = panGestureOffset, ) { _historical = historical this.originalEventPosition = originalEventPosition @@ -644,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 { @@ -730,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 { @@ -774,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 { @@ -860,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 { @@ -891,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, @@ -931,8 +961,8 @@ class PointerInputChange( "type=$type, " + "historical=$historical, " + "scrollDelta=$scrollDelta, " + - "scaleGestureFactor=$scaleGestureFactor, " + - "panGestureOffset=$panGestureOffset)" + "scaleFactor=$scaleFactor, " + + "panOffset=$panOffset)" } } @@ -945,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 @@ -968,8 +998,8 @@ class HistoricalChange( ) : this( uptimeMillis = uptimeMillis, position = position, - scaleGestureFactor = 1f, - panGestureOffset = Offset.Zero, + scaleFactor = 1f, + panOffset = Offset.Zero, ) internal constructor( @@ -985,8 +1015,8 @@ class HistoricalChange( override fun toString(): String { return "HistoricalChange(uptimeMillis=$uptimeMillis, " + "position=$position, " + - "scaleGestureFactor=$scaleGestureFactor, " + - "panGestureOffset=$panGestureOffset)" + "scaleFactor=$scaleFactor, " + + "panOffset=$panOffset)" } } 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/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/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/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/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/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/libraryversions.toml b/libraryversions.toml index b72a8502a1534..41f2d1424312e 100644 --- a/libraryversions.toml +++ b/libraryversions.toml @@ -14,20 +14,20 @@ 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" 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" @@ -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" @@ -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" } 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/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 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" } 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/androidMain/kotlin/androidx/tracing/ContextElements.android.kt b/tracing/tracing/src/androidMain/kotlin/androidx/tracing/ContextElements.android.kt deleted file mode 100644 index 5ecbb749239ab..0000000000000 --- a/tracing/tracing/src/androidMain/kotlin/androidx/tracing/ContextElements.android.kt +++ /dev/null @@ -1,69 +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 - -@Suppress("KmpSignatureClash") // also defined in desktop -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/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/jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt similarity index 78% rename from tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.kt rename to tracing/tracing/src/jvmAndAndroidMain/kotlin/androidx/tracing/ContextElements.jvmAndAndroid.kt index 6e1488a7bdc43..cc7267deddf5b 100644 --- a/tracing/tracing/src/desktopMain/kotlin/androidx/tracing/ContextElements.desktop.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,16 +19,22 @@ package androidx.tracing import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ThreadContextElement -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. @@ -37,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, 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,