From b93376c43427335f526101cae30d390135783ecb Mon Sep 17 00:00:00 2001 From: Santiago Seifert Date: Fri, 2 Jan 2026 13:10:18 +0000 Subject: [PATCH 01/19] Change log tag of MR2ProviderServiceAdapter So as to avoid matching the platform's tag. Bug: b/205124386 Test: N/A. Log change. Flag: EXEMPT DEBUG Change-Id: I9df8be69fe1714bb3c5929b4816a1f06abd3a601 --- .../mediarouter/media/MediaRoute2ProviderServiceAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java index 7d961ef2573aa..3ecd263535dfe 100644 --- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java +++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java @@ -67,7 +67,7 @@ @RequiresApi(api = Build.VERSION_CODES.R) class MediaRoute2ProviderServiceAdapter extends MediaRoute2ProviderService { - private static final String TAG = "MR2ProviderService"; + private static final String TAG = "AxMR2ProvdrSrvcAdapter"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final Object mLock = new Object(); From 2d4e3c01fba5c7cc08c6212e4c67ee29c7a8fe93 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Tue, 23 Dec 2025 09:39:29 +0100 Subject: [PATCH 02/19] Add tests for FastSafeIterableMap Adds `FastSafeIterableMapTest` to the common source set to verify the correctness of the internal map implementation. This ensures that basic operations function as expected and validates safe iteration behavior. The tests cover edge cases such as removing the current element, adding elements, and modifying the map during nested iterations to prevent regression in the lifecycle runtime. Bug: N/A Test: FastSafeIterableMapTest Change-Id: Ie6a96556298d6992bee6e97c565d8edec521d00f --- .../lifecycle/FastSafeIterableMapTest.kt | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt diff --git a/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt b/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt new file mode 100644 index 0000000000000..733c29fa9fb64 --- /dev/null +++ b/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt @@ -0,0 +1,157 @@ +/* + * 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.lifecycle + +import androidx.kruth.assertThat +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class FastSafeIterableMapTest { + + @Test + fun putAndRemove() { + val map = FastSafeIterableMap() + assertThat(map.putIfAbsent("a", 1)).isNull() + assertThat(map.putIfAbsent("a", 2)).isEqualTo(1) + assertThat(map.size()).isEqualTo(1) + + assertThat(map.remove("a")).isEqualTo(1) + assertThat(map.remove("a")).isNull() + assertThat(map.size()).isEqualTo(0) + } + + @Test + fun iterationFollowsInsertionOrder() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + map.putIfAbsent("b", 2) + map.putIfAbsent("c", 3) + + val keys = mutableListOf() + map.forEachWithAdditions { keys.add(it.key) } + + assertThat(keys).containsExactly("a", "b", "c").inOrder() + } + + @Test + fun removeCurrentDuringIteration() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + map.putIfAbsent("b", 2) + map.putIfAbsent("c", 3) + + val visited = mutableListOf() + map.forEachWithAdditions { entry -> + visited.add(entry.key) + if (entry.key == "b") { + map.remove("b") + } + } + + assertThat(visited).containsExactly("a", "b", "c").inOrder() + assertThat(map.contains("b")).isFalse() + } + + @Test + fun addDuringIteration() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + + val visited = mutableListOf() + map.forEachWithAdditions { entry -> + visited.add(entry.key) + if (entry.key == "a") { + map.putIfAbsent("b", 2) + } + } + + assertThat(visited).containsExactly("a", "b").inOrder() + } + + @Test + fun removeAllRemainingDuringIteration() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + map.putIfAbsent("b", 2) + map.putIfAbsent("c", 3) + + val visited = mutableListOf() + map.forEachWithAdditions { entry -> + visited.add(entry.key) + map.remove("a") + map.remove("b") + map.remove("c") + } + + assertThat(visited).containsExactly("a") + assertThat(map.size()).isEqualTo(0) + } + + @Test + @Ignore // TODO(mgalhardo): Android implementation doesn't support this case. + fun reAddRemovedElementDuringIteration() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + map.putIfAbsent("b", 2) + + val visited = mutableListOf() + map.forEachWithAdditions { entry -> + visited.add(entry.key) + if (entry.key == "a") { + map.remove("a") + map.putIfAbsent("a", 100) + } + } + + assertThat(visited).containsExactly("a", "b", "a").inOrder() + } + + @Test + fun nestedIterationSafety() { + val map = FastSafeIterableMap() + map.putIfAbsent("a", 1) + map.putIfAbsent("b", 2) + + val results = mutableListOf() + map.forEachWithAdditions { outer -> + results.add("outer:${outer.key}") + map.forEachWithAdditions { inner -> + results.add("inner:${inner.key}") + if (inner.key == "a" && outer.key == "a") { + map.remove("a") + } + } + } + + assertThat(results) + .containsExactly("outer:a", "inner:a", "inner:b", "outer:b", "inner:b") + .inOrder() + } + + @Test + @Ignore // TODO(mgalhardo): Android and CMP throws different exceptions today. + fun emptyMapThrowsWithCustomMessage() { + val map = FastSafeIterableMap() + + val e1 = assertFailsWith { map.first() } + assertThat(e1.message).contains("Collection is empty.") + + val e2 = assertFailsWith { map.last() } + assertThat(e2.message).contains("Collection is empty.") + } +} From cb3a0a326752a4b39d2fcb6af4f8cb2131fa2584 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 22 Dec 2025 17:26:31 +0100 Subject: [PATCH 03/19] Add dependency on androidx.collection Adds the `androidx.collection` library as an implementation dependency. This enables the usage of optimized data structures, such as `ScatterMap`. Bug: N/A Test: existing passes Change-Id: I8652d78ebd39879e5e7d5e29ccdd262e009122fd --- lifecycle/lifecycle-runtime/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle index b49016af7e22b..41ed4ddd0ca3e 100644 --- a/lifecycle/lifecycle-runtime/build.gradle +++ b/lifecycle/lifecycle-runtime/build.gradle @@ -49,6 +49,7 @@ androidXMultiplatform { commonMain.dependencies { api(project(":lifecycle:lifecycle-common")) api("androidx.annotation:annotation:1.9.1") + implementation("androidx.collection:collection:1.5.0") } commonTest.dependencies { From 01de3bdbc87cde2e2dc3956c6a83e8f740200b3a Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 5 Jan 2026 17:17:57 +0100 Subject: [PATCH 04/19] Optimize FastSafeIterableMap for zero-allocation iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the standard `LinkedHashMap` delegate with a custom intrusive doubly-linked list backed by `MutableScatterMap`. Previously, safe iteration required snapshotting keys into a list to avoid `ConcurrentModificationException`. This change introduces "Ghost Nodes"—removed entries that retain their pointers—allowing active iterators to bridge back to the live list without copying data. This results in zero-allocation iteration while maintaining safety when observers modify the lifecycle during event dispatch. Bug: N/A Test: existing passes Change-Id: I284858aadef483f197694ebf1980ed0b31b3cef4 --- .../androidx/lifecycle/FastSafeIterableMap.kt | 225 +++++++++++------- 1 file changed, 143 insertions(+), 82 deletions(-) diff --git a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt index 91c9175197caf..9f8e868219947 100644 --- a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt +++ b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt @@ -16,117 +16,178 @@ package androidx.lifecycle +import androidx.collection.MutableScatterMap + +/** + * An ordered, map-based collection specifically designed for the Android Lifecycle observer + * pattern. + * + * **The Problem:** Lifecycle observers frequently trigger events that cause other observers to be + * added or removed while the collection is being iterated. Standard collections like + * `LinkedHashMap` throw `ConcurrentModificationException` or require expensive iterator allocations + * to handle this. + * + * **The Solution:** This map uses an intrusive doubly-linked list. When an entry is removed, it is + * unlinked from the main chain but **retains its next/prev pointers**. This creates "Ghost Nodes" + * that allow active iterators to safely traverse back to the live list without complex state + * tracking or index shifting. + * + * Constraints: + * - NOT thread-safe. + * - Optimized for high-frequency iteration and zero-allocation modification. + */ internal actual class FastSafeIterableMap { - private val delegate = linkedMapOf() + /** Lookup table for O(1) access. Stores the intrusive entries. */ + private val map = MutableScatterMap>() - actual fun contains(key: K): Boolean { - return delegate.containsKey(key) - } + /** The start of the intrusive linked list. Used as the entry point for forward iteration. */ + private var head: Entry? = null + /** + * The end of the intrusive linked list. New entries are appended here to maintain insertion + * order. + */ + private var tail: Entry? = null + + actual fun contains(key: K): Boolean = map.containsKey(key) + + /** + * Adds a value only if the key is not already present. Appends to the tail to ensure iteration + * follows insertion order. + */ actual fun putIfAbsent(key: K, value: V): V? { - val existing = delegate[key] + val existing = map[key] if (existing != null) { - return existing + return existing.value } - delegate[key] = value - return null - } - actual fun remove(key: K): V? { - return delegate.remove(key) + val newEntry = Entry(key, value) + map[key] = newEntry + + if (tail == null) { + head = newEntry + tail = newEntry + } else { + tail?.next = newEntry + newEntry.prev = tail + tail = newEntry + } + return null } /** - * In FastSafeIterableMap (Android), `ceil` returns the PREVIOUS entry. We replicate that - * behavior here. + * Removes the entry and marks it as a "Ghost Node". + * + * We intentionally do not null out the entry's [Entry.next] and [Entry.prev] pointers. If a + * loop is currently processing this entry, those pointers serve as the "bridge" back to the + * remaining elements in the map. */ - actual fun ceil(key: K): Map.Entry? { - if (!contains(key)) return null + actual fun remove(key: K): V? { + val entry = map.remove(key) ?: return null - var previous: Map.Entry? = null + // Unlink from the live chain. + if (entry.prev == null) head = entry.next else entry.prev?.next = entry.next + if (entry.next == null) tail = entry.prev else entry.next?.prev = entry.prev - // Iterate over keys to avoid holding invalidatable Entry references. - for (currentKey in delegate.keys) { - if (currentKey == key) { - return previous - } - // Snapshot the value to ensure we return a stable entry - val value = delegate[currentKey] - if (value != null) { - previous = Entry(currentKey, value) - } - } - return null - } + // Marks this node as dead. Iterators check this flag to skip removed + // elements that are still in their traversal path. + entry.markRemoved() - actual fun first(): Map.Entry { - return delegate.entries.first() + return entry.value } - actual fun last(): Map.Entry { - return delegate.entries.last() - } + /** + * Returns the entry that was inserted immediately before the entry associated with the given + * [key], or null if no such entry exists. + * + * Note: Despite the name 'ceil', this retrieves the logical predecessor to support specific + * Lifecycle iteration patterns. + */ + actual fun ceil(key: K): Map.Entry? = map[key]?.prev - actual fun lastOrNull(): Map.Entry? { - return delegate.entries.lastOrNull() - } + /** + * Returns the first entry in the map. + * + * @throws IllegalArgumentException if the map is empty. + */ + actual fun first(): Map.Entry = + head ?: throw NoSuchElementException("Collection is empty.") - actual fun size(): Int { - return delegate.size - } + /** + * Returns the last entry in the map. + * + * @throws IllegalArgumentException if the map is empty. + */ + actual fun last(): Map.Entry = + tail ?: throw NoSuchElementException("Collection is empty.") - actual fun forEachWithAdditions(action: (Map.Entry) -> Unit) { - val visited = mutableSetOf() - - // Snapshot KEYS, not entries. Keys are safe immutable references. - // Copying to a list prevents CME on the iterator itself. - var candidates = delegate.keys.toList() - - while (candidates.isNotEmpty()) { - for (key in candidates) { - // Check if we already visited this key (optimization) - if (visited.add(key)) { - // Re-fetch value from the live map. - // If returns null, the item was removed during the loop -> skip it. - val value = delegate[key] - if (value != null) { - action(Entry(key, value)) - } - } - } + actual fun lastOrNull(): Map.Entry? = tail + + /** Current count of live (non-removed) elements. */ + actual fun size(): Int = map.size - // If the map grew while we were looping, we need to process the new additions. - if (delegate.size > visited.size) { - candidates = delegate.keys.filter { !visited.contains(it) } - } else { - break + /** + * Iterates forward through the map. Safe to add or remove elements (including the current one) + * during execution. + * + * New elements added during iteration will be visited if they are appended after the current + * position. + */ + actual fun forEachWithAdditions(action: (Map.Entry) -> Unit) { + var current = head + while (current != null) { + // We check isRemoved because a previous step in this loop might + // have removed 'current' or a future element we haven't reached yet. + if (!current.isRemoved) { + action(current) } + // Move to next AFTER action. If current was removed, its 'next' + // pointer still acts as a bridge to the rest of the list. + current = current.next } } + /** + * Iterates in reverse order. Modification-safe via the same 'Ghost Node' logic used in forward + * iteration. + */ actual fun forEachReversed(action: (Map.Entry) -> Unit) { - // Start with a safe snapshot of the current keys - val keys = delegate.keys.toList() - - // Iterate by index to avoid iterator allocation - var index = keys.size - 1 - while (index >= 0) { - val key = keys[index] - - // Re-fetch value safely and guard against removal during iteration - val value = delegate[key] - if (value != null) { - action(Entry(key, value)) + var current = tail + while (current != null) { + // We check isRemoved because a previous step in this loop might + // have removed 'current' or a future element we haven't reached yet. + if (!current.isRemoved) { + action(current) } - index-- + // Move to previous AFTER action. If current was removed, its 'previous' + // pointer still acts as a bridge to the rest of the list. + current = current.prev } } - /** - * A simple immutable implementation of Map.Entry. Used to pass safe snapshots to callers, - * preventing crashes if the backing map changes. - */ - private data class Entry(override val key: K, override val value: V) : Map.Entry + /** An intrusive doubly-linked list node that also serves as a [Map.Entry]. */ + private data class Entry(override val key: K, override val value: V) : Map.Entry { + + // Note: We retain next/prev pointers even after removal to allow active + // iterators to "bridge" back to the live list from this node. + var next: Entry? = null + var prev: Entry? = null + + /** + * Indicates this entry is no longer in the Map. Once true, it can never be set back to + * false. + */ + var isRemoved: Boolean = false + private set + + /** + * Marks this entry as a "Ghost Node." It remains linked to its neighbors to support + * iterator safety, but will be skipped during traversal. + */ + fun markRemoved() { + isRemoved = true + } + } } From 960420da5485f3b2f9657cb129e2efc4f694074f Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 5 Jan 2026 17:22:14 +0100 Subject: [PATCH 05/19] Use single FastSafeIterableMap implementation Replaces platform-specific `expect`/`actual` implementations with a single pure Kotlin implementation in the common source set. This unifies behavior across all targets (Android, JVM, Native) and removes the dependency on `androidx.arch.core` for the Android source set, simplifying maintenance and ensuring consistent iteration safety logic. Bug: N/A Test: existing passes Change-Id: I662c67c5c286178892a4acf910103e96f85ed736 --- .../androidx/lifecycle/FastSafeIterableMap.kt | 175 ++++++++++++++-- .../lifecycle/FastSafeIterableMapTest.kt | 3 - .../androidx/lifecycle/FastSafeIterableMap.kt | 63 ------ .../androidx/lifecycle/FastSafeIterableMap.kt | 193 ------------------ 4 files changed, 163 insertions(+), 271 deletions(-) delete mode 100644 lifecycle/lifecycle-runtime/src/jvmAndAndroidMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt delete mode 100644 lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt diff --git a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt index c10e0537602aa..6c3132f1fb99a 100644 --- a/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt +++ b/lifecycle/lifecycle-runtime/src/commonMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -16,25 +16,176 @@ package androidx.lifecycle -internal expect class FastSafeIterableMap() { +import androidx.collection.MutableScatterMap - fun contains(key: K): Boolean +/** + * An ordered, map-based collection specifically designed for the Android Lifecycle observer + * pattern. + * + * **The Problem:** Lifecycle observers frequently trigger events that cause other observers to be + * added or removed while the collection is being iterated. Standard collections like + * `LinkedHashMap` throw `ConcurrentModificationException` or require expensive iterator allocations + * to handle this. + * + * **The Solution:** This map uses an intrusive doubly-linked list. When an entry is removed, it is + * unlinked from the main chain but **retains its next/prev pointers**. This creates "Ghost Nodes" + * that allow active iterators to safely traverse back to the live list without complex state + * tracking or index shifting. + * + * Constraints: + * - NOT thread-safe. + * - Optimized for high-frequency iteration and zero-allocation modification. + */ +internal class FastSafeIterableMap { + + /** Lookup table for O(1) access. Stores the intrusive entries. */ + private val map = MutableScatterMap>() + + /** The start of the intrusive linked list. Used as the entry point for forward iteration. */ + private var head: Entry? = null + + /** + * The end of the intrusive linked list. New entries are appended here to maintain insertion + * order. + */ + private var tail: Entry? = null + + operator fun contains(key: K): Boolean = map.containsKey(key) + + /** + * Adds a value only if the key is not already present. Appends to the tail to ensure iteration + * follows insertion order. + */ + fun putIfAbsent(key: K, value: V): V? { + val existing = map[key] + if (existing != null) { + return existing.value + } + + val newEntry = Entry(key, value) + map[key] = newEntry + + if (tail == null) { + head = newEntry + tail = newEntry + } else { + tail?.next = newEntry + newEntry.prev = tail + tail = newEntry + } + return null + } + + /** + * Removes the entry and marks it as a "Ghost Node". + * + * We intentionally do not null out the entry's [Entry.next] and [Entry.prev] pointers. If a + * loop is currently processing this entry, those pointers serve as the "bridge" back to the + * remaining elements in the map. + */ + fun remove(key: K): V? { + val entry = map.remove(key) ?: return null + + // Unlink from the live chain. + if (entry.prev == null) head = entry.next else entry.prev?.next = entry.next + if (entry.next == null) tail = entry.prev else entry.next?.prev = entry.prev + + // Marks this node as dead. Iterators check this flag to skip removed + // elements that are still in their traversal path. + entry.markRemoved() + + return entry.value + } + + /** + * Returns the entry that was inserted immediately before the entry associated with the given + * [key], or null if no such entry exists. + * + * Note: Despite the name 'ceil', this retrieves the logical predecessor to support specific + * Lifecycle iteration patterns. + */ + fun ceil(key: K): Map.Entry? = map[key]?.prev + + /** + * Returns the first entry in the map. + * + * @throws IllegalArgumentException if the map is empty. + */ + fun first(): Map.Entry = head ?: throw NoSuchElementException("Collection is empty.") + + /** + * Returns the last entry in the map. + * + * @throws IllegalArgumentException if the map is empty. + */ + fun last(): Map.Entry = tail ?: throw NoSuchElementException("Collection is empty.") - fun putIfAbsent(key: K, value: V): V? + fun lastOrNull(): Map.Entry? = tail - fun remove(key: K): V? + /** Current count of live (non-removed) elements. */ + fun size(): Int = map.size - fun ceil(key: K): Map.Entry? + /** + * Iterates forward through the map. Safe to add or remove elements (including the current one) + * during execution. + * + * New elements added during iteration will be visited if they are appended after the current + * position. + */ + fun forEachWithAdditions(action: (Map.Entry) -> Unit) { + var current = head + while (current != null) { + // We check isRemoved because a previous step in this loop might + // have removed 'current' or a future element we haven't reached yet. + if (!current.isRemoved) { + action(current) + } + // Move to next AFTER action. If current was removed, its 'next' + // pointer still acts as a bridge to the rest of the list. + current = current.next + } + } - fun first(): Map.Entry + /** + * Iterates in reverse order. Modification-safe via the same 'Ghost Node' logic used in forward + * iteration. + */ + fun forEachReversed(action: (Map.Entry) -> Unit) { + var current = tail + while (current != null) { + // We check isRemoved because a previous step in this loop might + // have removed 'current' or a future element we haven't reached yet. + if (!current.isRemoved) { + action(current) + } - fun last(): Map.Entry + // Move to previous AFTER action. If current was removed, its 'previous' + // pointer still acts as a bridge to the rest of the list. + current = current.prev + } + } - fun lastOrNull(): Map.Entry? + /** An intrusive doubly-linked list node that also serves as a [Map.Entry]. */ + private data class Entry(override val key: K, override val value: V) : Map.Entry { - fun size(): Int + // Note: We retain next/prev pointers even after removal to allow active + // iterators to "bridge" back to the live list from this node. + var next: Entry? = null + var prev: Entry? = null - fun forEachWithAdditions(action: (Map.Entry) -> Unit) + /** + * Indicates this entry is no longer in the Map. Once true, it can never be set back to + * false. + */ + var isRemoved: Boolean = false + private set - fun forEachReversed(action: (Map.Entry) -> Unit) + /** + * Marks this entry as a "Ghost Node." It remains linked to its neighbors to support + * iterator safety, but will be skipped during traversal. + */ + fun markRemoved() { + isRemoved = true + } + } } diff --git a/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt b/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt index 733c29fa9fb64..d4203e0cf30c3 100644 --- a/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt +++ b/lifecycle/lifecycle-runtime/src/commonTest/kotlin/androidx/lifecycle/FastSafeIterableMapTest.kt @@ -17,7 +17,6 @@ package androidx.lifecycle import androidx.kruth.assertThat -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertFailsWith @@ -103,7 +102,6 @@ class FastSafeIterableMapTest { } @Test - @Ignore // TODO(mgalhardo): Android implementation doesn't support this case. fun reAddRemovedElementDuringIteration() { val map = FastSafeIterableMap() map.putIfAbsent("a", 1) @@ -144,7 +142,6 @@ class FastSafeIterableMapTest { } @Test - @Ignore // TODO(mgalhardo): Android and CMP throws different exceptions today. fun emptyMapThrowsWithCustomMessage() { val map = FastSafeIterableMap() diff --git a/lifecycle/lifecycle-runtime/src/jvmAndAndroidMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt b/lifecycle/lifecycle-runtime/src/jvmAndAndroidMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt deleted file mode 100644 index c98475cd38628..0000000000000 --- a/lifecycle/lifecycle-runtime/src/jvmAndAndroidMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt +++ /dev/null @@ -1,63 +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.lifecycle - -@Suppress("RestrictedApi") -internal actual class FastSafeIterableMap { - - private val delegate = androidx.arch.core.internal.FastSafeIterableMap() - - actual fun contains(key: K): Boolean { - return delegate.contains(key) - } - - actual fun putIfAbsent(key: K, value: V): V? { - return delegate.putIfAbsent(key, value) - } - - actual fun remove(key: K): V? { - return delegate.remove(key) - } - - actual fun ceil(key: K): Map.Entry? { - return delegate.ceil(key) - } - - actual fun first(): Map.Entry { - return requireNotNull(delegate.eldest()) - } - - actual fun last(): Map.Entry { - return requireNotNull(delegate.newest()) - } - - actual fun lastOrNull(): Map.Entry? { - return delegate.newest() - } - - actual fun size(): Int { - return delegate.size() - } - - actual fun forEachWithAdditions(action: (Map.Entry) -> Unit) { - delegate.iteratorWithAdditions().forEach(action) - } - - actual fun forEachReversed(action: (Map.Entry) -> Unit) { - delegate.descendingIterator().forEach(action) - } -} diff --git a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt b/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt deleted file mode 100644 index 9f8e868219947..0000000000000 --- a/lifecycle/lifecycle-runtime/src/nonJvmMain/kotlin/androidx/lifecycle/FastSafeIterableMap.kt +++ /dev/null @@ -1,193 +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.lifecycle - -import androidx.collection.MutableScatterMap - -/** - * An ordered, map-based collection specifically designed for the Android Lifecycle observer - * pattern. - * - * **The Problem:** Lifecycle observers frequently trigger events that cause other observers to be - * added or removed while the collection is being iterated. Standard collections like - * `LinkedHashMap` throw `ConcurrentModificationException` or require expensive iterator allocations - * to handle this. - * - * **The Solution:** This map uses an intrusive doubly-linked list. When an entry is removed, it is - * unlinked from the main chain but **retains its next/prev pointers**. This creates "Ghost Nodes" - * that allow active iterators to safely traverse back to the live list without complex state - * tracking or index shifting. - * - * Constraints: - * - NOT thread-safe. - * - Optimized for high-frequency iteration and zero-allocation modification. - */ -internal actual class FastSafeIterableMap { - - /** Lookup table for O(1) access. Stores the intrusive entries. */ - private val map = MutableScatterMap>() - - /** The start of the intrusive linked list. Used as the entry point for forward iteration. */ - private var head: Entry? = null - - /** - * The end of the intrusive linked list. New entries are appended here to maintain insertion - * order. - */ - private var tail: Entry? = null - - actual fun contains(key: K): Boolean = map.containsKey(key) - - /** - * Adds a value only if the key is not already present. Appends to the tail to ensure iteration - * follows insertion order. - */ - actual fun putIfAbsent(key: K, value: V): V? { - val existing = map[key] - if (existing != null) { - return existing.value - } - - val newEntry = Entry(key, value) - map[key] = newEntry - - if (tail == null) { - head = newEntry - tail = newEntry - } else { - tail?.next = newEntry - newEntry.prev = tail - tail = newEntry - } - return null - } - - /** - * Removes the entry and marks it as a "Ghost Node". - * - * We intentionally do not null out the entry's [Entry.next] and [Entry.prev] pointers. If a - * loop is currently processing this entry, those pointers serve as the "bridge" back to the - * remaining elements in the map. - */ - actual fun remove(key: K): V? { - val entry = map.remove(key) ?: return null - - // Unlink from the live chain. - if (entry.prev == null) head = entry.next else entry.prev?.next = entry.next - if (entry.next == null) tail = entry.prev else entry.next?.prev = entry.prev - - // Marks this node as dead. Iterators check this flag to skip removed - // elements that are still in their traversal path. - entry.markRemoved() - - return entry.value - } - - /** - * Returns the entry that was inserted immediately before the entry associated with the given - * [key], or null if no such entry exists. - * - * Note: Despite the name 'ceil', this retrieves the logical predecessor to support specific - * Lifecycle iteration patterns. - */ - actual fun ceil(key: K): Map.Entry? = map[key]?.prev - - /** - * Returns the first entry in the map. - * - * @throws IllegalArgumentException if the map is empty. - */ - actual fun first(): Map.Entry = - head ?: throw NoSuchElementException("Collection is empty.") - - /** - * Returns the last entry in the map. - * - * @throws IllegalArgumentException if the map is empty. - */ - actual fun last(): Map.Entry = - tail ?: throw NoSuchElementException("Collection is empty.") - - actual fun lastOrNull(): Map.Entry? = tail - - /** Current count of live (non-removed) elements. */ - actual fun size(): Int = map.size - - /** - * Iterates forward through the map. Safe to add or remove elements (including the current one) - * during execution. - * - * New elements added during iteration will be visited if they are appended after the current - * position. - */ - actual fun forEachWithAdditions(action: (Map.Entry) -> Unit) { - var current = head - while (current != null) { - // We check isRemoved because a previous step in this loop might - // have removed 'current' or a future element we haven't reached yet. - if (!current.isRemoved) { - action(current) - } - // Move to next AFTER action. If current was removed, its 'next' - // pointer still acts as a bridge to the rest of the list. - current = current.next - } - } - - /** - * Iterates in reverse order. Modification-safe via the same 'Ghost Node' logic used in forward - * iteration. - */ - actual fun forEachReversed(action: (Map.Entry) -> Unit) { - var current = tail - while (current != null) { - // We check isRemoved because a previous step in this loop might - // have removed 'current' or a future element we haven't reached yet. - if (!current.isRemoved) { - action(current) - } - - // Move to previous AFTER action. If current was removed, its 'previous' - // pointer still acts as a bridge to the rest of the list. - current = current.prev - } - } - - /** An intrusive doubly-linked list node that also serves as a [Map.Entry]. */ - private data class Entry(override val key: K, override val value: V) : Map.Entry { - - // Note: We retain next/prev pointers even after removal to allow active - // iterators to "bridge" back to the live list from this node. - var next: Entry? = null - var prev: Entry? = null - - /** - * Indicates this entry is no longer in the Map. Once true, it can never be set back to - * false. - */ - var isRemoved: Boolean = false - private set - - /** - * Marks this entry as a "Ghost Node." It remains linked to its neighbors to support - * iterator safety, but will be skipped during traversal. - */ - fun markRemoved() { - isRemoved = true - } - } -} From e241ca4c1372e23f8a59c6750e3e0bd1881ca59a Mon Sep 17 00:00:00 2001 From: Mariano Martin Date: Wed, 7 Jan 2026 16:22:47 -0500 Subject: [PATCH 06/19] [TimePicker] Added sound and haptic feedback to time input errors Test: Tested manually Change-Id: I9b5ec04f468ebc15bfff13e833f48d309dd9e364 --- .../compose/material3/TimePicker.android.kt | 41 +++++++++++++++++++ .../androidx/compose/material3/TimePicker.kt | 30 +++++++++++--- .../material3/TimePicker.commonStubs.kt | 21 ++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimePicker.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimePicker.android.kt index c6e5befc98429..b52af0bacae05 100644 --- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimePicker.android.kt +++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TimePicker.android.kt @@ -16,9 +16,17 @@ package androidx.compose.material3 +import android.content.Context +import android.media.AudioManager import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.hapticfeedback.HapticFeedbackType.Companion.Reject import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -31,3 +39,36 @@ internal actual fun defaultTimePickerLayoutType(): TimePickerLayoutType = TimePickerLayoutType.Vertical } } + +@Composable +internal actual fun rememberTimeInputErrorHandler( + isTouchExplorationEnabled: Boolean +): TimeInputErrorHandler { + val context = LocalContext.current + val haptics = LocalHapticFeedback.current + val audioManager = + remember(context) { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + + return remember(haptics, audioManager, isTouchExplorationEnabled) { + TimeInputErrorHandlerImpl( + haptics = haptics, + audioManager = audioManager, + isTouchExplorationEnabled = isTouchExplorationEnabled, + ) + } +} + +private class TimeInputErrorHandlerImpl( + private val haptics: HapticFeedback, + private val audioManager: AudioManager, + private val isTouchExplorationEnabled: Boolean, +) : TimeInputErrorHandler { + + override fun onError() { + haptics.performHapticFeedback(HapticFeedbackType.Reject) + + if (!isTouchExplorationEnabled) { + audioManager.playSoundEffect(AudioManager.FX_KEYPRESS_INVALID, 0.5f) + } + } +} diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt index a217be989bf49..698786a5862a3 100644 --- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt +++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt @@ -164,10 +164,12 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalInputModeManager import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.maxTextLength import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.role @@ -1130,6 +1132,7 @@ private fun TimeInputImpl(modifier: Modifier, colors: TimePickerColors, state: T ) val a11yServicesEnabled by rememberAccessibilityServiceState() + val errorHandler = rememberTimeInputErrorHandler(a11yServicesEnabled) CompositionLocalProvider( LocalTextStyle provides textStyle, @@ -1161,6 +1164,7 @@ private fun TimeInputImpl(modifier: Modifier, colors: TimePickerColors, state: T prevValue = hourValue, a11yServicesEnabled = a11yServicesEnabled, userOverride = userOverride, + errorHandler = errorHandler, ) { hourValue = it } @@ -1192,9 +1196,9 @@ private fun TimeInputImpl(modifier: Modifier, colors: TimePickerColors, state: T prevValue = minuteValue, userOverride = userOverride, a11yServicesEnabled = a11yServicesEnabled, - ) { - minuteValue = it - } + errorHandler = errorHandler, + { minuteValue = it }, + ) }, state = state, selection = TimePickerSelectionMode.Minute, @@ -1500,6 +1504,8 @@ private fun TimeSelector( colors: TimePickerColors, isValid: Boolean, ) { + LaunchedEffect(isValid) { if (!isValid) {} } + val selected = state.selection == selection val selectorContentDescription = getString( @@ -1941,6 +1947,7 @@ private fun timeInputOnChange( prevValue: TextFieldValue, userOverride: Ref, a11yServicesEnabled: Boolean, + errorHandler: TimeInputErrorHandler, onNewValue: (value: TextFieldValue) -> Unit, ) { userOverride.value = false @@ -1992,9 +1999,11 @@ private fun timeInputOnChange( value.copy(text = value.text[0].toString()) } ) + } else { + errorHandler.onError() } } catch (_: NumberFormatException) {} catch (_: IllegalArgumentException) { - // do nothing no state update + errorHandler.onError() } } @@ -2081,7 +2090,7 @@ private fun TimePickerTextField( state = state, selection = selection, colors = colors, - isValid, + isValid = isValid, ) } @@ -2146,7 +2155,7 @@ private fun TimePickerTextField( } SupportingText( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().semantics { liveRegion = LiveRegionMode.Polite }, selection = selection, state = state, isValid = isValid, @@ -2346,3 +2355,12 @@ internal class ClockFaceSizeModifier : LayoutModifier { return layout(placeable.width, placeable.height) { placeable.place(0, 0) } } } + +internal interface TimeInputErrorHandler { + fun onError() +} + +@Composable +internal expect fun rememberTimeInputErrorHandler( + isTouchExplorationEnabled: Boolean +): TimeInputErrorHandler diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/TimePicker.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/TimePicker.commonStubs.kt index 70092df7a67f2..92f32e177e75d 100644 --- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/TimePicker.commonStubs.kt +++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/TimePicker.commonStubs.kt @@ -18,6 +18,10 @@ package androidx.compose.material3 import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback @OptIn(ExperimentalMaterial3Api::class) @ReadOnlyComposable @@ -25,3 +29,20 @@ import androidx.compose.runtime.ReadOnlyComposable internal actual fun defaultTimePickerLayoutType(): TimePickerLayoutType { implementedInJetBrainsFork() } + +@Composable +internal actual fun rememberTimeInputErrorHandler( + isTouchExplorationEnabled: Boolean +): TimeInputErrorHandler { + val haptics = LocalHapticFeedback.current + + return remember(haptics) { TimeInputErrorHandlerImpl(haptics = haptics) } +} + +private class TimeInputErrorHandlerImpl(private val haptics: HapticFeedback) : + TimeInputErrorHandler { + + override fun onError() { + haptics.performHapticFeedback(HapticFeedbackType.Reject) + } +} From df9bc3b534b979be39b64cd91cab6e593050a9ef Mon Sep 17 00:00:00 2001 From: Aidan Melvin Date: Mon, 12 Jan 2026 09:41:20 -0500 Subject: [PATCH 07/19] Add Java JVM to project-creator script Add support for java jvm project type Bug: 468088488 Test: manual and ProjectCreatorTaskTest.kt Change-Id: I615637f9a83a06bc58e60382ed5d0ac86956f4eb --- .../androidx/build/ProjectCreatorTask.kt | 113 +++++++++++------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt index 77e71d8cd1acf..dd0b6e0a3283a 100644 --- a/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt +++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt @@ -105,7 +105,6 @@ abstract class ProjectCreatorTask : DefaultTask() { getPackageDocumentationFilename( projectSpec.groupIdWithPrefix, projectSpec.artifactId, - projectSpec.projectType, ) ) @@ -154,10 +153,10 @@ internal class GradleSettingsEditor(val settingsGradleFile: File) { } private fun getBuildType(spec: ProjectSpec): String { - return if (spec.projectType == ProjectType.KMP) { - "KMP" - } else if (isComposeProject(spec.groupId, spec.artifactId)) { + return if (isComposeProject(spec.groupId, spec.artifactId)) { "COMPOSE" + } else if (spec.projectType == ProjectType.KMP) { + "KMP" } else { "MAIN" } @@ -352,32 +351,49 @@ internal class ProjectGenerator { private fun createSrcDir(spec: ProjectSpec) { val basePath = if (spec.projectType == ProjectType.KMP) "src/commonMain" else "src/main" + val fullPath = + "$basePath/${spec.projectType.getLanguage()}/androidx/${ + spec.groupId.replace( + ".", + "/", + ) + }" val docFile = File( spec.fullArtifactPath, - "$basePath/kotlin/androidx/${spec.groupId.replace(".", "/")}/androidx-${ - spec.groupId.replace( - ".", - "-", - ) - }-${spec.artifactId}-documentation.md", + "$fullPath/${getPackageDocumentationFilename(spec.groupId, spec.artifactId)}", ) docFile.parentFile.mkdirs() docFile.writeText(spec.toPackageDocsText()) + if (spec.projectType == ProjectType.JAVA) { + val packageInfoFile = File(spec.fullArtifactPath, "$fullPath/package-info.java") + + packageInfoFile.writeText(spec.getPackageInfoFileText()) + } + if (spec.projectType == ProjectType.KMP) { val testFile = File( spec.fullArtifactPath, - "src/commonTest/kotlin/androidx/${spec.groupId.replace(".", "/")}/Test.kt", + "${fullPath.replace("commonMain", "commonTest")}/Test.kt", ) testFile.parentFile.mkdirs() testFile.writeText(spec.createTestFileText()) } } - private fun ProjectSpec.createTestFileText(): String { + private fun ProjectSpec.getPackageInfoFileText(): String { + return """ + ${getAOSPHeader()} + + package androidx.${groupId}.${artifactId.replace("-", ".")} + """ + .trimIndent() + } + + private fun getAOSPHeader(): String { return """ /* * Copyright ${getYear()} The Android Open Source Project @@ -394,6 +410,12 @@ internal class ProjectGenerator { * See the License for the specific language governing permissions and * limitations under the License. */ + """ + } + + private fun ProjectSpec.createTestFileText(): String { + return """ + ${getAOSPHeader()} package androidx.${groupId} class Test { @@ -417,21 +439,7 @@ internal class ProjectGenerator { private fun ProjectSpec.getBuildGradleText(isGroupIdAtomic: Boolean): String { return """ - /* - * Copyright (C) ${getYear()} 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. - */ + ${getAOSPHeader()} /** * This file was created using the `createProject` gradle task (./gradlew createProject) @@ -444,14 +452,16 @@ internal class ProjectGenerator { plugins { id("AndroidXPlugin") - ${if (projectType == ProjectType.KMP) "" else "id(\"com.android.library\")"} + ${projectType.getGradlePlugin()} } dependencies { // Add dependencies here } - ${if (projectType == ProjectType.KMP) """ + ${ + when (projectType) { + ProjectType.KMP -> """ androidXMultiplatform { ios() js() @@ -473,11 +483,20 @@ internal class ProjectGenerator { } } } - """ else """ + """ + ProjectType.ANDROID_LIBRARY -> """ android { namespace = "${generatePackageName(groupId, artifactId)}" } - """} + """ + ProjectType.JAVA -> """ + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + """ + } + } androidx { name = "${groupId}:${artifactId}" @@ -490,6 +509,22 @@ internal class ProjectGenerator { .trimIndent() } + private fun ProjectType.getGradlePlugin(): String { + return when (this) { + ProjectType.ANDROID_LIBRARY -> """id("com.android.library")""" + ProjectType.KMP -> "" + ProjectType.JAVA -> """id("java-library")""" + } + } + + private fun ProjectType.getLanguage(): String { + return when (this) { + ProjectType.ANDROID_LIBRARY -> "kotlin" + ProjectType.KMP -> "kotlin" + ProjectType.JAVA -> "java" + } + } + private fun getYear(): String = LocalDate.now().year.toString() } @@ -502,8 +537,8 @@ private fun getPackageDocumentationFileDir(spec: ProjectSpec): File { ProjectType.KMP -> { "src/commonMain/kotlin/" } - else -> { - error("Project type not yet supported") + ProjectType.JAVA -> { + "src/main/java/" } } + spec.groupIdWithPrefix.replace('.', '/') return File(spec.fullArtifactPath, subPath) @@ -567,14 +602,6 @@ internal fun getLibraryType(artifactId: String): String = else -> "PUBLISHED_LIBRARY" } -internal fun getPackageDocumentationFilename( - groupId: String, - artifactId: String, - projectType: ProjectType, -): String { - return if (projectType == ProjectType.JAVA) { - "package-info.java" - } else { - "${groupId.replace('.', '-')}-$artifactId-documentation.md" - } +internal fun getPackageDocumentationFilename(groupId: String, artifactId: String): String { + return "androidx-${groupId.replace('.', '-')}-$artifactId-documentation.md" } From 283b17e49d43756ddbef4b12f64d81152034d7aa Mon Sep 17 00:00:00 2001 From: Darren Zhu Date: Sat, 20 Dec 2025 00:29:56 +0000 Subject: [PATCH 08/19] Remove billboard API and add samples -removed billboard API -removed billboard unit tests -added billboard, custom up vector, and nested within the parent container to the test app and samples Relnote: remove billboard API Bug: 468410272 Bug: 468411122 Test: Tested locally with running unit tests and Moohan emulator Change-Id: Ib76cd43fe8338322d20b062ef937538a8c7fd04d --- xr/compose/compose/api/current.txt | 1 - xr/compose/compose/api/restricted_current.txt | 1 - .../xr/compose/samples/LookAtUserSample.kt | 86 ++++++ .../xr/compose/subspace/layout/LookAtUser.kt | 27 +- .../compose/subspace/layout/LookAtUserTest.kt | 198 +++----------- .../testapp/lookatuser/LookAtUserActivity.kt | 254 +++++++++--------- .../testapp/src/main/res/values/strings.xml | 2 +- 7 files changed, 253 insertions(+), 316 deletions(-) create mode 100644 xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/LookAtUserSample.kt diff --git a/xr/compose/compose/api/current.txt b/xr/compose/compose/api/current.txt index 79a2a2db88800..c75b03f260ec8 100644 --- a/xr/compose/compose/api/current.txt +++ b/xr/compose/compose/api/current.txt @@ -562,7 +562,6 @@ package androidx.xr.compose.subspace.layout { } public final class LookAtUserKt { - method public static androidx.xr.compose.subspace.layout.SubspaceModifier billboard(androidx.xr.compose.subspace.layout.SubspaceModifier); method public static androidx.xr.compose.subspace.layout.SubspaceModifier lookAtUser(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.runtime.math.Vector3 up); method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier! lookAtUser$default(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.runtime.math.Vector3!, int, Object!); } diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt index d4adda068cf2e..fbe5304068f0a 100644 --- a/xr/compose/compose/api/restricted_current.txt +++ b/xr/compose/compose/api/restricted_current.txt @@ -578,7 +578,6 @@ package androidx.xr.compose.subspace.layout { } public final class LookAtUserKt { - method public static androidx.xr.compose.subspace.layout.SubspaceModifier billboard(androidx.xr.compose.subspace.layout.SubspaceModifier); method public static androidx.xr.compose.subspace.layout.SubspaceModifier lookAtUser(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.runtime.math.Vector3 up); method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier! lookAtUser$default(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.runtime.math.Vector3!, int, Object!); } diff --git a/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/LookAtUserSample.kt b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/LookAtUserSample.kt new file mode 100644 index 0000000000000..74043ce4c07bc --- /dev/null +++ b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/LookAtUserSample.kt @@ -0,0 +1,86 @@ +/* + * 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.xr.compose.samples + +import androidx.annotation.Sampled +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialBox +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.gravityAligned +import androidx.xr.compose.subspace.layout.lookAtUser +import androidx.xr.compose.subspace.layout.rotate +import androidx.xr.runtime.math.Quaternion +import androidx.xr.runtime.math.Vector3 + +@Sampled +public fun LookAtUserSamples() { + /** + * A sample demonstrating how to combine [lookAtUser] and [gravityAligned] to achieve billboard + * behavior where the content automatically rotates to face the user. [gravityAligned] ensures + * the panel stays vertically upright and does not tilt forward or backward, even if the user + * views it from a high or low angle. + */ + @Composable + fun LookAtUserBillboardSample() { + Subspace { + SpatialPanel(modifier = SubspaceModifier.lookAtUser().gravityAligned()) { + Text("I always face you and stay upright!") + } + } + } + + /** + * A sample showing how to use the 'up' parameter. By providing a custom up vector, you can + * change the reference frame for the content's orientation. + */ + @Composable + fun LookAtUserWithUpVectorSample() { + Subspace { + SpatialPanel( + modifier = + SubspaceModifier.lookAtUser( + up = Vector3(0f, 1f, 2f) + ) // A slightly tilted "up" reference + ) { + Text("I have a custom 'Up' vector.") + } + } + } + + /** + * A sample showing how [lookAtUser] behaves within a parent spatial layout. In this example, + * even if the [SpatialBox] is moved or rotated, the panel with [lookAtUser] will independently + * calculate its local rotation to ensure it remains facing the user. + */ + @Composable + fun LookAtUserUnderParentContainerSample() { + val parentRotation = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) + + Subspace { + SpatialBox(SubspaceModifier.rotate(parentRotation)) { + // This panel will rotate to face the user regardless of where + // the parent SpatialBox is placed in the ActivitySpace. + SpatialPanel(modifier = SubspaceModifier.lookAtUser()) { + Text("I'm inside a SpatialBox, but I still see you!") + } + } + } + } +} diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LookAtUser.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LookAtUser.kt index 2c04bab146e2b..2a29918d66d4b 100644 --- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LookAtUser.kt +++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LookAtUser.kt @@ -38,27 +38,6 @@ import androidx.xr.scenecore.scene import kotlinx.coroutines.Job import kotlinx.coroutines.launch -/** - * A [SubspaceModifier] that forces the content to remain upright and will rotate on the y-axis that - * the content faces the user at all times. - * - * A user of this API should configure the activity's Session object with - * [Config.DeviceTrackingMode.LAST_KNOWN] which requires android.permission.HEAD_TRACKING Android - * permission be granted by the calling application. `session.configure( config = - * session.config.copy(headTracking = Config.HeadTrackingMode.LAST_KNOWN) )` - * - * This modifier might not work as expected when used on content within a - * [androidx.xr.compose.spatial.UserSubspace]. - * - * The preceding rotate modifiers will be disregarded because this modifier will override them. But - * the rotate after the lookAtUser modifier will be respected. - * - * @see lookAtUser modifier for making content that will tilt in all directions to face the user. - */ -// TODO(b/461808266): LookAtUser and UserSubspace not compatible with each other -public fun SubspaceModifier.billboard(): SubspaceModifier = - this.then(SubspaceModifier.lookAtUser().gravityAligned()) - /** * A [SubspaceModifier] that continuously rotates content so that it faces the user at all times. * @@ -73,12 +52,14 @@ public fun SubspaceModifier.billboard(): SubspaceModifier = * The preceding rotate modifiers will be disregarded because this modifier will override them. But * the rotate after the lookAtUser modifier will be respected. * + * To achieve a "billboard" effect—where the content rotates to face the user on the Y-axis while + * remaining upright and aligned with gravity—combine this with [gravityAligned]. + * + * @sample androidx.xr.compose.samples.LookAtUserSamples * @param up Defines the reference "up" direction for the content's orientation. Pointing the * content's forward vector at the user leaves the rotation around that axis (roll) undefined; * this vector resolves that ambiguity. The default is Vector3.Up, which corresponds to the up * direction of the ActivitySpace. - * @see billboard modifier for making content that will generally face the user's direction but - * keeps the content in an upright position. */ // TODO(b/461808266): LookAtUser and UserSubspace not compatible with each other // TODO(b/468104384): Optimize LookAtUser modifier initial rotation delay diff --git a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/LookAtUserTest.kt b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/LookAtUserTest.kt index 9e62c64c769b3..dbb6c3627eed6 100644 --- a/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/LookAtUserTest.kt +++ b/xr/compose/compose/src/test/kotlin/androidx/xr/compose/subspace/layout/LookAtUserTest.kt @@ -30,14 +30,17 @@ import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.semantics.testTag import androidx.xr.compose.testing.SubspaceTestingActivity import androidx.xr.compose.testing.assertRotationInRootIsEqualTo -import androidx.xr.compose.testing.configureFakeSession import androidx.xr.compose.testing.onSubspaceNodeWithTag +import androidx.xr.compose.testing.session import androidx.xr.runtime.Config +import androidx.xr.runtime.Session +import androidx.xr.runtime.SessionCreateSuccess import androidx.xr.runtime.math.Quaternion import androidx.xr.runtime.math.Quaternion.Companion.fromRotation import androidx.xr.runtime.math.Vector3 import androidx.xr.scenecore.Entity import androidx.xr.scenecore.Space +import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest @@ -67,48 +70,6 @@ class LookAtUserTest { activity = activityController.get() } - @Test - fun billboard_userTranslationChanges_contentTurnsTowardsUser() = - runTest(testDispatcher) { - val fakePerceptionManager = createSessionAndGetPerceptionManager() - - composeTestRule.setContent { - Subspace { - SpatialPanel(SubspaceModifier.testTag("TheWatcher").billboard()) { - Text(text = "Panel") - } - } - } - - composeTestRule - .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(Quaternion.Identity) - - val watcherEntity = - assertNotNull( - composeTestRule - .onSubspaceNodeWithTag("TheWatcher") - .fetchSemanticsNode() - .semanticsEntity - ) - - val userLocation = Vector3(x = 1F, y = 2F, z = 3F) - fakePerceptionManager.arDevice.apply { - devicePose = devicePose.translate(translation = userLocation) - } - - testDispatcher.scheduler.advanceUntilIdle() - composeTestRule.waitForIdle() - - val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val expectedRotation = - getBillboardRotationNeeded(watcherWorldPose.translation, userLocation) - - composeTestRule - .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(expectedRotation, tolerance = 0.04f) - } - @Test fun lookAtUser_userTranslationChanges_contentTurnsTowardsUser() = runTest(testDispatcher) { @@ -146,28 +107,35 @@ class LookAtUserTest { } @Test - fun billboard_withRotationModifier_retainsOffsetAfterBillboard() = + fun lookAtUser_withGravityAligned_ignoresPitchRotation_andContentTurnsTowardsUser() = runTest(testDispatcher) { val fakePerceptionManager = createSessionAndGetPerceptionManager() - val fixedRotateOffset = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) composeTestRule.setContent { Subspace { SpatialPanel( SubspaceModifier.testTag("TheWatcher") - .billboard() - .rotate(pitch = 40f, yaw = 30f, roll = 20f) + // Apply an initial pitch rotation to test billboard behavior + .rotate(pitch = 30f) + .lookAtUser() + .gravityAligned() ) { Text(text = "Panel") } } } - val watcherEntity = composeTestRule.getTaggedEntity("TheWatcher") - composeTestRule .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(fixedRotateOffset) + .assertRotationInRootIsEqualTo(Quaternion.Identity) + + val watcherEntity = + assertNotNull( + composeTestRule + .onSubspaceNodeWithTag("TheWatcher") + .fetchSemanticsNode() + .semanticsEntity + ) val userLocation = Vector3(x = 1F, y = 2F, z = 3F) fakePerceptionManager.arDevice.apply { @@ -178,9 +146,8 @@ class LookAtUserTest { composeTestRule.waitForIdle() val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val billboardRotationTowardsUser = + val expectedRotation = getBillboardRotationNeeded(watcherWorldPose.translation, userLocation) - val expectedRotation = billboardRotationTowardsUser * fixedRotateOffset composeTestRule .onSubspaceNodeWithTag("TheWatcher") @@ -188,7 +155,7 @@ class LookAtUserTest { } @Test - fun lookAtUser_withRotationModifier_retainsOffsetAfterLookAt() = + fun lookAtUser_withRotation_retainsOffset() = runTest(testDispatcher) { val fakePerceptionManager = createSessionAndGetPerceptionManager() val fixedRotateOffset = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) @@ -231,29 +198,31 @@ class LookAtUserTest { } @Test - fun billboard_precededByRotation_ignoresRotation() = + fun lookAtUser_withGravityAlignedAndRotation_retainsOffset() = runTest(testDispatcher) { val fakePerceptionManager = createSessionAndGetPerceptionManager() - val localRotation = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) + val fixedRotateOffset = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) composeTestRule.setContent { Subspace { SpatialPanel( - SubspaceModifier.testTag("TheWatcher").rotate(localRotation).billboard() + SubspaceModifier.testTag("TheWatcher") + // Apply an initial pitch rotation to test billboard behavior + .rotate(pitch = 30f) + .lookAtUser() + .gravityAligned() + .rotate(pitch = 40f, yaw = 30f, roll = 20f) ) { Text(text = "Panel") } } } - val yawOnlyRotation = - Quaternion.fromEulerAngles(pitch = 0f, yaw = localRotation.eulerAngles.y, roll = 0f) + val watcherEntity = composeTestRule.getTaggedEntity("TheWatcher") composeTestRule .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(yawOnlyRotation) - - val watcherEntity = composeTestRule.getTaggedEntity("TheWatcher") + .assertRotationInRootIsEqualTo(fixedRotateOffset) val userLocation = Vector3(x = 1F, y = 2F, z = 3F) fakePerceptionManager.arDevice.apply { @@ -264,8 +233,9 @@ class LookAtUserTest { composeTestRule.waitForIdle() val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val expectedRotation = + val billboardRotationTowardsUser = getBillboardRotationNeeded(watcherWorldPose.translation, userLocation) + val expectedRotation = billboardRotationTowardsUser * fixedRotateOffset composeTestRule .onSubspaceNodeWithTag("TheWatcher") @@ -311,105 +281,6 @@ class LookAtUserTest { .assertRotationInRootIsEqualTo(expectedRotation, tolerance = 0.04f) } - @Test - fun lookAtUser_followedByBillboard_behavesAsBillboard() = - runTest(testDispatcher) { - val fakePerceptionManager = createSessionAndGetPerceptionManager() - - composeTestRule.setContent { - Subspace { - SpatialPanel(SubspaceModifier.testTag("TheWatcher").lookAtUser().billboard()) { - Text(text = "Panel") - } - } - } - - val watcherEntity = composeTestRule.getTaggedEntity("TheWatcher") - - val userLocation = Vector3(x = 1F, y = 2F, z = 3F) - fakePerceptionManager.arDevice.apply { - devicePose = devicePose.translate(translation = userLocation) - } - - testDispatcher.scheduler.advanceUntilIdle() - composeTestRule.waitForIdle() - - val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val expectedRotation = - getBillboardRotationNeeded(watcherWorldPose.translation, userLocation) - - composeTestRule - .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(expectedRotation, tolerance = 0.04f) - } - - @Test - fun billboard_followedByLookAtUser_behavesAsLookAtUser() = - runTest(testDispatcher) { - val fakePerceptionManager = createSessionAndGetPerceptionManager() - - composeTestRule.setContent { - Subspace { - SpatialPanel(SubspaceModifier.testTag("TheWatcher").billboard().lookAtUser()) { - Text(text = "Panel") - } - } - } - - val watcherEntity = composeTestRule.getTaggedEntity("TheWatcher") - - val userLocation = Vector3(x = 1F, y = 2F, z = 3F) - fakePerceptionManager.arDevice.apply { - devicePose = devicePose.translate(translation = userLocation) - } - - testDispatcher.scheduler.advanceUntilIdle() - composeTestRule.waitForIdle() - - val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val targetVector = (userLocation - watcherWorldPose.translation).toNormalized() - val expectedRotation = Quaternion.fromLookTowards(targetVector, Vector3(0f, 1f, 0f)) - - composeTestRule - .onSubspaceNodeWithTag("TheWatcher") - .assertRotationInRootIsEqualTo(expectedRotation, tolerance = 0.04f) - } - - @Test - fun billboard_withRotatedParent_ignoresParentRotation() = - runTest(testDispatcher) { - val fakePerceptionManager = createSessionAndGetPerceptionManager() - val parentRotation = Quaternion.fromEulerAngles(pitch = 40f, yaw = 30f, roll = 20f) - - composeTestRule.setContent { - Subspace { - SpatialBox(SubspaceModifier.rotate(parentRotation)) { - SpatialPanel(SubspaceModifier.testTag("child").billboard()) { - Text(text = "Panel") - } - } - } - } - - val watcherEntity = composeTestRule.getTaggedEntity("child") - - val userLocation = Vector3(x = 1F, y = 2F, z = 3F) - fakePerceptionManager.arDevice.apply { - devicePose = devicePose.translate(translation = userLocation) - } - - testDispatcher.scheduler.advanceUntilIdle() - composeTestRule.waitForIdle() - - val watcherWorldPose = watcherEntity.getPose(Space.REAL_WORLD) - val expectedWorldRotation = - getBillboardRotationNeeded(watcherWorldPose.translation, userLocation) - - composeTestRule - .onSubspaceNodeWithTag("child") - .assertRotationInRootIsEqualTo(expectedWorldRotation, tolerance = 0.04f) - } - @Test fun lookAtUser_withRotatedParent_ignoresParentRotation() = runTest(testDispatcher) { @@ -451,10 +322,13 @@ class LookAtUserTest { } private fun createSessionAndGetPerceptionManager(): FakePerceptionManager { - val session = composeTestRule.configureFakeSession() + val sessionCreateResult = Session.create(composeTestRule.activity, testDispatcher) + assertThat(sessionCreateResult).isInstanceOf(SessionCreateSuccess::class.java) + val session = (sessionCreateResult as SessionCreateSuccess).session session.configure( config = session.config.copy(deviceTracking = Config.DeviceTrackingMode.LAST_KNOWN) ) + composeTestRule.session = session val fakeRuntime = session.runtimes.filterIsInstance().first() return fakeRuntime.perceptionManager } diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/lookatuser/LookAtUserActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/lookatuser/LookAtUserActivity.kt index 6d4762620f65c..fa3f21fad1278 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/lookatuser/LookAtUserActivity.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/lookatuser/LookAtUserActivity.kt @@ -26,9 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -46,6 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.xr.compose.platform.LocalSession import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialBox import androidx.xr.compose.subspace.SpatialColumn import androidx.xr.compose.subspace.SpatialExternalSurface import androidx.xr.compose.subspace.SpatialPanel @@ -54,12 +53,13 @@ import androidx.xr.compose.subspace.StereoMode import androidx.xr.compose.subspace.SubspaceComposable import androidx.xr.compose.subspace.layout.SpatialArrangement import androidx.xr.compose.subspace.layout.SubspaceModifier -import androidx.xr.compose.subspace.layout.billboard import androidx.xr.compose.subspace.layout.fillMaxSize +import androidx.xr.compose.subspace.layout.gravityAligned import androidx.xr.compose.subspace.layout.height import androidx.xr.compose.subspace.layout.lookAtUser import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.padding +import androidx.xr.compose.subspace.layout.rotate import androidx.xr.compose.subspace.layout.width import androidx.xr.compose.testapp.ui.components.TopBarWithBackArrow import androidx.xr.compose.testapp.ui.theme.IntegrationTestsAppTheme @@ -67,13 +67,31 @@ import androidx.xr.compose.testapp.ui.theme.Purple40 import androidx.xr.compose.testapp.ui.theme.PurpleGrey40 import androidx.xr.compose.testapp.ui.theme.PurpleGrey80 import androidx.xr.runtime.Config +import androidx.xr.runtime.math.Quaternion +import androidx.xr.runtime.math.Vector3 /** - * Test Activity for the [billboard] modifier. This activity demonstrates the effect of applying - * [billboard] to various spatial containers, including [SpatialPanel], [SpatialRow], and - * [SpatialColumn]. The modifier ensures the spatial composable's content always rotates to face the - * user's current head position, regardless of the user's movement or the composable's initial - * placement. + * Integration test activity for the [lookAtUser] modifier. + * + * This activity provides a visual test bed to validate how spatial entities track the user's head + * pose across different container types and modifier combinations. + * + * Test Scenarios + * 1. Standard lookAtUser: Validates full 3D orientation tracking (pitch, yaw, and roll) applied to + * a [SpatialPanel], [SpatialRow], [SpatialColumn], and [SpatialExternalSurface]. + * 2. Nested Hierarchies: Validates that the tracking logic correctly handles coordinate + * transformations when the child tracks the user inside a rotated [SpatialBox]. + * 3. Custom Up Vector: Validates tracking behavior when a specific 'up' orientation is provided, + * useful for tilted or non-standard tracking requirements. + * 4. Billboard: Demonstrates and validates the "Billboard" effect, achieved by chaining + * [lookAtUser] with [gravityAligned]. This should result in horizontal-only tracking while the + * panel remains vertically upright. + * + * Usage + * - Use the global switch in the top control panel to toggle the tracking behavior for all test + * cases simultaneously. + * - Move the headset or camera around the spatial environment to verify that all panels actively + * rotate to maintain a front-facing orientation toward the user. */ class LookAtUserActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -86,42 +104,24 @@ class LookAtUserActivity : ComponentActivity() { @Composable private fun MainContent() { val session = checkNotNull(LocalSession.current) { "session must be initialized" } - // Ensure head tracking is enabled session.configure( config = session.config.copy(deviceTracking = Config.DeviceTrackingMode.LAST_KNOWN) ) - // Global state for billboard toggling - var isBillboardOn by remember { mutableStateOf(true) } - // Global state for lookAtUser toggling var isLookAtUserOn by remember { mutableStateOf(true) } IntegrationTestsAppTheme { - Subspace(modifier = SubspaceModifier.width(1200.dp).height(1400.dp)) { - SpatialRow() { - SpatialColumn( - verticalArrangement = SpatialArrangement.spacedBy(20.dp), - // Offset the entire column slightly away from the user for better viewing - modifier = SubspaceModifier.offset(y = 100.dp, z = (-100).dp), - ) { - ControlPanel( - feature = "Billboard", - isFeatureOn = isBillboardOn, - onToggle = { isBillboardOn = it }, - ) - TestGrid(feature = "Billboard", isFeatureOn = isBillboardOn) - } - SpatialColumn( - verticalArrangement = SpatialArrangement.spacedBy(20.dp), - // Offset the entire column slightly away from the user for better viewing - modifier = SubspaceModifier.offset(y = 100.dp, z = (-100).dp), - ) { + Subspace(modifier = SubspaceModifier.width(1600.dp).height(1400.dp)) { + SpatialRow( + modifier = SubspaceModifier.offset(y = 100.dp), + horizontalArrangement = SpatialArrangement.spacedBy(40.dp), + ) { + SpatialColumn(verticalArrangement = SpatialArrangement.spacedBy(20.dp)) { ControlPanel( - feature = "LookAtUser", - isFeatureOn = isLookAtUserOn, + isLookAtUserOn = isLookAtUserOn, onToggle = { isLookAtUserOn = it }, ) - TestGrid(feature = "LookAtUser", isFeatureOn = isLookAtUserOn) + TestGrid(isFeatureOn = isLookAtUserOn) } } } @@ -131,8 +131,7 @@ class LookAtUserActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @SubspaceComposable @Composable - private fun ControlPanel(feature: String, isFeatureOn: Boolean, onToggle: (Boolean) -> Unit) { - // SpatialPanel container for the controls + private fun ControlPanel(isLookAtUserOn: Boolean, onToggle: (Boolean) -> Unit) { SpatialPanel(modifier = SubspaceModifier.width(550.dp).height(200.dp).padding(25.dp)) { Column( modifier = Modifier.fillMaxSize().background(PurpleGrey80), @@ -143,7 +142,7 @@ class LookAtUserActivity : ComponentActivity() { Row(modifier = Modifier.fillMaxWidth()) { TopBarWithBackArrow( scrollBehavior = null, - title = "$feature Modifier Test", + title = "Look At User Modifier Test", onClick = { finish() }, ) } @@ -155,41 +154,44 @@ class LookAtUserActivity : ComponentActivity() { horizontalArrangement = Arrangement.Center, ) { Text( - "Enable $feature Modifier:", + "Enable Look At User Modifier:", color = PurpleGrey40, fontSize = 24.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(end = 20.dp), ) - Switch(checked = isFeatureOn, onCheckedChange = onToggle) + Switch(checked = isLookAtUserOn, onCheckedChange = onToggle) } } } } /** - * A reusable container that encapsulates a spatial composable and applies the billboard + * A reusable container that encapsulates a spatial composable and applies the lookAtUser * modifier based on the global state. */ @SubspaceComposable @Composable private fun TestPanelContainer( title: String, - feature: String, isFeatureOn: Boolean, - extraContent: (@Composable () -> Unit)? = null, + upVector: Vector3? = null, + width: Int = 400, + height: Int = 200, container: @Composable @SubspaceComposable (SubspaceModifier, @Composable () -> Unit) -> Unit, ) { - var finalModifier = SubspaceModifier.width(400.dp).height(200.dp) + var finalModifier = SubspaceModifier.width(width.dp).height(height.dp) if (isFeatureOn) { - if (feature == "Billboard") finalModifier = finalModifier.billboard() - if (feature == "LookAtUser") finalModifier = finalModifier.lookAtUser() + finalModifier = + when { + upVector != null -> finalModifier.lookAtUser(up = upVector) + else -> finalModifier.lookAtUser() + } } - // The inner content function (passed to the container) val innerContent: @Composable () -> Unit = { @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 Box( @@ -199,18 +201,13 @@ class LookAtUserActivity : ComponentActivity() { .padding(16.dp), contentAlignment = Alignment.Center, ) { - // Use Column to stack the title and the optional content - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = title, - color = Color.White, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Center, - ) - // Conditionally display the optional content - extraContent?.invoke() - } + Text( + text = title, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + ) } } @@ -219,86 +216,87 @@ class LookAtUserActivity : ComponentActivity() { @SubspaceComposable @Composable - private fun TestGrid(feature: String, isFeatureOn: Boolean) { - SpatialColumn(verticalArrangement = SpatialArrangement.spacedBy(20.dp)) { - // SpatialPanel - TestPanelContainer( - title = "SpatialPanel", - feature = feature, - isFeatureOn = isFeatureOn, - ) { modifier, content -> - SpatialPanel(modifier = modifier, content = content) - } + private fun TestGrid(isFeatureOn: Boolean) { + SpatialRow(horizontalArrangement = SpatialArrangement.spacedBy(20.dp)) { + // Column 1: Standard Composables + SpatialColumn(verticalArrangement = SpatialArrangement.spacedBy(20.dp)) { + TestPanelContainer(title = "SpatialPanel", isFeatureOn = isFeatureOn) { + modifier, + content -> + SpatialPanel(modifier = modifier, content = content) + } - // SpatialRow - TestPanelContainer( - title = "SpatialRow", - feature = feature, - isFeatureOn = isFeatureOn, - extraContent = { - @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 - Row( - modifier = Modifier.padding(top = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - StaticBox("1") - StaticBox("2") + TestPanelContainer(title = "SpatialExternalSurface", isFeatureOn = isFeatureOn) { + modifier, + content -> + SpatialExternalSurface(modifier = modifier, stereoMode = StereoMode.Mono) { + SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) } - }, - ) { modifier, content -> - SpatialRow(modifier = modifier) { - SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) } - } - // SpatialColumn - TestPanelContainer( - title = "SpatialColumn", - feature = feature, - isFeatureOn = isFeatureOn, - extraContent = { - @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 - Column( - modifier = Modifier.padding(top = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - StaticBox("1") - StaticBox("2") + TestPanelContainer(title = "SpatialRow", isFeatureOn = isFeatureOn) { + modifier, + content -> + SpatialRow(modifier = modifier) { + SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) } - }, - ) { modifier, content -> - SpatialColumn(modifier = modifier) { - SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) } - } - // SpatialExternalSurface - TestPanelContainer( - title = "SpatialExternalSurface", - feature = feature, - isFeatureOn = isFeatureOn, - ) { modifier, content -> - SpatialExternalSurface(modifier = modifier, stereoMode = StereoMode.Mono) { - SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) + TestPanelContainer(title = "SpatialColumn", isFeatureOn = isFeatureOn) { + modifier, + content -> + SpatialColumn(modifier = modifier) { + SpatialPanel(modifier = SubspaceModifier.fillMaxSize(), content = content) + } } } - } - } - @Composable - private fun StaticBox(title: String) { - Box( - modifier = Modifier.width(100.dp).height(50.dp).background(PurpleGrey80), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = title, - color = PurpleGrey40, - fontSize = 24.sp, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Center, - ) + // Column 2: Advanced Configurations + SpatialColumn(verticalArrangement = SpatialArrangement.spacedBy(60.dp)) { + // Nested rotation test: Demonstrates a tracking child within a fixed rotated parent + val parentRotation = Quaternion.fromEulerAngles(pitch = 0f, yaw = 0f, roll = 10f) + SpatialBox( + modifier = SubspaceModifier.width(400.dp).height(200.dp).rotate(parentRotation) + ) { + SpatialPanel(modifier = SubspaceModifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize().background(PurpleGrey40)) { + Text( + "PARENT (FIXED ROTATION)", + color = Color.White, + fontSize = 12.sp, + modifier = Modifier.padding(8.dp).align(Alignment.TopStart), + ) + } + } + + TestPanelContainer( + title = "CHILD (TRACKING)", + isFeatureOn = isFeatureOn, + width = 300, + height = 100, + ) { modifier, content -> + // Offset by 5dp on Z axis to prevent clipping with parent panel + SpatialPanel(modifier = modifier.offset(z = 5.dp), content = content) + } + } + + // Custom up vector: Tracking with a specific 'up' orientation + TestPanelContainer( + title = "Custom Up Vector (1, 0, 0)", + isFeatureOn = isFeatureOn, + upVector = Vector3(1f, 0f, 0f), + width = 300, + ) { modifier, content -> + SpatialPanel(modifier = modifier, content = content) + } + + // Billboard: Horizontal tracking only (upright) + TestPanelContainer( + title = "Billboard (Look + Gravity)", + isFeatureOn = isFeatureOn, + ) { modifier, content -> + SpatialPanel(modifier = modifier.gravityAligned(), content = content) + } } } } diff --git a/xr/compose/integration-tests/testapp/src/main/res/values/strings.xml b/xr/compose/integration-tests/testapp/src/main/res/values/strings.xml index 84483c9a6394d..0ee0ea3dcc438 100644 --- a/xr/compose/integration-tests/testapp/src/main/res/values/strings.xml +++ b/xr/compose/integration-tests/testapp/src/main/res/values/strings.xml @@ -41,7 +41,7 @@ Runtime Session Test Case Pose Test Case Gravity Aligned Test Case - LookAtUser and Billboard Test Case + LookAtUser Test Case Hello Android XR. Switch to Home Space Mode Switch to Full Space Mode From 2fe11ecd0a2992f822ed3b88274bd6d107390388 Mon Sep 17 00:00:00 2001 From: Aurimas Liutikas Date: Mon, 10 Nov 2025 13:39:30 -0800 Subject: [PATCH 09/19] Enable configuration caching for all Gradle plugins that use ProjectSetupRule Test: None Change-Id: I0fb381c7c5f5ef78d53a2a30682d460f39cf5469 --- .../CreateLibraryBuildInfoFileTaskTest.kt | 20 ++++++++++++++----- .../safeargs/gradle/BasePluginTest.kt | 2 -- .../testutils/gradle/ProjectSetupRule.kt | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt index 9d4cd2a39d966..d25ac897dae32 100644 --- a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt +++ b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt @@ -71,7 +71,9 @@ class CreateLibraryBuildInfoFileTaskTest { @Test fun buildInfoTaskCreatesSimpleFile() { setupBuildInfoProject() - gradleRunner.withArguments("createLibraryBuildInfoFiles").build() + gradleRunner + .withArguments("createLibraryBuildInfoFiles", "--no-configuration-cache") + .build() val buildInfoFile = distDir.root.resolve("build-info/androidx.build_info_test_test_build_info.txt") @@ -100,7 +102,9 @@ class CreateLibraryBuildInfoFileTaskTest { @Test fun buildInfoTaskCreatesSimpleFileWithAllDependencies() { setupBuildInfoProjectWithAllDependencies() - gradleRunner.withArguments("createLibraryBuildInfoFiles").build() + gradleRunner + .withArguments("createLibraryBuildInfoFiles", "--no-configuration-cache") + .build() val buildInfoFile = distDir.root.resolve("build-info/androidx.build_info_test_test_build_info.txt") @@ -146,7 +150,9 @@ class CreateLibraryBuildInfoFileTaskTest { @Test fun buildInfoSelectsCorrectKmpVariant() { setupBuildInfoProjectWithKmpDependency() - gradleRunner.withArguments("createLibraryBuildInfoFiles").build() + gradleRunner + .withArguments("createLibraryBuildInfoFiles", "--no-configuration-cache") + .build() val buildInfoFile = distDir.root.resolve("build-info/androidx.build_info_test_test_build_info.txt") @@ -169,7 +175,9 @@ class CreateLibraryBuildInfoFileTaskTest { @Test fun buildInfoTaskAddsTestModuleNames() { setupBuildInfoProject() - gradleRunner.withArguments("createLibraryBuildInfoFiles").build() + gradleRunner + .withArguments("createLibraryBuildInfoFiles", "--no-configuration-cache") + .build() val buildInfoFile = distDir.root.resolve("build-info/androidx.build_info_test_test_build_info.txt") @@ -183,7 +191,9 @@ class CreateLibraryBuildInfoFileTaskTest { @Test fun buildInfoTaskWithSuffixSkipsTestModuleNames() { setupBuildInfoProjectForArtifactWithSuffix() - gradleRunner.withArguments("createLibraryBuildInfoFiles").build() + gradleRunner + .withArguments("createLibraryBuildInfoFiles", "--no-configuration-cache") + .build() val buildInfoFile = distDir.root.resolve("build-info/androidx.build_info_test_test-jvm_build_info.txt") diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt index 80b05793f4e41..892275c8cb26a 100644 --- a/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt +++ b/navigation/navigation-safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/BasePluginTest.kt @@ -78,8 +78,6 @@ abstract class BasePluginTest { .withArguments( // b/175897186 set explicit metaspace size in hopes of fewer crashes "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m", - // Enable configuration cache for these tests - "-Dorg.gradle.configuration-cache=true", *args, ) return runner diff --git a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt index 8416bfe9d7b50..5faf2dc055377 100644 --- a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt +++ b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt @@ -209,6 +209,7 @@ class ProjectSetupRule(parentFolder: File? = null) : ExternalResource() { gradlePropertiesFile.writer().use { val props = Properties() props.setProperty("android.useAndroidX", "true") + props.setProperty("org.gradle.configuration-cache", "true") props.store(it, null) } } From f12cec30362f078b3fdfe405842c7f657e745775 Mon Sep 17 00:00:00 2001 From: Hongwei Wang Date: Fri, 9 Jan 2026 15:43:17 -0800 Subject: [PATCH 10/19] Add PictureInPictureImpl for typically PiP usages - BasicPictureInPicture for navigation and video call use cases, which does not require sourceRectHint and not able to resize seamlessly - VideoPlaybackPictureInPicture for video playback use case, app can provide the player view and the library keeps track the bounds of it - README doc is updated accordingly Bug: 475328144 Relnote: "Add BasicPictureInPicture and VideoPlaybackPictureInPicture classes for typical PiP usages" Test: updated and new test suites pass Change-Id: I7f9895dd5a0c6a5853dda519faf0ac62a4f9d5c1 --- core/core-pip/api/current.txt | 24 ++- core/core-pip/api/restricted_current.txt | 24 ++- .../core/pip/PictureInPictureImplTest.kt | 135 +++++++++++++++++ .../androidx-core-core-pip-documentation.md | 122 ++++++++++++++++ .../core/pip/PictureInPictureDelegate.kt | 7 +- .../androidx/core/pip/PictureInPictureImpl.kt | 137 ++++++++++++++++++ 6 files changed, 439 insertions(+), 10 deletions(-) create mode 100644 core/core-pip/src/androidTest/java/androidx/core/pip/PictureInPictureImplTest.kt create mode 100644 core/core-pip/src/main/java/androidx/core/pip/PictureInPictureImpl.kt diff --git a/core/core-pip/api/current.txt b/core/core-pip/api/current.txt index 437b4ef216464..ccd638db07221 100644 --- a/core/core-pip/api/current.txt +++ b/core/core-pip/api/current.txt @@ -1,11 +1,20 @@ // Signature format: 4.0 package androidx.core.pip { - public final class PictureInPictureDelegate { + public class BasicPictureInPicture extends androidx.core.pip.PictureInPictureDelegate { + ctor public BasicPictureInPicture(androidx.core.app.PictureInPictureProvider pictureInPictureProvider); + method @InaccessibleFromKotlin protected final androidx.core.app.PictureInPictureParamsCompat.Builder getPictureInPictureParamsBuilder(); + method public final androidx.core.pip.BasicPictureInPicture setActions(java.util.List actions); + method public final androidx.core.pip.BasicPictureInPicture setAspectRatio(android.util.Rational aspectRatio); + method public final androidx.core.pip.BasicPictureInPicture setEnabled(boolean enabled); + property protected final androidx.core.app.PictureInPictureParamsCompat.Builder pictureInPictureParamsBuilder; + } + + public class PictureInPictureDelegate { ctor public PictureInPictureDelegate(androidx.core.app.PictureInPictureProvider pictureInPictureProvider); - method public void addOnPictureInPictureEventListener(java.util.concurrent.Executor executor, androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); - method public void removeOnPictureInPictureEventListener(androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); - method public void setPictureInPictureParams(androidx.core.app.PictureInPictureParamsCompat pictureInPictureParamsCompat); + method public final void addOnPictureInPictureEventListener(java.util.concurrent.Executor executor, androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); + method public final void removeOnPictureInPictureEventListener(androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); + method public final void setPictureInPictureParams(androidx.core.app.PictureInPictureParamsCompat pictureInPictureParamsCompat); } public static final class PictureInPictureDelegate.Event { @@ -31,5 +40,12 @@ package androidx.core.pip { method public void onPictureInPictureEvent(androidx.core.pip.PictureInPictureDelegate.Event event, android.content.res.Configuration? config); } + public final class VideoPlaybackPictureInPicture extends androidx.core.pip.BasicPictureInPicture implements java.lang.AutoCloseable { + ctor public VideoPlaybackPictureInPicture(androidx.core.app.PictureInPictureProvider provider); + method public void close(); + method public void onViewBoundsChanged(android.view.View view, android.graphics.Rect newBounds); + method public androidx.core.pip.VideoPlaybackPictureInPicture setPlayerView(android.view.View? view); + } + } diff --git a/core/core-pip/api/restricted_current.txt b/core/core-pip/api/restricted_current.txt index 437b4ef216464..ccd638db07221 100644 --- a/core/core-pip/api/restricted_current.txt +++ b/core/core-pip/api/restricted_current.txt @@ -1,11 +1,20 @@ // Signature format: 4.0 package androidx.core.pip { - public final class PictureInPictureDelegate { + public class BasicPictureInPicture extends androidx.core.pip.PictureInPictureDelegate { + ctor public BasicPictureInPicture(androidx.core.app.PictureInPictureProvider pictureInPictureProvider); + method @InaccessibleFromKotlin protected final androidx.core.app.PictureInPictureParamsCompat.Builder getPictureInPictureParamsBuilder(); + method public final androidx.core.pip.BasicPictureInPicture setActions(java.util.List actions); + method public final androidx.core.pip.BasicPictureInPicture setAspectRatio(android.util.Rational aspectRatio); + method public final androidx.core.pip.BasicPictureInPicture setEnabled(boolean enabled); + property protected final androidx.core.app.PictureInPictureParamsCompat.Builder pictureInPictureParamsBuilder; + } + + public class PictureInPictureDelegate { ctor public PictureInPictureDelegate(androidx.core.app.PictureInPictureProvider pictureInPictureProvider); - method public void addOnPictureInPictureEventListener(java.util.concurrent.Executor executor, androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); - method public void removeOnPictureInPictureEventListener(androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); - method public void setPictureInPictureParams(androidx.core.app.PictureInPictureParamsCompat pictureInPictureParamsCompat); + method public final void addOnPictureInPictureEventListener(java.util.concurrent.Executor executor, androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); + method public final void removeOnPictureInPictureEventListener(androidx.core.pip.PictureInPictureDelegate.OnPictureInPictureEventListener listener); + method public final void setPictureInPictureParams(androidx.core.app.PictureInPictureParamsCompat pictureInPictureParamsCompat); } public static final class PictureInPictureDelegate.Event { @@ -31,5 +40,12 @@ package androidx.core.pip { method public void onPictureInPictureEvent(androidx.core.pip.PictureInPictureDelegate.Event event, android.content.res.Configuration? config); } + public final class VideoPlaybackPictureInPicture extends androidx.core.pip.BasicPictureInPicture implements java.lang.AutoCloseable { + ctor public VideoPlaybackPictureInPicture(androidx.core.app.PictureInPictureProvider provider); + method public void close(); + method public void onViewBoundsChanged(android.view.View view, android.graphics.Rect newBounds); + method public androidx.core.pip.VideoPlaybackPictureInPicture setPlayerView(android.view.View? view); + } + } diff --git a/core/core-pip/src/androidTest/java/androidx/core/pip/PictureInPictureImplTest.kt b/core/core-pip/src/androidTest/java/androidx/core/pip/PictureInPictureImplTest.kt new file mode 100644 index 0000000000000..47fc7d63b7475 --- /dev/null +++ b/core/core-pip/src/androidTest/java/androidx/core/pip/PictureInPictureImplTest.kt @@ -0,0 +1,135 @@ +/* + * 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.core.pip + +import android.app.PendingIntent +import android.app.RemoteAction +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.Build +import android.util.Rational +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.app.PictureInPictureParamsCompat +import androidx.core.app.PictureInPictureProvider +import androidx.core.app.PictureInPictureUiStateCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) +class PictureInPictureImplTest { + + private lateinit var provider: FakePictureInPictureProvider + + @Before + fun setUp() { + provider = FakePictureInPictureProvider() + } + + @Test + fun basicImpl_setAspectRatio() { + val aspectRatio = Rational(16, 9) + val impl = BasicPictureInPicture(provider) + impl.setAspectRatio(aspectRatio) + assertThat(provider.receivedParams!!.aspectRatio).isEqualTo(aspectRatio) + } + + @Test + fun basicImpl_setEnabled() { + val impl = BasicPictureInPicture(provider) + impl.setEnabled(false) + assertThat(provider.receivedParams!!.isEnabled).isFalse() + impl.setEnabled(true) + assertThat(provider.receivedParams!!.isEnabled).isTrue() + } + + @Test + fun basicImpl_defaults() { + BasicPictureInPicture(provider) + val params = provider.receivedParams + assertThat(params!!.isSeamlessResizeEnabled).isFalse() + } + + @Test + fun basicImpl_setActions() { + val actions = createFakeActions() + val impl = BasicPictureInPicture(provider) + impl.setActions(actions) + assertThat(provider.receivedParams!!.actions).isEqualTo(actions) + } + + @Test + fun videoPlaybackImpl_defaults() { + VideoPlaybackPictureInPicture(provider) + val params = provider.receivedParams + assertThat(params!!.isSeamlessResizeEnabled).isTrue() + } + + private fun createFakeActions(): List { + val context = ApplicationProvider.getApplicationContext() + val intent = Intent("TEST_ACTION") + val pendingIntent = + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val action = + RemoteAction( + Icon.createWithContentUri("content://uri"), + "title", + "description", + pendingIntent, + ) + return listOf(action) + } +} + +private class FakePictureInPictureProvider : PictureInPictureProvider { + var receivedParams: PictureInPictureParamsCompat? = null + private set + + override fun setPictureInPictureParams(params: PictureInPictureParamsCompat) { + this.receivedParams = params + } + + override fun enterPictureInPictureMode(params: PictureInPictureParamsCompat) {} + + override fun addOnPictureInPictureUiStateChangedListener( + listener: androidx.core.util.Consumer + ) {} + + override fun removeOnPictureInPictureUiStateChangedListener( + listener: androidx.core.util.Consumer + ) {} + + override fun addOnUserLeaveHintListener(listener: Runnable) {} + + override fun removeOnUserLeaveHintListener(listener: Runnable) {} + + override fun addOnPictureInPictureModeChangedListener( + listener: androidx.core.util.Consumer + ) {} + + override fun removeOnPictureInPictureModeChangedListener( + listener: androidx.core.util.Consumer + ) {} +} diff --git a/core/core-pip/src/main/java/androidx/core/androidx-core-core-pip-documentation.md b/core/core-pip/src/main/java/androidx/core/androidx-core-core-pip-documentation.md index 78ed0cfc29d93..2d49640256899 100644 --- a/core/core-pip/src/main/java/androidx/core/androidx-core-core-pip-documentation.md +++ b/core/core-pip/src/main/java/androidx/core/androidx-core-core-pip-documentation.md @@ -11,3 +11,125 @@ The PiP Jetpack library addresses several challenges in Android's Picture-in-Pic - Boilerplate Code: It reduces boilerplate by offering predefined RemoteAction sets for common use cases like playback and video calls. Furthermore, all new PiP features will be delivered through the Jetpack library, ensuring that library adopters can access these features with minimal to no effort. + +# Usage of the library + +This library depends on the latest `androidx.core` library, and it's recommended to use the ComponentActivity from the latest `androidx.activity` as well + +- androidx.core 1.18.0-alpha01 +- androidx.activity 1.13.0-alpha01 (optional, highly recommended) + +The code snippets below would assume the application references both. + +## Navigation and Video Call applications + +For these usages + +- Application does not need to specify the sourceRectHint +- The seamlessResize flag is set to false +- Typically, does not listen on `ENTER_ANIMATION_START` and `ENTER_ANIMATION_END` events + +``` +// Pseudo code in Kotlin +class NavigationActivity : + ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener { + + private lateinit var pictureInPictureImpl: BasicPictureInPicture + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pictureInPictureImpl = BasicPictureInPicture(this) + pictureInPictureImpl.addOnPictureInPictureEventListener(this) + } + + override fun onPictureInPictureEvent( + event: PictureInPictureDelegate.Event, + newConfig: Configuration? + ) { + when (event) { + PictureInPictureDelegate.Event.ENTERED -> { + /* Change to PiP layout*/ + } + PictureInPictureDelegate.Event.STASHED -> { + /* Optional: PiP is now in stashed state */ + } + PictureInPictureDelegate.Event.UNSTASHED -> { + /* Optional: PiP is now in unstashed state */ + } + PictureInPictureDelegate.Event.EXITED -> { + /* Change to full-screen layout*/ + } + } + } + + private fun onNavigationStateChanged(isInActiveNavigation: Boolean) { + pictureInPictureImpl.apply { + setEnabled(isInActiveNavigation) + setAspectRatio(desiredAspectRatio) + setActions(actions) + } + } +} +``` + +## Video Playback applications + +For the video playback usage +- Application can specify the player view, and the library can continuously track the view bounds as sourceRectHint +- The seamlessResize flag is set to true +- It's highly recommended to listen on `ENTER_ANIMATION_START` event to hide the overlays upon the video to achieve a cleaner entering PiP animation + +``` +// Pseudo code in Kotlin +class VideoPlaybackActivity : + ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener { + + private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pictureInPictureImpl = VideoPlaybackPictureInPicture(this) + pictureInPictureImpl.addOnPictureInPictureEventListener(this) + } + + override fun onDestroy() { + super.onDestroy() + pictureInPictureImpl.close() + } + + override fun onPictureInPictureEvent( + event: PictureInPictureDelegate.Event, + newConfig: Configuration? + ) { + when (event) { + PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { + /* Optional: hide overlays that are hidden in PiP mode. */ + } + PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { + /* Optional: the animation to enter PiP ends */ + } + PictureInPictureDelegate.Event.ENTERED -> { + /* Change to PiP layout*/ + } + PictureInPictureDelegate.Event.STASHED -> { + /* Optional: PiP is now in stashed state */ + } + PictureInPictureDelegate.Event.UNSTASHED -> { + /* Optional: PiP is now in unstashed state */ + } + PictureInPictureDelegate.Event.EXITED -> { + /* Change to full-screen layout*/ + } + } + } + + private fun onPlaybackStateChanged(isPlaying: Boolean) { + pictureInPictureImpl.apply { + setEnabled(isPlaying) + setPlayerView(if (isPlaying) playerView else null) + setAspectRatio(videoAspectRatio) + setActions(actions) + } + } +} +``` diff --git a/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureDelegate.kt b/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureDelegate.kt index 5db2eb4368294..e9de3d4250cf1 100644 --- a/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureDelegate.kt +++ b/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureDelegate.kt @@ -28,13 +28,16 @@ import java.lang.ref.WeakReference import java.util.concurrent.Executor /** - * A delegate class to help setup PiP (Picture-in-Picture) functionalities on behalf of the given + * A delegate class to help set up PiP (Picture-in-Picture) functionalities on behalf of the given * [PictureInPictureProvider] instance. * + * It's highly recommended to choose one of the implementations: [BasicPictureInPicture], + * [VideoPlaybackPictureInPicture] instead of using this class directly. + * * @param pictureInPictureProvider [PictureInPictureProvider] instance that this delegate will call * into for actual Picture-in-Picture functionalities. */ -public class PictureInPictureDelegate(pictureInPictureProvider: PictureInPictureProvider) { +public open class PictureInPictureDelegate(pictureInPictureProvider: PictureInPictureProvider) { private var pictureInPictureProviderRef: WeakReference = WeakReference(pictureInPictureProvider) diff --git a/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureImpl.kt b/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureImpl.kt new file mode 100644 index 0000000000000..b14258b2e95dd --- /dev/null +++ b/core/core-pip/src/main/java/androidx/core/pip/PictureInPictureImpl.kt @@ -0,0 +1,137 @@ +/* + * 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.core.pip + +import android.app.RemoteAction +import android.graphics.Rect +import android.util.Rational +import android.view.View +import androidx.core.app.PictureInPictureParamsCompat +import androidx.core.app.PictureInPictureProvider + +/** + * Basic Picture-in-Picture implementation. + * + * Configures PiP with a specific aspect ratio, custom actions, and controls enter behavior. + * Seamless resize is disabled and no sourceRectHint is used. + */ +public open class BasicPictureInPicture(pictureInPictureProvider: PictureInPictureProvider) : + PictureInPictureDelegate(pictureInPictureProvider) { + protected val pictureInPictureParamsBuilder: PictureInPictureParamsCompat.Builder = + PictureInPictureParamsCompat.Builder() + + init { + pictureInPictureParamsBuilder.setSeamlessResizeEnabled(false) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + + /** + * Sets the desired aspect ratio for the Picture-in-Picture window. + * + * @param aspectRatio The desired width/height ratio. + * @return This implementation instance for chaining. + */ + public fun setAspectRatio(aspectRatio: Rational): BasicPictureInPicture { + pictureInPictureParamsBuilder.setAspectRatio(aspectRatio) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + return this + } + + /** + * Sets whether the activity should automatically enter Picture-in-Picture mode when eligible + * (e.g., when swiping to home). This indicates the "willingness to enter PiP". + * + * @param enabled True if the Activity is PiP-able, false otherwise. + * @return This implementation instance for chaining. + */ + public fun setEnabled(enabled: Boolean): BasicPictureInPicture { + pictureInPictureParamsBuilder.setEnabled(enabled) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + return this + } + + /** + * Sets the custom actions to be available in the Picture-in-Picture menu. + * + * @param actions A list of RemoteActions. + * @return This implementation instance for chaining. + */ + public fun setActions(actions: List): BasicPictureInPicture { + pictureInPictureParamsBuilder.setActions(actions) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + return this + } +} + +/** + * Picture-in-Picture implementation optimized for Video Playback applications. + * + * Enables seamless resize and allows tracking a View to automatically update the source rectangle + * hint for smooth animations using the package's ViewBoundsTracker. + */ +public class VideoPlaybackPictureInPicture(provider: PictureInPictureProvider) : + BasicPictureInPicture(provider), ViewBoundsTracker.OnViewBoundsChangedListener, AutoCloseable { + + private var viewBoundsTracker: ViewBoundsTracker? = null + + init { + pictureInPictureParamsBuilder.setSeamlessResizeEnabled(true) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + + /** + * Sets the View to be tracked for updating the source rectangle hint. The bounds of this View + * (e.g., the player view) will be used to ensure smooth entry/exit animations into/out of + * Picture-in-Picture. + * + * @param view The View to track, or null to stop tracking and clear the hint. + * @return This implementation instance for chaining. + */ + public fun setPlayerView(view: View?): VideoPlaybackPictureInPicture { + // Close any previous tracker + close() + + if (view != null) { + viewBoundsTracker = + ViewBoundsTracker(view).apply { addListener(this@VideoPlaybackPictureInPicture) } + val initialBounds = Rect() + if (view.getGlobalVisibleRect(initialBounds)) { + pictureInPictureParamsBuilder.setSourceRectHint(initialBounds) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + } else { + // Clear hint if view is removed + pictureInPictureParamsBuilder.setSourceRectHint(null) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + return this + } + + override fun onViewBoundsChanged(view: View, newBounds: Rect) { + pictureInPictureParamsBuilder.setSourceRectHint(newBounds) + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + } + + /** + * Releases resources used by this implementation, such as the ViewBoundsTracker. Call this when + * the implementation is no longer needed (e.g., in Activity.onDestroy). + */ + public override fun close() { + viewBoundsTracker?.release() + viewBoundsTracker = null + } +} From 11b4748592d2632a2369ca3e07a777777c46d624 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Tue, 13 Jan 2026 10:06:08 +0000 Subject: [PATCH 11/19] Optimize PredictiveBackHandler lookups Previously, `BackHandler` and `PredictiveBackHandler` resolved both Navigation and OnBackPressed owners unconditionally. This created unnecessary composition dependencies and overhead. This change optimizes the lookup by short-circuiting the resolution; the legacy owner is now only observed if the primary Navigation owner is missing. Additionally, this fixes a correctness bug where the internal dispatcher wrapper used `remember` without keys. The handler will now correctly update if the parent dispatcher owner changes after the initial composition. Bug: 468589644 Test: existing passes Change-Id: I396710f43c2318749bb558eb10e999fb2453862a --- .../androidx/activity/compose/BackHandler.kt | 33 ++++++++++-------- .../activity/compose/PredictiveBackHandler.kt | 34 +++++++++++-------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt index 37786d88bd8b6..7bb660e675a6f 100644 --- a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt +++ b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.platform.LocalView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.navigationevent.NavigationEventDispatcherOwner import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner @@ -105,24 +106,26 @@ public object LocalOnBackPressedDispatcherOwner { @OptIn(ExperimentalActivityApi::class) @Composable public fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) { - val navigationEventDispatcherOwner = LocalNavigationEventDispatcherOwner.current - val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + // Short-circuit: Only read the legacy owner if the new one is missing. val owner = - requireNotNull(navigationEventDispatcherOwner ?: onBackPressedDispatcherOwner) { - "No NavigationEventDispatcherOwner was provided via " + - "LocalNavigationEventDispatcherOwner and no OnBackPressedDispatcherOwner was " + - "provided via LocalOnBackPressedDispatcherOwner. Please provide one of the two." + LocalNavigationEventDispatcherOwner.current + ?: LocalOnBackPressedDispatcherOwner.current + ?: error( + "No NavigationEventDispatcherOwner was provided via " + + "LocalNavigationEventDispatcherOwner and no OnBackPressedDispatcherOwner was " + + "provided via LocalOnBackPressedDispatcherOwner. Please provide one of the two." + ) + + val dispatcher = + remember(owner) { + // Create a dispatcher compatibility layer that decides whether to use the new + // 'NavigationEventDispatcher' or the legacy 'OnBackPressedDispatcher'. + BackHandlerDispatcherCompat( + (owner as? NavigationEventDispatcherOwner)?.navigationEventDispatcher, + (owner as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher, + ) } - val dispatcher = remember { - // Create a dispatcher compatibility layer that decides whether to use the new - // 'NavigationEventDispatcher' or the legacy 'OnBackPressedDispatcher'. - BackHandlerDispatcherCompat( - navigationEventDispatcher = navigationEventDispatcherOwner?.navigationEventDispatcher, - onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher, - ) - } - val compositeKey = currentCompositeKeyHashCode val handler = remember(dispatcher, compositeKey) { diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt index d9e96f39bdcf4..b0d195a2d42a4 100644 --- a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt +++ b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt @@ -21,6 +21,7 @@ import androidx.activity.ActivityFlags import androidx.activity.BackEventCompat import androidx.activity.ExperimentalActivityApi import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.compose.internal.BackHandlerCompat import androidx.activity.compose.internal.BackHandlerDispatcherCompat import androidx.compose.runtime.Composable @@ -32,6 +33,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.navigationevent.NavigationEventDispatcherOwner import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner import java.util.concurrent.CancellationException @@ -115,23 +117,25 @@ public fun PredictiveBackHandler( suspend (progress: @JvmSuppressWildcards Flow) -> @JvmSuppressWildcards Unit, ) { - val navigationEventDispatcherOwner = LocalNavigationEventDispatcherOwner.current - val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + // Short-circuit: Only read the legacy owner if the new one is missing. val owner = - requireNotNull(navigationEventDispatcherOwner ?: onBackPressedDispatcherOwner) { - "No NavigationEventDispatcherOwner was provided via " + - "LocalNavigationEventDispatcherOwner and no OnBackPressedDispatcherOwner was " + - "provided via LocalOnBackPressedDispatcherOwner. Please provide one of the two." - } + LocalNavigationEventDispatcherOwner.current + ?: LocalOnBackPressedDispatcherOwner.current + ?: error( + "No NavigationEventDispatcherOwner was provided via " + + "LocalNavigationEventDispatcherOwner and no OnBackPressedDispatcherOwner was " + + "provided via LocalOnBackPressedDispatcherOwner. Please provide one of the two." + ) - val dispatcher = remember { - // Create a dispatcher compatibility layer that decides whether to use the new - // 'NavigationEventDispatcher' or the legacy 'OnBackPressedDispatcher'. - BackHandlerDispatcherCompat( - navigationEventDispatcher = navigationEventDispatcherOwner?.navigationEventDispatcher, - onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher, - ) - } + val dispatcher = + remember(owner) { + // Create a dispatcher compatibility layer that decides whether to use the new + // 'NavigationEventDispatcher' or the legacy 'OnBackPressedDispatcher'. + BackHandlerDispatcherCompat( + (owner as? NavigationEventDispatcherOwner)?.navigationEventDispatcher, + (owner as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher, + ) + } val scope = rememberCoroutineScope() val compositeKey = currentCompositeKeyHashCode From 074eb1364cdcc841da201306a64455e6dcccef6f Mon Sep 17 00:00:00 2001 From: Fred Sladkey Date: Thu, 8 Jan 2026 13:01:18 -0500 Subject: [PATCH 12/19] Throw on added abstract properties as well as functions Test: Added new test Change-Id: I75a45a3a1d8d58e1ff3a49e38f5bfa607e59dc82 --- .../BinaryCompatibilityChecker.kt | 5 ++-- .../BinaryCompatibilityCheckerTest.kt | 23 +++++++++++++++++++ .../adaptive-layout/bcv/native/current.ignore | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt index 28edcd8c1b6c5..3fb10d51116e4 100644 --- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt +++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt @@ -233,8 +233,9 @@ class BinaryCompatibilityChecker( isBinaryCompatibleWith(other, parentName, errs) }, isAllowedAddition = { - when { - this is AbiFunction -> modality != AbiModality.ABSTRACT + when (this) { + is AbiFunction -> modality != AbiModality.ABSTRACT + is AbiProperty -> modality != AbiModality.ABSTRACT else -> true } }, diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt index 9c3105ffd1d60..c9e0b162f607e 100644 --- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt +++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt @@ -1370,6 +1370,29 @@ class BinaryCompatibilityCheckerTest { ) } + @Test + fun addNewAbstractPropertyToClass() { + val beforeText = + """ + abstract class my.lib/MyClass { // my.lib/MyClass|null[0] + constructor () // my.lib/MyClass.|(){}[0] + } + """ + val afterText = + """ + abstract class my.lib/MyClass { // my.lib/MyClass|null[0] + constructor () // my.lib/MyClass.|(){}[0] + abstract val myProperty // my.lib/MyClass.myProperty|{}myProperty[0] + abstract fun (): kotlin/String // my.lib/MyClass.myProperty.|(){}[0] + } + """ + testBeforeAndAfterIsIncompatible( + beforeText, + afterText, + listOf("Added declaration myProperty to my.lib/MyClass"), + ) + } + @Test fun interfaceToFunctionalInterface() { val beforeText = diff --git a/compose/material3/adaptive/adaptive-layout/bcv/native/current.ignore b/compose/material3/adaptive/adaptive-layout/bcv/native/current.ignore index bf4ac5a41f647..b99911f98bdf3 100644 --- a/compose/material3/adaptive/adaptive-layout/bcv/native/current.ignore +++ b/compose/material3/adaptive/adaptive-layout/bcv/native/current.ignore @@ -57,6 +57,7 @@ [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/ExtendedPaneScaffoldPaneScope [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(androidx.compose.foundation.layout/PaddingValues, kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/ExtendedPaneScaffoldScope [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/ExtendedPaneScaffoldScope +[linuxX64]: Added declaration paneMargins to androidx.compose.material3.adaptive.layout/PaneScaffoldParentData [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(androidx.compose.foundation.layout/PaddingValues, kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/PaneScaffoldScope [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/PaneScaffoldScope [linuxX64]: Added declaration (androidx.compose.ui/Modifier).paneMargins(androidx.compose.foundation.layout/PaddingValues, kotlin/Array, androidx.compose.runtime/Composer?, kotlin/Int) to androidx.compose.material3.adaptive.layout/ThreePaneScaffoldPaneScope From aca89973d6d230d3447e46d21801402579d5f463 Mon Sep 17 00:00:00 2001 From: Fred Sladkey Date: Mon, 5 Jan 2026 15:39:08 -0500 Subject: [PATCH 13/19] Allow new abstract methods to be added to sealed classes as long as none of their subclasses are abstract Bug: 432044765 Test: Added new tests Change-Id: I8c032eac04223f7eb936bca95bd0451fdede58f9 --- .../BinaryCompatibilityChecker.kt | 209 +++++++++++------- .../BinaryCompatibilityCheckerTest.kt | 166 ++++++++++++++ 2 files changed, 293 insertions(+), 82 deletions(-) diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt index 3fb10d51116e4..1c21597474a91 100644 --- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt +++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmMain/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt @@ -158,7 +158,12 @@ class BinaryCompatibilityChecker( return } when (this) { - is AbiClass -> isBinaryCompatibleWith(oldDeclaration as AbiClass, errors) + is AbiClass -> + DecoratedAbiClass(this, newLibraryDeclarations) + .isBinaryCompatibleWith( + DecoratedAbiClass(oldDeclaration as AbiClass, oldLibraryDeclarations), + errors, + ) is DecoratedAbiFunction -> isBinaryCompatibleWith(oldDeclaration as DecoratedAbiFunction, errors) is DecoratedAbiProperty -> @@ -171,7 +176,10 @@ class BinaryCompatibilityChecker( } } - private fun AbiClass.isBinaryCompatibleWith(oldClass: AbiClass, errors: CompatibilityErrors) { + private fun DecoratedAbiClass.isBinaryCompatibleWith( + oldClass: DecoratedAbiClass, + errors: CompatibilityErrors, + ) { if (modality != oldClass.modality) { when { modality == AbiModality.OPEN && oldClass.modality == AbiModality.FINAL -> Unit @@ -205,9 +213,9 @@ class BinaryCompatibilityChecker( } // Check that previous supertypes are still currently supertypes - allSuperTypes(newLibraryDeclarations) + allSuperTypes() .isBinaryCompatibleWith( - oldClass.allSuperTypes(oldLibraryDeclarations), + oldClass.allSuperTypes(), entityName = "superType", uniqueId = AbiType::asString, isBinaryCompatibleWith = AbiType::isBinaryCompatibleWith, @@ -223,8 +231,8 @@ class BinaryCompatibilityChecker( errors = errors, isAllowedAddition = { false }, ) - val newDecs = allDeclarationsIncludingInherited(newLibraryDeclarations) - val oldDecs = oldClass.allDeclarationsIncludingInherited(oldLibraryDeclarations) + val newDecs = allDeclarationsIncludingInherited() + val oldDecs = oldClass.allDeclarationsIncludingInherited() newDecs.isBinaryCompatibleWith( oldDecs, entityName = "declaration", @@ -234,8 +242,13 @@ class BinaryCompatibilityChecker( }, isAllowedAddition = { when (this) { - is AbiFunction -> modality != AbiModality.ABSTRACT - is AbiProperty -> modality != AbiModality.ABSTRACT + is DecoratedAbiFunction, + is DecoratedAbiProperty -> (this as HasEffectiveModality).isSafeAddition() + is AbiFunction, + is AbiProperty -> + throw IllegalStateException( + "All functions / properties should be decorated" + ) else -> true } }, @@ -244,70 +257,6 @@ class BinaryCompatibilityChecker( ) } - private fun AbiClass.allSuperTypes(declarations: Map): List { - return superTypes + superTypes.flatMap { it.allSuperTypes(declarations) } - } - - private fun AbiType.allSuperTypes(declarations: Map): List { - val abiClass = declarations[asString()] as? AbiClass ?: return emptyList() - val superTypes = abiClass.superTypes - return superTypes + superTypes.flatMap { it.allSuperTypes(declarations) } - } - - private fun AbiClass.allDeclarationsIncludingInherited( - oldLibraryDeclarations: Map - ): List { - // Collect all the declarations directly on the class (without functions) + - // + all functions, (including inherited). The filterNot is to avoid listing - // functions directly on the class twice. - return declarations.filterNot { it is AbiFunction }.filterNot { it is AbiProperty } + - allMethodsIncludingInherited(oldLibraryDeclarations) + - allPropertiesIncludingInherited(oldLibraryDeclarations) - } - - private fun AbiClass.allPropertiesIncludingInherited( - oldLibraryDeclarations: Map, - baseClass: AbiClass = this, - ): List { - val propertyMap = - declarations - .filterIsInstance() - .associate { it.asUnqualifiedTypeString() to DecoratedAbiProperty(it, baseClass) } - .toMutableMap() - superTypes - .map { - // we should throw here if we can't find the class in the package/dependencies - oldLibraryDeclarations[it.asString()] - } - .filterIsInstance() - .flatMap { it.allPropertiesIncludingInherited(oldLibraryDeclarations, baseClass) } - .associateBy { it.asUnqualifiedTypeString() } - .forEach { (key, prop) -> propertyMap.putIfAbsent(key, prop) } - return propertyMap.values.toList() - } - - private fun AbiClass.allMethodsIncludingInherited( - oldLibraryDeclarations: Map, - baseClass: AbiClass = this, - ): List { - val functionMap = - declarations - .filterIsInstance() - .associate { it.asUnqualifiedTypeString() to DecoratedAbiFunction(it, baseClass) } - .toMutableMap() - superTypes - .map { - oldLibraryDeclarations.getOrElse(it.className.toString()) { - throw IllegalStateException("Missing declaration ${it.asString()}") - } - } - .filterIsInstance() - .flatMap { it.allMethodsIncludingInherited(oldLibraryDeclarations, baseClass) } - .associateBy { it.asUnqualifiedTypeString() } - .forEach { (key, func) -> functionMap.putIfAbsent(key, func) } - return functionMap.values.toList() - } - private fun DecoratedAbiFunction.isBinaryCompatibleWith( otherFunction: DecoratedAbiFunction, errors: CompatibilityErrors, @@ -855,24 +804,120 @@ private fun File.asBaselineErrors(): Set = } } -private class DecoratedAbiFunction(abiFunction: AbiFunction, val parentClass: AbiClass?) : - AbiFunction by abiFunction { - val effectiveModality +private interface HasEffectiveModality { + val effectiveModality: AbiModality + + fun isSafeAddition(): Boolean +} + +private class ClassMember( + private val parentClass: DecoratedAbiClass?, + private val modality: AbiModality, +) : HasEffectiveModality { + override val effectiveModality get() = when (parentClass?.modality) { AbiModality.FINAL -> AbiModality.FINAL else -> modality } + + override fun isSafeAddition(): Boolean { + if (parentClass?.modality == AbiModality.SEALED && !parentClass.hasAbstractSubClasses()) { + return true + } + return modality != AbiModality.ABSTRACT + } } -private class DecoratedAbiProperty(abiProperty: AbiProperty, val parentClass: AbiClass?) : - AbiProperty by abiProperty { - val effectiveModality - get() = - when (parentClass?.modality) { - AbiModality.FINAL -> AbiModality.FINAL - else -> modality +private class DecoratedAbiFunction(abiFunction: AbiFunction, val parentClass: DecoratedAbiClass?) : + AbiFunction by abiFunction, + HasEffectiveModality by ClassMember(parentClass, abiFunction.modality) + +private class DecoratedAbiProperty(abiProperty: AbiProperty, val parentClass: DecoratedAbiClass?) : + AbiProperty by abiProperty, + HasEffectiveModality by ClassMember(parentClass, abiProperty.modality) + +private class DecoratedAbiClass( + abiClass: AbiClass, + private val allDeclarations: Map, +) : AbiClass by abiClass { + + fun allSuperTypes(): List { + return superTypes + superTypes.flatMap { it.allSuperTypes(allDeclarations) } + } + + fun subClasses(): List { + return allDeclarations.values.filterIsInstance().filter { abiClass -> + DecoratedAbiClass(abiClass, allDeclarations).allSuperTypes().any { + it.className == qualifiedName + } + } + } + + fun hasAbstractSubClasses(): Boolean = subClasses().any { it.modality == AbiModality.ABSTRACT } + + fun allDeclarationsIncludingInherited(): List { + // Collect all the declarations directly on the class (without functions / properties) + + // + all functions / properties (including inherited). The filterNot is to avoid listing + // functions / properties directly on the class twice. + return declarations.filterNot { it is AbiFunction || it is AbiProperty } + + allMethodsIncludingInherited() + + allPropertiesIncludingInherited() + } + + fun allPropertiesIncludingInherited(baseClass: AbiClass = this): List { + val propertyMap = + declarations + .filterIsInstance() + .associate { + it.asUnqualifiedTypeString() to + DecoratedAbiProperty(it, DecoratedAbiClass(baseClass, allDeclarations)) + } + .toMutableMap() + superTypes + .asSequence() + .map { + allDeclarations.getOrElse(it.className.toString()) { + throw IllegalStateException("Missing declaration ${it.asString()}") + } + } + .filterIsInstance() + .map { DecoratedAbiClass(it, allDeclarations) } + .flatMap { it.allPropertiesIncludingInherited(baseClass) } + .associateBy { it.asUnqualifiedTypeString() } + .forEach { (key, prop) -> propertyMap.putIfAbsent(key, prop) } + return propertyMap.values.toList() + } + + fun allMethodsIncludingInherited(baseClass: AbiClass = this): List { + val functionMap = + declarations + .filterIsInstance() + .associate { + it.asUnqualifiedTypeString() to + DecoratedAbiFunction(it, DecoratedAbiClass(baseClass, allDeclarations)) + } + .toMutableMap() + superTypes + .asSequence() + .map { + allDeclarations.getOrElse(it.className.toString()) { + throw IllegalStateException("Missing declaration ${it.asString()}") + } } + .filterIsInstance() + .map { DecoratedAbiClass(it, allDeclarations) } + .flatMap { it.allMethodsIncludingInherited(baseClass) } + .associateBy { it.asUnqualifiedTypeString() } + .forEach { (key, func) -> functionMap.putIfAbsent(key, func) } + return functionMap.values.toList() + } + + private fun AbiType.allSuperTypes(declarations: Map): List { + val abiClass = declarations[asString()] as? AbiClass ?: return emptyList() + val superTypes = abiClass.superTypes + return superTypes + superTypes.flatMap { it.allSuperTypes(declarations) } + } } private class DecoratedAbiValueParameter(val index: Int, param: AbiValueParameter) : diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt index c9e0b162f607e..8d33c3eb054c4 100644 --- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt +++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/jvmTest/kotlin/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt @@ -1623,6 +1623,172 @@ class BinaryCompatibilityCheckerTest { testBeforeAndAfterIsCompatible(beforeText, afterText) } + @Test + fun newMethodToSealedClassExtendedByAbstractIsInvalid() { + val beforeText = + """ + abstract class example/Abstract : example/Sealed { // example/Abstract|null[0] + constructor () // example/Abstract.|(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + abstract class example/Abstract : example/Sealed { // example/Abstract|null[0] + constructor () // example/Abstract.|(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsIncompatible( + beforeText, + afterText, + listOf("Added declaration newFunFromSealed() to example/Sealed"), + ) + } + + @Test + @Ignore("Not implemented yet") + fun newMethodToSealedClassExtendedByAbstractIsValidIfAdditionIsConcreteInSubclass() { + val beforeText = + """ + abstract class example/Concrete : example/Sealed { // example/Concrete|null[0] + constructor () // example/Concrete.|(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + abstract class example/Concrete : example/Sealed { // example/Concrete|null[0] + constructor () // example/Concrete.|(){}[0] + open fun newFunFromSealed() // example/Concrete.newFunFromSealed|newFunFromSealed(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsCompatible(beforeText, afterText) + } + + @Test + fun newMethodToSealedClassWithAbstractSubclassThatIsAlsoSealedWithNoAbstractSubclass() { + val beforeText = + """ + sealed class example/DoubleSealed : example/Sealed { // example/DoubleSealed|null[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + sealed class example/DoubleSealed : example/Sealed { // example/DoubleSealed|null[0] + open fun newFunFromSealed() // example/DoubleSealed.newFunFromSealed|newFunFromSealed(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsCompatible(beforeText, afterText) + } + + @Test + fun newMethodToSealedClassWithAbstractSubclassThatIsAlsoSealedWithAbstractSubclassIsIncompatible() { + val beforeText = + """ + abstract class example/DoubleSealedAbstract : example/DoubleSealed { // example/DoubleSealedAbstract|null[0] + constructor () // example/DoubleSealedAbstract.|(){}[0] + } + sealed class example/DoubleSealed : example/Sealed { // example/DoubleSealed|null[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + abstract class example/DoubleSealedAbstract : example/DoubleSealed { // example/DoubleSealedAbstract|null[0] + constructor () // example/DoubleSealedAbstract.|(){}[0] + } + sealed class example/DoubleSealed : example/Sealed { // example/DoubleSealed|null[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsIncompatible( + beforeText, + afterText, + listOf("Added declaration newFunFromSealed() to example/Sealed"), + ) + } + + @Test + fun newMethodToSealedClassExtendedByAnotherSealedWithNoAbstractChildren() { + val beforeText = + """ + abstract class example/DoubleSealedChild : example/DoubleSealed { // example/DoubleSealedChild|null[0] + constructor () // example/DoubleSealedChild.|(){}[0] + } + sealed class example/DoubleSealed : example/Sealed // example/DoubleSealed|null[0] + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + abstract class example/DoubleSealedChild : example/DoubleSealed { // example/DoubleSealedChild|null[0] + constructor () // example/DoubleSealedChild.|(){}[0] + } + sealed class example/DoubleSealed : example/Sealed // example/DoubleSealed|null[0] + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsIncompatible( + beforeText, + afterText, + listOf("Added declaration newFunFromSealed() to example/Sealed"), + ) + } + + @Test + fun newMethodToSealedClassNotExtendedByAbstractIsValid() { + val beforeText = + """ + final class example/Concrete : example/Sealed { // example/Concrete|null[0] + constructor () // example/Concrete.|(){}[0] + final fun funFromSealed() // example/Concrete.funFromSealed|funFromSealed(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + } + """ + val afterText = + """ + final class example/Concrete : example/Sealed { // example/Concrete|null[0] + constructor () // example/Concrete.|(){}[0] + final fun funFromSealed() // example/Concrete.funFromSealed|funFromSealed(){}[0] + final fun newFunFromSealed() // example/Concrete.newFunFromSealed|newFunFromSealed(){}[0] + } + sealed class example/Sealed { // example/Sealed|null[0] + abstract fun funFromSealed() // example/Sealed.funFromSealed|funFromSealed(){}[0] + abstract fun newFunFromSealed() // example/Sealed.newFunFromSealed|newFunFromSealed(){}[0] + } + """ + testBeforeAndAfterIsCompatible(beforeText, afterText) + } + @Ignore // b/409298472 @Test fun changedOrdinalOfEnumEntries() { From 8e2310b5ba567aea6f50dc80b7b3b08249a73c24 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Tue, 13 Jan 2026 06:27:12 -0800 Subject: [PATCH 14/19] Fix typo Change-Id: If43866df0d46cf1455f0523cb7c011fc9e2fa9c8 --- .../compose/remote/creation/compose/capture/WriterEvents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/WriterEvents.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/WriterEvents.kt index a4695d8b2ce49..367a1452c32a9 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/WriterEvents.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/WriterEvents.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.util.fastForEachIndexed /** * A callback interface used during the capture process to write out the captured composable - * information. This allows the capture system to be pass on types that can't be serialized into the + * information. This allows the capture system to pass on types that can't be serialized into the * document such as PendingIntent. * * Implementations of this interface will handle the serialization or transformation of the captured From 784363f131150b8b9e46db55d3f1617c0379b42f Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Fri, 9 Jan 2026 12:16:19 +0000 Subject: [PATCH 15/19] Introduce RemoteStateScope and use for id generation RemoteStateScope indicates safe places to generate ids, and replaces FallbackState. Test: Existing Bug: 465453482 Change-Id: Ibd51038c5a4b96e0cb13f70b7eab26a8acf88a71 --- .../remote/integration/demos/ListActivity.kt | 3 +- .../integration/view/demos/examples/Clock.kt | 15 +- .../view/demos/examples/PathChecks.kt | 25 ++-- .../view/demos/examples/RemoteImage.kt | 55 ++----- .../remote/remote-creation-compose/GEMINI.md | 4 +- .../compose/remote/a11y/ListA11yTest.kt | 3 +- .../compose/layout/RemoteCanvasTest.kt | 14 +- .../compose/modifier/BorderModifierTest.kt | 85 +++++++++++ .../compose/vector/RemoteVectorPainterTest.kt | 6 +- .../remote/creation/compose/action/Action.kt | 4 +- .../creation/compose/action/CombinedAction.kt | 6 +- .../creation/compose/action/HostAction.kt | 28 ++-- .../compose/action/PendingIntentAction.kt | 17 +-- .../creation/compose/action/ValueChange.kt | 48 +++---- .../creation/compose/capture/Capture.kt | 12 +- .../compose/capture/RecordingCanvas.kt | 7 +- .../compose/capture/RemoteComposeCapture.kt | 4 - .../compose/capture/RemoteDrawScope0.kt | 9 +- .../compose/capture/RemoteImageVector.kt | 6 +- .../compose/capture/RemotePathParser.kt | 6 +- .../creation/compose/layout/DrawHelpers.kt | 2 +- .../remote/creation/compose/layout/FitBox.kt | 7 +- .../compose/layout/RemoteAlignment.kt | 8 +- .../compose/layout/RemoteArrangement.kt | 12 +- .../creation/compose/layout/RemoteBox.kt | 7 +- .../creation/compose/layout/RemoteCanvas.kt | 7 +- .../creation/compose/layout/RemoteCanvas0.kt | 57 ++++---- .../compose/layout/RemoteCanvasComposable.kt | 5 +- .../compose/layout/RemoteCanvasDrawScope0.kt | 134 +++++++++--------- .../compose/layout/RemoteCollapsibleColumn.kt | 9 +- .../compose/layout/RemoteCollapsibleRow.kt | 9 +- .../creation/compose/layout/RemoteColumn.kt | 7 +- .../compose/layout/RemoteDrawScope.kt | 5 +- .../compose/layout/RemoteFloatContext.kt | 3 +- .../creation/compose/layout/RemoteOffset.kt | 7 +- .../creation/compose/layout/RemotePath.kt | 96 ------------- .../creation/compose/layout/RemoteRow.kt | 9 +- .../creation/compose/layout/RemoteSize.kt | 7 +- .../compose/layout/RemoteStringList.kt | 4 +- .../creation/compose/layout/RemoteText.kt | 12 +- .../creation/compose/layout/StateLayout.kt | 10 +- .../modifier/AlignByBaselineModifier.kt | 3 +- .../compose/modifier/AnimateSpecModifier.kt | 3 +- .../compose/modifier/BackgroundModifier.kt | 10 +- .../compose/modifier/BorderModifier.kt | 27 ++-- .../compose/modifier/ClickActionModifier.kt | 5 +- .../creation/compose/modifier/ClipModifier.kt | 3 +- .../modifier/CollapsiblePriorityModifier.kt | 5 +- .../modifier/DrawWithContentModifier.kt | 3 +- .../compose/modifier/GraphicsLayerModifier.kt | 108 +++++++------- .../compose/modifier/HeightInModifier.kt | 3 +- .../compose/modifier/HeightModifier.kt | 8 +- .../compose/modifier/MarqueeModifier.kt | 3 +- .../compose/modifier/OffsetModifier.kt | 8 +- .../compose/modifier/PaddingModifier.kt | 19 +-- .../compose/modifier/RemoteModifier.kt | 32 +++-- .../compose/modifier/RippleModifier.kt | 3 +- .../compose/modifier/ScrollModifier.kt | 5 +- .../compose/modifier/SemanticsModifier.kt | 14 +- .../modifier/TouchCancelActionModifier.kt | 5 +- .../modifier/TouchDownActionModifier.kt | 5 +- .../compose/modifier/TouchUpActionModifier.kt | 5 +- .../compose/modifier/VisibilityModifier.kt | 6 +- .../compose/modifier/WidthInModifier.kt | 3 +- .../compose/modifier/WidthModifier.kt | 8 +- .../compose/modifier/ZIndexModifier.kt | 5 +- .../creation/compose/shaders/RemoteBrush.kt | 3 +- .../compose/shaders/RemoteLinearGradient.kt | 5 +- .../compose/shaders/RemoteRadialGradient.kt | 3 +- .../compose/shaders/RemoteSolidColor.kt | 3 +- .../compose/shaders/RemoteSweepGradient.kt | 3 +- .../creation/compose/shapes/RemoteOutline.kt | 49 ++++--- .../creation/compose/state/RemoteBitmap.kt | 74 ++++------ .../creation/compose/state/RemoteColor.kt | 19 +-- .../creation/compose/state/RemoteFloat.kt | 45 ++---- .../compose/state/RemoteFloatArray.kt | 2 +- .../creation/compose/state/RemoteInt.kt | 33 ----- .../creation/compose/state/RemoteLong.kt | 13 -- .../creation/compose/state/RemotePaint.kt | 4 +- .../creation/compose/state/RemoteState.kt | 14 -- .../compose/state/RemoteStateScope.kt | 45 ++++++ .../creation/compose/state/RemoteString.kt | 35 ++++- .../compose/v2/RemoteComposeNodeV2.kt | 47 +++--- .../compose/vector/RemotePathBuilder.kt | 70 +++++---- .../creation/compose/vector/RemoteVector.kt | 8 +- .../compose/action/CombinedActionTest.kt | 10 +- .../compose/action/PendingIntentActionTest.kt | 10 +- .../compose/state/RemoteFloatArrayTest.kt | 5 +- .../creation/compose/capture/BlendModeTest.kt | 12 +- .../creation/compose/state/RemoteStateTest.kt | 11 +- .../wear/parcel/WearWidgetCaptureTest.kt | 12 +- .../material3/previews/RemoteIconPreview.kt | 6 +- .../material3/previews/TestImageVectors.kt | 6 +- .../remote/material3/TestImageVectors.kt | 6 +- .../compose/remote/material3/RemoteButton.kt | 2 +- .../compose/remote/material3/RemoteImage.kt | 15 +- .../remote/material3/RemoteTimeText.kt | 2 +- 97 files changed, 829 insertions(+), 816 deletions(-) create mode 100644 compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/modifier/BorderModifierTest.kt delete mode 100644 compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemotePath.kt create mode 100644 compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteStateScope.kt diff --git a/compose/remote/integration-tests/demos/src/main/java/androidx/compose/remote/integration/demos/ListActivity.kt b/compose/remote/integration-tests/demos/src/main/java/androidx/compose/remote/integration/demos/ListActivity.kt index 4a9955397429e..4f465c72e347f 100644 --- a/compose/remote/integration-tests/demos/src/main/java/androidx/compose/remote/integration/demos/ListActivity.kt +++ b/compose/remote/integration-tests/demos/src/main/java/androidx/compose/remote/integration/demos/ListActivity.kt @@ -38,6 +38,7 @@ import androidx.compose.remote.creation.compose.modifier.rememberRemoteScrollSta import androidx.compose.remote.creation.compose.modifier.semantics import androidx.compose.remote.creation.compose.modifier.verticalScroll import androidx.compose.remote.creation.compose.state.RemoteColor +import androidx.compose.remote.creation.compose.state.rc import androidx.compose.remote.creation.compose.state.rdp import androidx.compose.remote.player.compose.RemoteDocumentPlayer import androidx.compose.remote.player.core.RemoteDocument @@ -109,7 +110,7 @@ fun ScrollableList(modifier: RemoteModifier = RemoteModifier) { modifier = RemoteModifier.fillMaxWidth() .height(96.rdp) - .border(1.rdp, Color.LightGray) + .border(1.rdp, Color.LightGray.rc) // Must be direct child of the scrollable item .semantics(mergeDescendants = true) {}, horizontalAlignment = RemoteAlignment.CenterHorizontally, diff --git a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/Clock.kt b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/Clock.kt index bd2770743d2cf..f92eb12770c6a 100644 --- a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/Clock.kt +++ b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/Clock.kt @@ -150,7 +150,10 @@ fun RcSimpleClock1( // hour hand withTransform({ - translate(centerX - 64f, (centerY + top) / 2f) + translate( + with(this@RemoteCanvas0) { (centerX - 64f).floatId }, + with(this@RemoteCanvas0) { ((centerY + top) / 2f).floatId }, + ) scale(5f, 5f, Offset(0f, 0f)) }) { drawPath(androidPath.asComposePath(), Color(0xFFA4C639)) @@ -221,8 +224,8 @@ fun RcSimpleClock1( path.close() translate( - (centerX).internalAsFloat(), - (faceTop + bezel_thick / 2f - 20f).internalAsFloat(), + (centerX).floatId, + (faceTop + bezel_thick / 2f - 20f).floatId, ) { drawPath(path = path, color = minHandColor) } @@ -293,9 +296,9 @@ fun RcSimpleClock1( val gmtPath = Path() gmtPath.moveTo(1f, 1f) - gmtPath.moveTo(centerX - 20f, top + (bezel_thick + 60f)) - gmtPath.lineTo(centerX + 20f, top + (bezel_thick + 60f)) - gmtPath.lineTo(centerX, top + (bezel_thick + 30f)) + gmtPath.moveTo((centerX - 20f).floatId, (top + (bezel_thick + 60f)).floatId) + gmtPath.lineTo((centerX + 20f).floatId, (top + (bezel_thick + 60f)).floatId) + gmtPath.lineTo(centerX.floatId, (top + (bezel_thick + 30f)).floatId) gmtPath.close() rotate(gmtAngle, centerX, centerY) { diff --git a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/PathChecks.kt b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/PathChecks.kt index 6697798395292..680b96fac7984 100644 --- a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/PathChecks.kt +++ b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/PathChecks.kt @@ -17,7 +17,6 @@ package androidx.compose.remote.integration.view.demos.examples import android.graphics.Typeface -import androidx.compose.remote.creation.compose.capture.RecordingCanvas import androidx.compose.remote.creation.compose.layout.RemoteAlignment import androidx.compose.remote.creation.compose.layout.RemoteCanvas0 import androidx.compose.remote.creation.compose.layout.RemoteComposable @@ -28,6 +27,7 @@ import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.fillMaxHeight import androidx.compose.remote.creation.compose.modifier.fillMaxSize import androidx.compose.remote.creation.compose.modifier.fillMaxWidth +import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.tooling.preview.RemotePreview import androidx.compose.runtime.Composable import androidx.compose.ui.geometry.Rect @@ -37,7 +37,6 @@ import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview @@ -50,14 +49,11 @@ fun SimplePath() { verticalAlignment = RemoteAlignment.CenterVertically, ) { RemoteCanvas0(modifier = RemoteModifier.fillMaxWidth().fillMaxHeight()) { - val rec = - Rect( - 0f, - 0f, - remote.component.width.internalAsFloat(), - remote.component.height.internalAsFloat(), - ) - drawRect(Color.DarkGray, RemoteOffset(rec.topLeft), RemoteSize(rec.size)) + drawRect( + Color.DarkGray, + RemoteOffset(0f.rf, 0f.rf), + RemoteSize(remote.component.width, remote.component.height), + ) val path = Path().apply { @@ -73,7 +69,7 @@ fun SimplePath() { } .asFrameworkPaint() .apply { - textSize = with(density) { 32f } + textSize = 32f typeface = Typeface.DEFAULT color = Color.Red.toArgb() @@ -82,12 +78,7 @@ fun SimplePath() { } drawPath(path, color = Color.Green, style = Stroke(4f)) - val canvas = drawScope.drawContext.canvas.nativeCanvas - if (canvas is RecordingCanvas) { - canvas.drawTextOnPath("10:10", path.asAndroidPath(), 20f, 0f, textPaint) - } else { - canvas.drawTextOnPath("10:10", path.asAndroidPath(), 20f, 0f, textPaint) - } + canvas.drawTextOnPath("10:10", path.asAndroidPath(), 20f, 0f, textPaint) } } } diff --git a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/RemoteImage.kt b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/RemoteImage.kt index 3e662df5e37c5..7b5865a2fe8a4 100644 --- a/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/RemoteImage.kt +++ b/compose/remote/integration-tests/player-view-demos/src/main/java/androidx/compose/remote/integration/view/demos/examples/RemoteImage.kt @@ -19,12 +19,11 @@ package androidx.compose.remote.integration.view.demos.examples import androidx.compose.foundation.layout.Box import androidx.compose.remote.core.operations.utilities.ImageScaling import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState -import androidx.compose.remote.creation.compose.capture.RecordingCanvas import androidx.compose.remote.creation.compose.layout.RemoteComposable +import androidx.compose.remote.creation.compose.layout.drawIntoRemoteCanvas import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.semantics import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout -import androidx.compose.remote.creation.compose.state.RemoteBitmap import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.rf @@ -34,8 +33,6 @@ import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.layout.ContentScale @Suppress("RestrictedApiAndroidX") @@ -60,17 +57,13 @@ internal class RemoteComposeImageModifier( val alpha: RemoteFloat, ) : DrawModifier { override fun ContentDrawScope.draw() { - drawIntoCanvas { - if (it.nativeCanvas is RecordingCanvas) { - (it.nativeCanvas as RecordingCanvas) - .document - .image( - modifier.toRemoteCompose(), - bitmapId, - contentScale.toRemoteCompose(), - alpha.internalAsFloat(), - ) - } + drawIntoRemoteCanvas { + it.document.image( + with(modifier) { it.toRecordingModifier() }, + bitmapId, + contentScale.toRemoteCompose(), + with(it) { alpha.floatId }, + ) } } } @@ -108,35 +101,3 @@ public fun RemoteImage( ) ) } - -/** - * A composable that lays out and draws a given [RemoteBitmap]. This is the remote equivalent of - * [androidx.compose.foundation.Image]. - * - * @param remoteBitmap The [RemoteBitmap] to be drawn. - * @param contentDescription Text used by accessibility services to describe what this image - * represents. - * @param modifier The [RemoteModifier] to be applied to this layout node. - * @param contentScale The rule to apply to scale the image when its size does not match the layout - * size. Defaults to [ContentScale.Fit]. - * @param alpha Optional opacity to be applied to the [remoteBitmap] when it is rendered. - */ -@Suppress("RestrictedApiAndroidX") -@Composable -@RemoteComposable -public fun RemoteImage( - remoteBitmap: RemoteBitmap, - contentDescription: RemoteString?, - modifier: RemoteModifier = RemoteModifier, - contentScale: ContentScale = ContentScale.Fit, - alpha: RemoteFloat = DefaultAlpha.rf, -) { - @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 - Box( - modifier = - RemoteComposeImageModifier(modifier, remoteBitmap.id, contentScale, alpha) - .then( - modifier.semantics { contentDescription?.let { text = it } }.toComposeUiLayout() - ) - ) -} diff --git a/compose/remote/remote-creation-compose/GEMINI.md b/compose/remote/remote-creation-compose/GEMINI.md index 2cabf15ee7859..31e1aa651fa9e 100644 --- a/compose/remote/remote-creation-compose/GEMINI.md +++ b/compose/remote/remote-creation-compose/GEMINI.md @@ -23,8 +23,8 @@ All public drawing APIs should prioritize "Remote" types over standard platform ### 1. Handling `RemoteFloat` `RemoteFloat` can represent either a constant value or a dynamic expression (identified by an ID). -- **NEVER** use `internalAsFloat()` for arithmetic or logic. It returns the remote ID encoded as a NaN, which will corrupt any calculation. -- **DO** use `getFloatIdForCreationState(creationState)` when you need to serialize the value/ID into a `RecordingCanvas` command. If this is required put a TODO in RecordingCanvas to add a Remote overload. +- **NEVER** use `RemoteFloat.floatId` for arithmetic or logic. It returns the remote ID encoded as a NaN, which will corrupt any calculation. +- **DO** use `RemoteStateScope` when you need to serialize the value/ID into a `RecordingCanvas` command. If this is required put a TODO in RecordingCanvas to add a Remote overload. ### 2. `RemotePaint` usage `RemotePaint` is a critical bridge. It allows standard `android.graphics.Paint` properties to be associated with remote IDs. diff --git a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/a11y/ListA11yTest.kt b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/a11y/ListA11yTest.kt index 001a9840770b3..9d7425b180f21 100644 --- a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/a11y/ListA11yTest.kt +++ b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/a11y/ListA11yTest.kt @@ -34,6 +34,7 @@ import androidx.compose.remote.creation.compose.modifier.rememberRemoteScrollSta import androidx.compose.remote.creation.compose.modifier.semantics import androidx.compose.remote.creation.compose.modifier.verticalScroll import androidx.compose.remote.creation.compose.state.RemoteColor +import androidx.compose.remote.creation.compose.state.rc import androidx.compose.remote.creation.compose.state.rdp import androidx.compose.remote.player.compose.test.utils.screenshot.TargetPlayer import androidx.compose.remote.player.compose.test.utils.screenshot.rule.RemoteComposeScreenshotTestRule @@ -134,7 +135,7 @@ class ListA11yTest { modifier = RemoteModifier.fillMaxWidth() .height(192.rdp) - .border(1.rdp, Color.LightGray) + .border(1.rdp, Color.LightGray.rc) // Must be direct child of the scrollable item .semantics(mergeDescendants = true) {}, horizontalAlignment = RemoteAlignment.CenterHorizontally, diff --git a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasTest.kt b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasTest.kt index ce294f1868989..6ef90929bea26 100644 --- a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasTest.kt +++ b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasTest.kt @@ -110,7 +110,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { color = Color.Red.toArgb() - textSize = SMALL_FONT_SIZE.rf.id + textSize = SMALL_FONT_SIZE }, ) drawAnchoredText( @@ -120,7 +120,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { color = Color.Green.toArgb() - textSize = MEDIUM_FONT_SIZE.rf.id + textSize = MEDIUM_FONT_SIZE }, ) drawAnchoredText( @@ -130,7 +130,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { color = Color.Blue.toArgb() - textSize = LARGE_FONT_SIZE.rf.id + textSize = LARGE_FONT_SIZE }, ) } @@ -180,7 +180,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { applyRemoteBrush(RemoteBrush.solidColor(Color.Red.rc), remoteSize) - textSize = SMALL_FONT_SIZE.rf.id + textSize = SMALL_FONT_SIZE }, ) drawAnchoredText( @@ -190,7 +190,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { applyRemoteBrush(RemoteBrush.solidColor(Color.Green.rc), remoteSize) - textSize = MEDIUM_FONT_SIZE.rf.id + textSize = MEDIUM_FONT_SIZE }, ) drawAnchoredText( @@ -200,7 +200,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { applyRemoteBrush(RemoteBrush.solidColor(Color.Blue.rc), remoteSize) - textSize = LARGE_FONT_SIZE.rf.id + textSize = LARGE_FONT_SIZE }, ) } @@ -220,7 +220,7 @@ class RemoteCanvasTest { paint = RemotePaint().apply { remoteColor = color - textSize = SMALL_FONT_SIZE.rf.id + textSize = SMALL_FONT_SIZE }, ) } diff --git a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/modifier/BorderModifierTest.kt b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/modifier/BorderModifierTest.kt new file mode 100644 index 0000000000000..1e740dd65d2bd --- /dev/null +++ b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/modifier/BorderModifierTest.kt @@ -0,0 +1,85 @@ +/* + * 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.compose.remote.creation.compose.modifier + +import androidx.compose.remote.creation.compose.SCREENSHOT_GOLDEN_DIRECTORY +import androidx.compose.remote.creation.compose.layout.RemoteBox +import androidx.compose.remote.creation.compose.layout.RemoteComposable +import androidx.compose.remote.creation.compose.state.rc +import androidx.compose.remote.creation.compose.state.rdp +import androidx.compose.remote.creation.compose.state.rf +import androidx.compose.remote.creation.compose.test.base.GridScreenshotUI +import androidx.compose.remote.creation.compose.test.base.GridScreenshotUI.Companion.DefaultContainerSize +import androidx.compose.remote.player.compose.test.utils.screenshot.TargetPlayer +import androidx.compose.remote.player.compose.test.utils.screenshot.rule.RemoteComposeScreenshotTestRule +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.test.filters.MediumTest +import androidx.test.filters.SdkSuppress +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@MediumTest +@SdkSuppress(minSdkVersion = 35, maxSdkVersion = 35) +@RunWith(TestParameterInjector::class) +class BorderModifierTest { + @TestParameter private lateinit var targetPlayer: TargetPlayer + + @get:Rule + val composeTestRule: RemoteComposeScreenshotTestRule by lazy { + RemoteComposeScreenshotTestRule( + moduleDirectory = SCREENSHOT_GOLDEN_DIRECTORY, + targetPlayer = targetPlayer, + ) + } + + private val gridScreenshotUI = GridScreenshotUI() + + @Test + fun grid() = + composeTestRule.runScreenshotTest { + val borders = + listOf RemoteModifier>( + { this }, + { border(1.rdp, Color.Red.rc) }, + { border(2.rdp, Color.Red.rc) }, + { border(3.rdp, Color.Red.rc) }, + { border(3.rdp, Color.Blue.rc.copy(alpha = 0.5f.rf)) }, + ) + + gridScreenshotUI.GridContent( + sequence { + for (borderFn in borders) { + yield( + @RemoteComposable @Composable { + RemoteBox { + RemoteBox( + modifier = + RemoteModifier.size(DefaultContainerSize).borderFn() + ) + } + } + ) + } + } + .toList() + ) + } +} diff --git a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/vector/RemoteVectorPainterTest.kt b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/vector/RemoteVectorPainterTest.kt index f61ad8047b9fe..72141dc063f04 100644 --- a/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/vector/RemoteVectorPainterTest.kt +++ b/compose/remote/remote-creation-compose/src/androidTest/java/androidx/compose/remote/creation/compose/vector/RemoteVectorPainterTest.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.vector import android.content.Context import androidx.compose.remote.creation.CreationDisplayInfo import androidx.compose.remote.creation.compose.SCREENSHOT_GOLDEN_DIRECTORY +import androidx.compose.remote.creation.compose.capture.NoRemoteCompose import androidx.compose.remote.creation.compose.capture.RemoteImageVector import androidx.compose.remote.creation.compose.layout.RemoteBox import androidx.compose.remote.creation.compose.layout.RemoteCanvas @@ -232,15 +233,18 @@ private object TestImageVectors { ) .build() + val testRemoteStateScope = NoRemoteCompose() + val RemoteVolumeUp = RemoteImageVector.Builder( + testRemoteStateScope, name = "Volume up", viewportWidth = 24.0f.rf, viewportHeight = 24.0f.rf, tintColor = RemoteColor(Color.Black), ) .addPath( - RemotePathData { + RemotePathData(testRemoteStateScope) { moveTo(3.0f.rf, 9.0f.rf) verticalLineToRelative(6.0f.rf) horizontalLineToRelative(4.0f.rf) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/Action.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/Action.kt index 7951cd8dfb5fb..aa20f856f1385 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/Action.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/Action.kt @@ -18,7 +18,7 @@ package androidx.compose.remote.creation.compose.action import androidx.annotation.RestrictTo -import androidx.compose.remote.creation.actions.Action +import androidx.compose.remote.creation.compose.state.RemoteStateScope /** * A RemoteCompose frontend model of Actions that can be converted to either RemoteCompose @@ -26,5 +26,5 @@ import androidx.compose.remote.creation.actions.Action */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public interface Action { - public fun toRemoteAction(): Action + public fun RemoteStateScope.toRemoteAction(): androidx.compose.remote.creation.actions.Action } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/CombinedAction.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/CombinedAction.kt index dc1f0aa58d888..6c57c32868248 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/CombinedAction.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/CombinedAction.kt @@ -18,14 +18,16 @@ package androidx.compose.remote.creation.compose.action import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.compose.state.RemoteStateScope /** Creates an action that's a composite of multiple actions. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class CombinedAction(public vararg val actions: Action) : Action { - override fun toRemoteAction(): androidx.compose.remote.creation.actions.Action { + override fun RemoteStateScope.toRemoteAction(): + androidx.compose.remote.creation.actions.Action { return androidx.compose.remote.creation.actions.Action { writer -> for (action in actions) { - action.toRemoteAction().write(writer) + with(action) { toRemoteAction().write(writer) } } } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/HostAction.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/HostAction.kt index 2f2a064fbe660..b26eb1da72bef 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/HostAction.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/HostAction.kt @@ -20,9 +20,10 @@ package androidx.compose.remote.creation.compose.action import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.layout.modifiers.HostNamedActionOperation import androidx.compose.remote.creation.actions.HostAction -import androidx.compose.remote.creation.compose.state.FallbackCreationState import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteInt +import androidx.compose.remote.creation.compose.state.RemoteState +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString /** Run the named host action when invoked. */ @@ -30,7 +31,7 @@ import androidx.compose.remote.creation.compose.state.RemoteString public class HostAction( public val name: RemoteString, public val type: Type = Type.INT, - public var id: Int = -1, + public val value: RemoteState<*>? = null, ) : Action { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -43,27 +44,20 @@ public class HostAction( } // TODO: Add a RemoteFloatArray type and use it here! - public constructor( - name: RemoteString, - value: RemoteFloat, - ) : this(name, Type.FLOAT, value.getIdForCreationState(FallbackCreationState.state)) + public constructor(name: RemoteString, value: RemoteFloat) : this(name, Type.FLOAT, value) - public constructor( - name: RemoteString, - value: RemoteInt, - ) : this(name, Type.INT, value.getIdForCreationState(FallbackCreationState.state)) + public constructor(name: RemoteString, value: RemoteInt) : this(name, Type.INT, value) - public constructor( - name: RemoteString, - value: RemoteString, - ) : this(name, Type.STRING, value.getIdForCreationState(FallbackCreationState.state)) + public constructor(name: RemoteString, value: RemoteString) : this(name, Type.STRING, value) - override fun toRemoteAction(): androidx.compose.remote.creation.actions.Action { + override fun RemoteStateScope.toRemoteAction(): + androidx.compose.remote.creation.actions.Action { + val valueId = value?.id ?: -1 val constantValue = name.constantValue return if (constantValue != null) { - HostAction(constantValue, type.ordinal, id) + HostAction(constantValue, type.ordinal, valueId) } else { - HostAction(name.getIdForCreationState(FallbackCreationState.state), id) + HostAction(name.id, valueId) } } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/PendingIntentAction.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/PendingIntentAction.kt index 6ebb35d8feedd..10636b164001b 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/PendingIntentAction.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/PendingIntentAction.kt @@ -22,28 +22,25 @@ import android.app.PendingIntent import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.creation.compose.ExperimentalRemoteCreationComposeApi -import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState -import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.creation.compose.capture.WriterEvents +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.runtime.Composable /** Create a [PendingIntentAction] to send the [PendingIntent] when invoked. */ @Composable public fun pendingIntentAction(pendingIntent: PendingIntent): PendingIntentAction = - PendingIntentAction(LocalRemoteComposeCreationState.current, pendingIntent) + PendingIntentAction(pendingIntent) /** Send the [PendingIntent] when invoked. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class PendingIntentAction( - public val remoteComposeCreationState: RemoteComposeCreationState, - public val pendingIntent: PendingIntent, -) : Action { +public class PendingIntentAction(public val pendingIntent: PendingIntent) : Action { - override fun toRemoteAction(): androidx.compose.remote.creation.actions.Action { - val writerCallback = remoteComposeCreationState.document.writerCallback + override fun RemoteStateScope.toRemoteAction(): + androidx.compose.remote.creation.actions.Action { + val writerCallback = document.writerCallback if (writerCallback is WriterEvents) { val index = writerCallback.storePendingIntent(pendingIntent) - val valueId = remoteComposeCreationState.document.addInteger(index) + val valueId = document.addInteger(index) return androidx.compose.remote.creation.actions.HostAction( ACTION_NAME, HostAction.Type.INT.ordinal, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/ValueChange.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/ValueChange.kt index 69931bb97eb93..f60f4fb8c1b30 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/ValueChange.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/action/ValueChange.kt @@ -18,14 +18,12 @@ package androidx.compose.remote.creation.compose.action import androidx.annotation.RestrictTo -import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.creation.actions.Action import androidx.compose.remote.creation.actions.ValueFloatChange import androidx.compose.remote.creation.actions.ValueFloatExpressionChange import androidx.compose.remote.creation.actions.ValueIntegerChange import androidx.compose.remote.creation.actions.ValueIntegerExpressionChange import androidx.compose.remote.creation.actions.ValueStringChange -import androidx.compose.remote.creation.compose.state.FallbackCreationState import androidx.compose.remote.creation.compose.state.MutableRemoteFloat import androidx.compose.remote.creation.compose.state.MutableRemoteInt import androidx.compose.remote.creation.compose.state.MutableRemoteState @@ -34,9 +32,9 @@ import androidx.compose.remote.creation.compose.state.RemoteDp import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteInt import androidx.compose.remote.creation.compose.state.RemoteState +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.isLiteral -import androidx.compose.runtime.MutableState // TODO fix up types after RemoteType refactor /** Update a value on click. */ @@ -45,35 +43,23 @@ public class ValueChangeAction( public val remoteValue: MutableRemoteState, public val updatedValue: RemoteState, ) : androidx.compose.remote.creation.compose.action.Action { - public override fun toRemoteAction(): Action { + public override fun RemoteStateScope.toRemoteAction(): Action { return if (remoteValue is MutableRemoteInt) { updatedValue as RemoteInt - val array = updatedValue.arrayForCreationState(FallbackCreationState.state) + val array = updatedValue.arrayForCreationState(creationState) if (array.isLiteral()) { - ValueIntegerChange( - remoteValue.getIdForCreationState(FallbackCreationState.state), - array[0].toInt(), - ) + ValueIntegerChange(remoteValue.id, array[0].toInt()) } else { // TODO validate why these are direct ids as a Long. - ValueIntegerExpressionChange( - remoteValue.getIdForCreationState(FallbackCreationState.state).toLong(), - updatedValue.getIdForCreationState(FallbackCreationState.state).toLong(), - ) + ValueIntegerExpressionChange(remoteValue.longId, updatedValue.longId) } } else if (remoteValue is MutableRemoteFloat) { updatedValue as RemoteFloat - ValueFloatExpressionChange( - remoteValue.getIdForCreationState(FallbackCreationState.state), - updatedValue.getIdForCreationState(FallbackCreationState.state), - ) + ValueFloatExpressionChange(remoteValue.id, updatedValue.id) } else if (remoteValue is RemoteString) { updatedValue as RemoteString - ValueStringChange( - remoteValue.getIdForCreationState(FallbackCreationState.state), - updatedValue.constantValue!!, - ) + ValueStringChange(remoteValue.id, updatedValue.constantValue!!) } else { TODO("println unsupported type in ValueChange $remoteValue") } @@ -82,11 +68,11 @@ public class ValueChangeAction( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ValueFloatChangeAction( - public val value: MutableState, + public val value: MutableRemoteFloat, public val updatedValue: Float, ) : androidx.compose.remote.creation.compose.action.Action { - public override fun toRemoteAction(): Action { - val id = Utils.idFromNan(value.value.internalAsFloat()) + public override fun RemoteStateScope.toRemoteAction(): Action { + val id = value.id return ValueFloatChange(id, updatedValue) } } @@ -94,8 +80,8 @@ public class ValueFloatChangeAction( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ValueFloatDpChangeAction(public val value: RemoteDp, public val updatedValue: Float) : androidx.compose.remote.creation.compose.action.Action { - public override fun toRemoteAction(): Action { - val id = Utils.idFromNan(value.value.internalAsFloat()) + public override fun RemoteStateScope.toRemoteAction(): Action { + val id = value.value.id return ValueFloatChange(id, updatedValue) } } @@ -104,14 +90,14 @@ public fun ValueChange( value: MutableRemoteFloat, updatedValue: Float, ): androidx.compose.remote.creation.compose.action.Action { - return ValueChangeAction(value, RemoteFloat(updatedValue)) + return ValueChangeAction(value, RemoteFloat(updatedValue)) } public fun ValueChange( value: MutableRemoteFloat, updatedValue: RemoteFloat, ): androidx.compose.remote.creation.compose.action.Action { - return ValueChangeAction(value, updatedValue) + return ValueChangeAction(value, updatedValue) } public fun ValueChange( @@ -129,14 +115,14 @@ public fun ValueChange( } public fun ValueChange(remoteState: MutableRemoteInt, updatedValue: Int): ValueChangeAction = - ValueChangeAction(remoteState, RemoteInt(v = updatedValue)) + ValueChangeAction(remoteState, RemoteInt(v = updatedValue)) public fun ValueChange( remoteState: MutableRemoteInt, updatedValue: RemoteInt, -): ValueChangeAction = ValueChangeAction(remoteState, updatedValue) +): ValueChangeAction = ValueChangeAction(remoteState, updatedValue) public fun ValueChange( remoteState: MutableRemoteString, updatedValue: String, -): ValueChangeAction = ValueChangeAction(remoteState, RemoteString(updatedValue)) +): ValueChangeAction = ValueChangeAction(remoteState, RemoteString(updatedValue)) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/Capture.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/Capture.kt index c0cfac802a386..0f6a289d1c35c 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/Capture.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/Capture.kt @@ -31,6 +31,7 @@ import androidx.compose.remote.creation.compose.state.AnimatedRemoteFloat import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteInt import androidx.compose.remote.creation.compose.state.RemoteState +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.profile.Profile import androidx.compose.remote.creation.profile.RcPlatformProfiles import androidx.compose.runtime.MutableState @@ -40,7 +41,10 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.ui.geometry.Size @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public open class RemoteComposeCreationState { +public open class RemoteComposeCreationState : RemoteStateScope { + + override val creationState: RemoteComposeCreationState + get() = this public val creationDisplayInfo: CreationDisplayInfo public val profile: Profile @@ -49,7 +53,7 @@ public open class RemoteComposeCreationState { public val expressionCache: MutableIntObjectMap = MutableIntObjectMap() public val intExpressionCache: MutableIntObjectMap = MutableIntObjectMap() public var ready: Boolean = true - public var document: RemoteComposeWriter + override lateinit var document: RemoteComposeWriter public val remoteVariableToId: MutableObjectIntMap> = MutableObjectIntMap() public val floatArrayCache: HashMap, FloatArray> = HashMap() public val longArrayCache: HashMap, LongArray> = HashMap() @@ -125,11 +129,11 @@ public open class RemoteComposeCreationState { public constructor( creationDisplayInfo: CreationDisplayInfo, profile: Profile, - document: RemoteComposeWriter, + writer: RemoteComposeWriter, ) { this.creationDisplayInfo = creationDisplayInfo this.profile = profile - this.document = document + this.document = writer } public constructor(size: Size, profile: Profile) { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RecordingCanvas.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RecordingCanvas.kt index b7326b43df9f6..824bec26ee44d 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RecordingCanvas.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RecordingCanvas.kt @@ -48,6 +48,7 @@ import androidx.compose.remote.creation.compose.state.RemoteColorFilter import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteInt import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.getFloatIdForCreationState import androidx.compose.remote.creation.compose.state.rf @@ -83,7 +84,7 @@ import androidx.compose.ui.graphics.toArgb * compose perspective (are paint objects reused this way?) */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public open class RecordingCanvas(bitmap: Bitmap) : Canvas(bitmap) { +public open class RecordingCanvas(bitmap: Bitmap) : Canvas(bitmap), RemoteStateScope { private var lastStyleOrdinal: Int = -1 private var typeface: Int = -1 @@ -101,8 +102,8 @@ public open class RecordingCanvas(bitmap: Bitmap) : Canvas(bitmap) { private var lastRemoteShader: RemoteShader? = null private var lastBlendMode: BlendMode? = null private var lastRemoteColorFilter: RemoteColorFilter? = null - public lateinit var document: RemoteComposeWriter - public lateinit var creationState: RemoteComposeCreationState + override lateinit var document: RemoteComposeWriter + override lateinit var creationState: RemoteComposeCreationState private var usingShaderMatrix: Boolean = false diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteComposeCapture.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteComposeCapture.kt index da760163b473c..d3b88930a56c8 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteComposeCapture.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteComposeCapture.kt @@ -30,7 +30,6 @@ import androidx.compose.remote.creation.CreationDisplayInfo import androidx.compose.remote.creation.RemoteComposeWriter import androidx.compose.remote.creation.compose.ExperimentalRemoteCreationComposeApi import androidx.compose.remote.creation.compose.layout.RemoteComposable -import androidx.compose.remote.creation.compose.state.FallbackCreationState import androidx.compose.remote.creation.profile.Profile import androidx.compose.remote.creation.profile.RcPlatformProfiles import androidx.compose.runtime.Composable @@ -265,7 +264,6 @@ public fun RemoteComposeExecution( ) } CompositionLocalProvider(LocalRemoteComposeCreationState provides remoteComposeCreationState) { - FallbackCreationState.state = remoteComposeCreationState captureComposeView.setRemoteComposeState(remoteComposeCreationState) content.invoke() } @@ -285,7 +283,6 @@ public fun RemoteComposeExecution( RemoteComposeCreationState(platform, size, apiLevel, profiles) } CompositionLocalProvider(LocalRemoteComposeCreationState provides remoteComposeCreationState) { - FallbackCreationState.state = remoteComposeCreationState captureComposeView.setRemoteComposeState(remoteComposeCreationState) content.invoke() } @@ -300,7 +297,6 @@ public fun RemoteComposeExecution( ) { val remoteComposeCreationState = remember { RemoteComposeCreationState(size, profile) } CompositionLocalProvider(LocalRemoteComposeCreationState provides remoteComposeCreationState) { - FallbackCreationState.state = remoteComposeCreationState captureComposeView.setRemoteComposeState(remoteComposeCreationState) content.invoke() } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteDrawScope0.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteDrawScope0.kt index fc311f543df4c..a019f2f551e67 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteDrawScope0.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteDrawScope0.kt @@ -24,6 +24,7 @@ import androidx.compose.remote.creation.compose.layout.RemoteOffset import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.shaders.RemoteBrush import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.geometry.CornerRadius @@ -130,7 +131,7 @@ public inline fun RemoteDrawScope0.rotate( degrees: Float, pivot: RemoteOffset = remoteCenter, block: RemoteDrawScope0.() -> Unit, -): Unit = withTransform({ rotate(degrees, pivot.asOffset()) }, block) +): Unit = withTransform({ rotate(degrees, pivot.asOffset(this@rotate)) }, block) /** * Add a rotation (in radians clockwise) to the current transform at the given pivot point. The @@ -164,7 +165,7 @@ public inline fun RemoteDrawScope0.scale( scaleY: Float, pivot: RemoteOffset = remoteCenter, block: RemoteDrawScope0.() -> Unit, -): Unit = withTransform({ scale(scaleX, scaleY, pivot.asOffset()) }, block) +): Unit = withTransform({ scale(scaleX, scaleY, pivot.asOffset(this@scale)) }, block) /** * Add an axis-aligned scale to the current transform, scaling both the horizontal direction and the @@ -180,7 +181,7 @@ public inline fun RemoteDrawScope0.scale( scale: Float, pivot: RemoteOffset = remoteCenter, block: RemoteDrawScope0.() -> Unit, -): Unit = withTransform({ scale(scale, scale, pivot.asOffset()) }, block) +): Unit = withTransform({ scale(scale, scale, pivot.asOffset(this@scale)) }, block) /** * Reduces the clip region to the intersection of the current clip and the given rectangle indicated @@ -280,7 +281,7 @@ public inline fun RemoteDrawScope0.withTransform( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @DrawScopeMarker public // @JvmDefaultWithCompatibility -interface RemoteDrawScope0 : Density { +interface RemoteDrawScope0 : Density, RemoteStateScope { public val canvas: RecordingCanvas get() = drawContext.canvas.nativeCanvas as RecordingCanvas diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteImageVector.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteImageVector.kt index 74c26ef55b5d3..ffa4ca1c1a88a 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteImageVector.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemoteImageVector.kt @@ -21,6 +21,7 @@ import android.graphics.Path import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.vector.RemotePathBuilder import androidx.compose.remote.creation.compose.vector.RemotePathData import androidx.compose.ui.graphics.BlendMode @@ -87,6 +88,7 @@ public class RemoteImageVector( @Suppress("MissingGetterMatchingBuilder") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class Builder( + scope: RemoteStateScope, /** Name of the vector asset */ private val name: String = DefaultGroupName, @@ -113,7 +115,7 @@ public class RemoteImageVector( * Determines if the vector asset should automatically be mirrored for right to left locales */ private val autoMirror: Boolean = false, - ) { + ) : RemoteStateScope by scope { private val nodes = ArrayList() @@ -521,7 +523,7 @@ public inline fun RemoteImageVector.Builder.path( pathBuilder: RemotePathBuilder.() -> Unit, ): RemoteImageVector.Builder = addPath( - RemotePathData(pathBuilder), + RemotePathData(this, pathBuilder), pathFillType, name, fill, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemotePathParser.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemotePathParser.kt index a96fc4ff238c6..16f33daafdae4 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemotePathParser.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/capture/RemotePathParser.kt @@ -17,8 +17,6 @@ package androidx.compose.remote.creation.compose.capture import androidx.compose.remote.creation.RemotePath -import androidx.compose.remote.creation.compose.layout.quadraticTo -import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.graphics.vector.PathNode import androidx.compose.ui.graphics.vector.PathNode.ArcTo import androidx.compose.ui.graphics.vector.PathNode.Close @@ -169,7 +167,7 @@ internal fun List.toRemotePath(target: RemotePath = RemotePath()): Rem currentY += node.dy2 } is QuadTo -> { - target.quadraticTo(node.x1.rf, node.y1.rf, node.x2.rf, node.y2.rf) + target.quadTo(node.x1, node.y1, node.x2, node.y2) ctrlX = node.x1 ctrlY = node.y1 currentX = node.x2 @@ -197,7 +195,7 @@ internal fun List.toRemotePath(target: RemotePath = RemotePath()): Rem reflectiveCtrlX = currentX reflectiveCtrlY = currentY } - target.quadraticTo(reflectiveCtrlX.rf, reflectiveCtrlY.rf, node.x.rf, node.y.rf) + target.quadTo(reflectiveCtrlX, reflectiveCtrlY, node.x, node.y) ctrlX = reflectiveCtrlX ctrlY = reflectiveCtrlY currentX = node.x diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/DrawHelpers.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/DrawHelpers.kt index a44bc43e3ee31..3199e4b0b1971 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/DrawHelpers.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/DrawHelpers.kt @@ -79,7 +79,7 @@ public fun StrokeJoin.toAndroidJoin(): android.graphics.Paint.Join = } /** Converts [ContentScale] to [ImageScaling]. */ -internal fun ContentScale.toRemoteCompose(): Int { +internal fun ContentScale.toImageScalingInt(): Int { return when (this) { ContentScale.Fit -> ImageScaling.SCALE_FIT ContentScale.Crop -> ImageScaling.SCALE_CROP diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/FitBox.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/FitBox.kt index f2a45a949c5a1..86802739dc0a9 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/FitBox.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/FitBox.kt @@ -20,6 +20,7 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.runtime.Composable import androidx.compose.ui.draw.DrawModifier import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -34,9 +35,9 @@ public class RemoteComposeFitBoxModifier( override fun ContentDrawScope.draw() { drawIntoRemoteCanvas { canvas -> canvas.document.startFitBox( - modifier.toRemoteCompose(), - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + canvas.toRecordingModifier(modifier), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) this@draw.drawContent() canvas.document.endFitBox() diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteAlignment.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteAlignment.kt index 587c47cdb7974..a8d6108d7cf04 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteAlignment.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteAlignment.kt @@ -39,7 +39,7 @@ public object RemoteAlignment { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toComposeUi(): androidx.compose.ui.Alignment.Horizontal - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemoteCompose(): Int + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemote(): Int } /** @@ -54,7 +54,7 @@ public object RemoteAlignment { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toComposeUi(): androidx.compose.ui.Alignment.Vertical - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemoteCompose(): Int + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemote(): Int } /** @@ -99,7 +99,7 @@ public data class RemoteHorizontalAlignment(var type: Int) : RemoteAlignment.Hor return androidx.compose.ui.Alignment.Start } - override fun toRemoteCompose(): Int { + override fun toRemote(): Int { when (type) { 0 -> return ColumnLayout.START 1 -> return ColumnLayout.CENTER @@ -120,7 +120,7 @@ public data class RemoteVerticalAlignment(var type: Int) : RemoteAlignment.Verti return androidx.compose.ui.Alignment.Top } - override fun toRemoteCompose(): Int { + override fun toRemote(): Int { when (type) { 3 -> return ColumnLayout.TOP 4 -> return ColumnLayout.CENTER diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteArrangement.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteArrangement.kt index 10e274b8b45ec..feea0e68dee44 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteArrangement.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteArrangement.kt @@ -32,7 +32,7 @@ public object RemoteArrangement { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toComposeUi(): androidx.compose.foundation.layout.Arrangement.Horizontal - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemoteCompose(): Int + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemote(): Int } /** A contract for laying out children vertically. */ @@ -40,7 +40,7 @@ public object RemoteArrangement { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toComposeUi(): androidx.compose.foundation.layout.Arrangement.Vertical - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemoteCompose(): Int + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemote(): Int } /** A contract for laying out children horizontally or vertically. */ @@ -49,7 +49,7 @@ public object RemoteArrangement { override fun toComposeUi(): androidx.compose.foundation.layout.Arrangement.HorizontalOrVertical - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun toRemoteCompose(): Int + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) override fun toRemote(): Int } /** @@ -98,7 +98,7 @@ public data class VerticalArrangement(var type: Int) : RemoteArrangement.Vertica return androidx.compose.foundation.layout.Arrangement.Top } - override fun toRemoteCompose(): Int { + override fun toRemote(): Int { when (type) { 0 -> return ColumnLayout.TOP 1 -> return ColumnLayout.CENTER @@ -121,7 +121,7 @@ public data class HorizontalOrVerticalArrangement(var type: Int) : return androidx.compose.foundation.layout.Arrangement.spacedBy(0.dp) } - override fun toRemoteCompose(): Int { + override fun toRemote(): Int { when (type) { 6 -> return ColumnLayout.SPACE_BETWEEN 7 -> return ColumnLayout.SPACE_EVENLY @@ -145,7 +145,7 @@ public data class HorizontalArrangement(var type: Int) : RemoteArrangement.Horiz return androidx.compose.foundation.layout.Arrangement.Start } - override fun toRemoteCompose(): Int { + override fun toRemote(): Int { when (type) { 3 -> return ColumnLayout.START 4 -> return ColumnLayout.CENTER diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteBox.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteBox.kt index eee2b9fb7ea43..6409c0c4b21e2 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteBox.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteBox.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.v2.RemoteBoxV2 import androidx.compose.remote.creation.compose.v2.RemoteComposeApplierV2 import androidx.compose.runtime.Composable @@ -39,9 +40,9 @@ public class RemoteComposeBoxModifier( override fun ContentDrawScope.draw() { drawIntoRemoteCanvas { canvas -> canvas.document.startBox( - modifier.toRemoteCompose(), - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + canvas.toRecordingModifier(modifier), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) this@draw.drawContent() canvas.document.endBox() diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas.kt index f07ac1fe78c58..b966ccc43d73f 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas.kt @@ -21,11 +21,11 @@ import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.DrawTextOnCircle import androidx.compose.remote.creation.RemotePath import androidx.compose.remote.creation.compose.capture.RecordingCanvas -import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.creation.compose.state.RemoteBitmap import androidx.compose.remote.creation.compose.state.RemoteBoolean import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Matrix @@ -38,10 +38,7 @@ import androidx.compose.ui.graphics.Matrix public class RemoteCanvas( /** The underlying [RecordingCanvas] being wrapped. */ public val internalCanvas: RecordingCanvas -) { - /** The [RemoteComposeCreationState] associated with the document being drawn into. */ - public val creationState: RemoteComposeCreationState - get() = internalCanvas.creationState +) : RemoteStateScope by internalCanvas { /** Saves the current canvas state. */ public fun save() { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas0.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas0.kt index 2a8c3eed7d6fd..adf253c7a9c5e 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas0.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvas0.kt @@ -30,10 +30,11 @@ import androidx.compose.remote.creation.compose.capture.RemoteDrawScope0.Compani import androidx.compose.remote.creation.compose.capture.withTransform import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.shaders.RemoteBrush import androidx.compose.remote.creation.compose.shaders.RemoteSolidColor -import androidx.compose.remote.creation.compose.state.FallbackCreationState import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.rf import androidx.compose.runtime.Composable @@ -71,7 +72,7 @@ public fun RemoteCanvas0( @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 Spacer( modifier = - RemoteComposeCanvasModifier(modifier.toRemoteCompose()) + RemoteComposeCanvasModifier(captureMode.toRecordingModifier(modifier)) .drawBehind { RemoteCanvasDrawScope0(captureMode, drawScope = this).content() } .then(modifier.toComposeUiLayout()) ) @@ -97,19 +98,23 @@ public fun RemoteCanvasDrawScope0.clipRect( clipOp: ClipOp = ClipOp.Intersect, block: RemoteCanvasDrawScope0.() -> Unit, ) { - withTransform({ clipRect(left.id, top.id, right.id, bottom.id, clipOp) }) { + withTransform({ + with(this@clipRect) { + this@withTransform.clipRect( + left.floatId, + top.floatId, + right.floatId, + bottom.floatId, + clipOp, + ) + } + }) { this@clipRect.block() } } -public fun DrawTransform.translate(x: RemoteFloat, y: RemoteFloat) { - val ix: Float = - if (x is RemoteFloat) x.getFloatIdForCreationState(FallbackCreationState.state) - else x.toFloat() - val iy: Float = - if (y is RemoteFloat) y.getFloatIdForCreationState(FallbackCreationState.state) - else y.toFloat() - this.translate(ix, iy) +public fun RemoteStateScope.translate(transform: DrawTransform, x: RemoteFloat, y: RemoteFloat) { + transform.translate(x.floatId, y.floatId) } public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( @@ -127,7 +132,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( val blendMode: BlendMode = DefaultBlendMode val size = RemoteSize(remote.component.width, remote.component.height) - val paint = toPaint(brush, drawStyle, alpha.id, colorFilter, blendMode, size = size) + val paint = toPaint(brush, drawStyle, alpha, colorFilter, blendMode, size = size) val ap = paint.asFrameworkPaint() @@ -136,7 +141,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( } else { ap.setTypeface(Typeface.DEFAULT) } - ap.textSize = textSize.id + ap.textSize = textSize.floatId canvas.drawAnchoredText( text.toString(), anchorX = anchor.x, @@ -163,7 +168,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( val blendMode: BlendMode = DefaultBlendMode val size = RemoteSize(remote.component.width, remote.component.height) - val paint = toPaint(brush, drawStyle, alpha.id, colorFilter, blendMode, size = size) + val paint = toPaint(brush, drawStyle, alpha, colorFilter, blendMode, size = size) val ap = paint.asFrameworkPaint() @@ -172,7 +177,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( } else { ap.setTypeface(Typeface.DEFAULT) } - ap.textSize = textSize.id + ap.textSize = textSize.floatId canvas.drawAnchoredText( text, anchorX = anchor.x, @@ -198,7 +203,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( val colorFilter: ColorFilter? = null val blendMode: BlendMode = DefaultBlendMode - val paint = configurePaint(color, drawStyle, alpha.id, colorFilter, blendMode) + val paint = configurePaint(color, drawStyle, alpha.floatId, colorFilter, blendMode) val ap = paint.asFrameworkPaint() if (typeface != null) { @@ -206,7 +211,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( } else { ap.setTypeface(Typeface.DEFAULT) } - ap.textSize = textSize.id + ap.textSize = textSize.floatId canvas.drawAnchoredText( text.toString(), anchorX = anchor.x, @@ -232,7 +237,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( val colorFilter: ColorFilter? = null val blendMode: BlendMode = DefaultBlendMode - val paint = configurePaint(color, drawStyle, alpha.id, colorFilter, blendMode) + val paint = configurePaint(color, drawStyle, alpha.floatId, colorFilter, blendMode) val ap = paint.asFrameworkPaint() if (typeface != null) { @@ -240,7 +245,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawAnchoredText( } else { ap.setTypeface(Typeface.DEFAULT) } - ap.textSize = textSize.id + ap.textSize = textSize.floatId canvas.drawAnchoredText( text, anchorX = anchor.x, @@ -404,7 +409,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawRect( blendMode: BlendMode = DrawScope.DefaultBlendMode, ) { val size = RemoteSize(remote.component.width, remote.component.height) - val paint = toPaint(brush, style, alpha, colorFilter, blendMode, size = size) + val paint = toPaint(brush, style, alpha.rf, colorFilter, blendMode, size = size) canvas.drawRect( left = left, top = top, @@ -450,7 +455,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawRoundRect( blendMode: BlendMode, ) { val size = RemoteSize(remote.component.width, remote.component.height) - val paint = toPaint(brush, style, alpha, colorFilter, blendMode, size = size) + val paint = toPaint(brush, style, alpha.rf, colorFilter, blendMode, size = size) canvas.drawRoundRect( left, top, @@ -534,7 +539,7 @@ public fun RemoteCanvasDrawScope0.remoteDrawOval( blendMode: BlendMode, ) { val size = RemoteSize(remote.component.width, remote.component.height) - val paint = toPaint(brush, style, alpha, colorFilter, blendMode, size = size) + val paint = toPaint(brush, style, alpha.rf, colorFilter, blendMode, size = size) canvas.drawOval( left = left, top = top, @@ -601,10 +606,10 @@ internal fun toPaint( if (this.filterQuality != filterQuality) this.filterQuality = filterQuality } -internal fun toPaint( +internal fun RemoteStateScope.toPaint( brush: RemoteBrush, drawStyle: DrawStyle, - alpha: Float, + alpha: RemoteFloat, colorFilter: ColorFilter?, blendMode: BlendMode, filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, @@ -627,12 +632,12 @@ internal fun toPaint( } val shader = if (brush.hasShader) { - brush.createShader(size = size) + with(brush) { this@toPaint.createShader(size = size) } } else { null } if (this.shader != shader) this.shader = shader - this.alpha = alpha + this.alpha = alpha.floatId when (brush) { is RemoteSolidColor -> { val constantValue = brush.color.constantValue diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasComposable.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasComposable.kt index c8696722f483e..da5bbb8ff850b 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasComposable.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasComposable.kt @@ -18,9 +18,11 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.foundation.layout.Spacer +import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.capture.RecordingCanvas import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.creation.compose.v2.RemoteCanvasV2 import androidx.compose.remote.creation.compose.v2.RemoteComposeApplierV2 @@ -49,10 +51,11 @@ public fun RemoteCanvas( RemoteCanvasV2(modifier, content) return } + val creationState = LocalRemoteComposeCreationState.current @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 Spacer( modifier = - RemoteComposeCanvasModifier(modifier.toRemoteCompose()) + RemoteComposeCanvasModifier(creationState.toRecordingModifier(modifier)) .drawBehind { RemoteDrawScope( remoteCanvas = diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasDrawScope0.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasDrawScope0.kt index 9ead9f0a34abc..353a11206a3b7 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasDrawScope0.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCanvasDrawScope0.kt @@ -36,6 +36,7 @@ import androidx.compose.remote.creation.compose.capture.RemoteDrawScope0 import androidx.compose.remote.creation.compose.shaders.RemoteBrush import androidx.compose.remote.creation.compose.state.AnimatedRemoteFloat import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.geometry.CornerRadius @@ -80,7 +81,10 @@ public open class RemoteCanvasDrawScope0( override val fontScale: Float = drawScope.fontScale, override val drawContext: DrawContext = drawScope.drawContext, override val layoutDirection: LayoutDirection = drawScope.layoutDirection, -) : RemoteDrawScope0 { +) : RemoteDrawScope0, RemoteStateScope { + + override val creationState: RemoteComposeCreationState + get() = remoteComposeCreationState @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class RemoteAccess( @@ -246,8 +250,8 @@ public open class RemoteCanvasDrawScope0( ) { drawScope.drawLine( brush, - start.asOffset(), - end.asOffset(), + start.asOffset(this), + end.asOffset(this), strokeWidth, cap, pathEffect, @@ -270,8 +274,8 @@ public open class RemoteCanvasDrawScope0( ) { drawScope.drawLine( color, - start.asOffset(), - end.asOffset(), + start.asOffset(this), + end.asOffset(this), strokeWidth, cap, pathEffect, @@ -294,10 +298,10 @@ public open class RemoteCanvasDrawScope0( val bottom = ofAdd(topLeft.y, size.height) remoteDrawRect( RemoteBrush.fromComposeUi(brush), - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, alpha, style, colorFilter, @@ -319,11 +323,11 @@ public open class RemoteCanvasDrawScope0( remoteDrawRect( brush, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, - alpha.toFloat(), + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, + alpha.floatId, style, colorFilter, blendMode, @@ -353,10 +357,10 @@ public open class RemoteCanvasDrawScope0( val bottom = ofAdd(topLeft.y, size.height) remoteDrawRect( color, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, alpha, style, colorFilter, @@ -385,16 +389,16 @@ public open class RemoteCanvasDrawScope0( remoteDrawScaledBitmap( image.asAndroidBitmap(), - srcOffset.x.id, - srcOffset.y.id, - srcR.id, - srcB.id, - dstOffset.x.id, - dstOffset.y.id, - dstR.id, - dstB.id, + srcOffset.x.floatId, + srcOffset.y.floatId, + srcR.floatId, + srcB.floatId, + dstOffset.x.floatId, + dstOffset.y.floatId, + dstR.floatId, + dstB.floatId, scaleType, - scaleFactor.id, + scaleFactor.floatId, description, ) } @@ -407,7 +411,7 @@ public open class RemoteCanvasDrawScope0( colorFilter: ColorFilter?, blendMode: BlendMode, ) { - drawScope.drawImage(image, topLeft.asOffset(), alpha, style, colorFilter, blendMode) + drawScope.drawImage(image, topLeft.asOffset(this), alpha, style, colorFilter, blendMode) } @Deprecated( @@ -485,10 +489,10 @@ public open class RemoteCanvasDrawScope0( remoteDrawRoundRect( brush, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, cornerRadius, alpha, style, @@ -513,12 +517,12 @@ public open class RemoteCanvasDrawScope0( remoteDrawRoundRect( brush, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, cornerRadius, - alpha.toFloat(), + alpha.floatId, style, colorFilter, blendMode, @@ -541,10 +545,10 @@ public open class RemoteCanvasDrawScope0( remoteDrawRoundRect( RemoteBrush.fromComposeUi(brush), - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, cornerRadius, alpha, style, @@ -566,7 +570,7 @@ public open class RemoteCanvasDrawScope0( drawScope.drawText( textLayoutResult = textLayoutResult, color = color, - topLeft = topLeft.asOffset(), + topLeft = topLeft.asOffset(this), alpha = alpha, shadow = shadow, textDecoration = textDecoration, @@ -588,7 +592,7 @@ public open class RemoteCanvasDrawScope0( drawScope.drawText( textLayoutResult = textLayoutResult, brush = brush, - topLeft = topLeft.asOffset(), + topLeft = topLeft.asOffset(this), alpha = alpha, shadow = shadow, textDecoration = textDecoration, @@ -611,7 +615,7 @@ public open class RemoteCanvasDrawScope0( drawScope.drawText( textMeasurer = textMeasurer, text = text, - topLeft = topLeft.asOffset(), + topLeft = topLeft.asOffset(this), style = style, overflow = overflow, softWrap = softWrap, @@ -636,7 +640,7 @@ public open class RemoteCanvasDrawScope0( drawScope.drawText( textMeasurer = textMeasurer, text = text, - topLeft = topLeft.asOffset(), + topLeft = topLeft.asOffset(this), style = style, overflow = overflow, softWrap = softWrap, @@ -662,10 +666,10 @@ public open class RemoteCanvasDrawScope0( remoteDrawRoundRect( color, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, cornerRadius, style, alpha, @@ -684,10 +688,10 @@ public open class RemoteCanvasDrawScope0( blendMode: BlendMode, ) { canvas.drawCircle( - center.x.id, - center.y.id, - radius.id, - toPaint(color, style, alpha.id, colorFilter, blendMode).asFrameworkPaint(), + center.x.floatId, + center.y.floatId, + radius.floatId, + toPaint(color, style, alpha.floatId, colorFilter, blendMode).asFrameworkPaint(), ) } @@ -705,10 +709,10 @@ public open class RemoteCanvasDrawScope0( remoteDrawOval( RemoteBrush.fromComposeUi(brush), - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, alpha, style, colorFilter, @@ -730,10 +734,10 @@ public open class RemoteCanvasDrawScope0( remoteDrawOval( color, - topLeft.x.id, - topLeft.y.id, - right.id, - bottom.id, + topLeft.x.floatId, + topLeft.y.floatId, + right.floatId, + bottom.floatId, alpha, style, colorFilter, @@ -758,8 +762,8 @@ public open class RemoteCanvasDrawScope0( startAngle, sweepAngle, useCenter, - topLeft.asOffset(), - size.asSize(), + topLeft.asOffset(this), + size.asSize(this), alpha, style, colorFilter, @@ -784,8 +788,8 @@ public open class RemoteCanvasDrawScope0( startAngle, sweepAngle, useCenter, - topLeft.asOffset(), - size.asSize(), + topLeft.asOffset(this), + size.asSize(this), alpha, style, colorFilter, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleColumn.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleColumn.kt index 7cd7d7667e4e2..b71408be1b084 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleColumn.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleColumn.kt @@ -20,10 +20,12 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.layout.managers.CollapsiblePriority import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifierOperation.Type +import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.modifier.CollapsiblePriorityModifier import androidx.compose.remote.creation.compose.modifier.HeightModifier import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @@ -42,8 +44,8 @@ public class RemoteComposeCollapsibleColumnModifier( drawIntoRemoteCanvas { canvas -> canvas.document.startCollapsibleColumn( modifier, - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) this@draw.drawContent() canvas.document.endCollapsibleColumn() @@ -77,11 +79,12 @@ public fun RemoteCollapsibleColumn( content: @Composable RemoteCollapsibleColumnScope.() -> Unit, ) { + val creationState = LocalRemoteComposeCreationState.current val scope = remember { RemoteCollapsibleColumnScope() } val composeModifiers = RemoteComposeCollapsibleColumnModifier( - modifier.toRemoteCompose(), + creationState.toRecordingModifier(modifier), horizontalAlignment, verticalArrangement, ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleRow.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleRow.kt index b38fb0b5db694..63792ccb1a8ed 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleRow.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteCollapsibleRow.kt @@ -20,10 +20,12 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.layout.managers.CollapsiblePriority import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifierOperation.Type +import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.modifier.CollapsiblePriorityModifier import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.WidthModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @@ -42,8 +44,8 @@ public class RemoteComposeCollapsibleRowModifier( drawIntoRemoteCanvas { canvas -> canvas.document.startCollapsibleRow( modifier, - horizontalArrangement.toRemoteCompose(), - verticalAlignment.toRemoteCompose(), + horizontalArrangement.toRemote(), + verticalAlignment.toRemote(), ) this@draw.drawContent() canvas.document.endCollapsibleRow() @@ -79,9 +81,10 @@ public fun RemoteCollapsibleRow( val scope = remember { RemoteCollapsibleRowScope() } + val creationState = LocalRemoteComposeCreationState.current val composeModifiers = RemoteComposeCollapsibleRowModifier( - modifier.toRemoteCompose(), + creationState.toRecordingModifier(modifier), horizontalArrangement, verticalAlignment, ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteColumn.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteColumn.kt index fc001624f80b0..8279fa9ef13f2 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteColumn.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteColumn.kt @@ -22,6 +22,7 @@ import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifie import androidx.compose.remote.creation.compose.modifier.HeightModifier import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.v2.RemoteColumnV2 import androidx.compose.remote.creation.compose.v2.RemoteComposeApplierV2 @@ -41,9 +42,9 @@ public class RemoteComposeColumnModifier( override fun ContentDrawScope.draw() { drawIntoRemoteCanvas { canvas -> canvas.document.startColumn( - modifier.toRemoteCompose(), - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + canvas.toRecordingModifier(modifier), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) this@draw.drawContent() canvas.document.endColumn() diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteDrawScope.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteDrawScope.kt index 5ddd536c03898..be55a2eeae3eb 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteDrawScope.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteDrawScope.kt @@ -24,6 +24,7 @@ import androidx.compose.remote.creation.compose.state.RemoteBitmap import androidx.compose.remote.creation.compose.state.RemoteBoolean import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.graphics.ClipOp @@ -42,7 +43,7 @@ public open class RemoteDrawScope( public val remoteCanvas: RemoteCanvas, public val fontScale: RemoteFloat, public val layoutDirection: LayoutDirection, -) { +) : RemoteStateScope by remoteCanvas { public val remoteComposeCreationState: RemoteComposeCreationState get() = remoteCanvas.creationState @@ -194,7 +195,7 @@ public open class RemoteDrawScope( dstOffset.y, dstOffset.x + dstSize.width, dstOffset.y + dstSize.height, - scaleType.toRemoteCompose(), + scaleType.toImageScalingInt(), scaleFactor, contentDescription, ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteFloatContext.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteFloatContext.kt index 4e6b2c62c77d8..e2594c90710db 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteFloatContext.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteFloatContext.kt @@ -22,10 +22,11 @@ import androidx.compose.remote.core.operations.utilities.AnimatedFloatExpression import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteFloatExpression +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.remoteFloat @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class RemoteFloatContext(public val state: RemoteComposeCreationState) { +public class RemoteFloatContext(public val state: RemoteStateScope) { public fun componentWidth(): RemoteFloat { val doc = state.document val value = doc.addComponentWidthValue() diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteOffset.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteOffset.kt index a9e900ee0ba45..3b4177e842397 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteOffset.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteOffset.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.geometry.Offset @@ -46,8 +47,10 @@ public class RemoteOffset { public val minDimension: RemoteFloat get() = x.min(y) - public fun asOffset(): Offset { - return Offset(x.internalAsFloat(), y.internalAsFloat()) + public fun asOffset(scope: RemoteStateScope): Offset { + with(scope) { + return Offset(x.floatId, y.floatId) + } } public companion object { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemotePath.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemotePath.kt deleted file mode 100644 index 6c3f6753c2f72..0000000000000 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemotePath.kt +++ /dev/null @@ -1,96 +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. - */ -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - -package androidx.compose.remote.creation.compose.layout - -import androidx.annotation.RestrictTo -import androidx.compose.remote.creation.RemotePath -import androidx.compose.remote.creation.compose.state.RemoteFloat -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Path - -public fun Path.moveTo(x: RemoteFloat, y: RemoteFloat) { - this.moveTo(x.id, y.id) -} - -public fun Path.lineTo(x: RemoteFloat, y: RemoteFloat) { - this.lineTo(x.id, y.id) -} - -public fun Path.addArc( - left: RemoteFloat, - top: RemoteFloat, - right: RemoteFloat, - bottom: RemoteFloat, - startAngle: RemoteFloat, - sweepAngle: RemoteFloat, -) { - this.addArc(Rect(left.id, top.id, right.id, bottom.id), startAngle.id, sweepAngle.id) -} - -public fun Path.quadraticTo(x1: RemoteFloat, y1: RemoteFloat, x2: RemoteFloat, y2: RemoteFloat) { - this.quadraticTo(x1.id, y1.id, x2.id, y2.id) -} - -public fun Path.cubicTo( - x1: RemoteFloat, - y1: RemoteFloat, - x2: RemoteFloat, - y2: RemoteFloat, - x3: RemoteFloat, - y3: RemoteFloat, -) { - this.cubicTo(x1.id, y1.id, x2.id, y2.id, x3.id, y3.id) -} - -public fun RemotePath.moveTo(x: RemoteFloat, y: RemoteFloat) { - this.moveTo(x.id, y.id) -} - -public fun RemotePath.lineTo(x: RemoteFloat, y: RemoteFloat) { - this.lineTo(x.id, y.id) -} - -public fun RemotePath.quadraticTo( - x1: RemoteFloat, - y1: RemoteFloat, - x2: RemoteFloat, - y2: RemoteFloat, -) { - this.quadTo(x1.id, y1.id, x2.id, y2.id) -} - -public fun RemotePath.cubicTo( - x1: RemoteFloat, - y1: RemoteFloat, - x2: RemoteFloat, - y2: RemoteFloat, - x3: RemoteFloat, - y3: RemoteFloat, -) { - this.cubicTo(x1.id, y1.id, x2.id, y2.id, x3.id, y3.id) -} - -public fun RemotePath.conicTo( - x1: RemoteFloat, - y1: RemoteFloat, - x2: RemoteFloat, - y2: RemoteFloat, - weight: RemoteFloat, -) { - this.conicTo(x1.id, y1.id, x2.id, y2.id, weight.id) -} diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteRow.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteRow.kt index e4642970daaff..d4d295b9a8555 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteRow.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteRow.kt @@ -19,9 +19,11 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifierOperation.Type +import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.WidthModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.v2.RemoteComposeApplierV2 import androidx.compose.remote.creation.compose.v2.RemoteRowV2 @@ -43,8 +45,8 @@ public class RemoteComposeRowModifier( drawIntoRemoteCanvas { canvas -> canvas.document.startRow( modifier, - horizontalArrangement.toRemoteCompose(), - verticalAlignment.toRemoteCompose(), + horizontalArrangement.toRemote(), + verticalAlignment.toRemote(), ) this@draw.drawContent() canvas.document.endRow() @@ -83,10 +85,11 @@ public fun RemoteRow( return } + val creationState = LocalRemoteComposeCreationState.current val scope = remember { RemoteRowScope() } val composeModifiers = RemoteComposeRowModifier( - modifier.toRemoteCompose(), + creationState.toRecordingModifier(modifier), horizontalArrangement, verticalAlignment, ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteSize.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteSize.kt index 25bdbe0fe2258..111c917cbb820 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteSize.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteSize.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.layout import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.geometry.Size @@ -45,8 +46,10 @@ public class RemoteSize { public val minDimension: RemoteFloat get() = width.min(height) - public fun asSize(): Size { - return Size(width.internalAsFloat(), height.internalAsFloat()) + public fun asSize(scope: RemoteStateScope): Size { + with(scope) { + return Size(width.floatId, height.floatId) + } } public val center: RemoteOffset diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteStringList.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteStringList.kt index f2fa5cdd66692..83d5f573234d9 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteStringList.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteStringList.kt @@ -37,7 +37,7 @@ public class RemoteStringList(public var listId: Float) { public operator fun get(value: RemoteInt): RemoteIntReference { val state = LocalRemoteComposeCreationState.current - val valueId = value.id.toInt() + val valueId = with(state) { value.id } return RemoteIntReference(state.document.textLookup(listId, valueId)) } @@ -45,7 +45,7 @@ public class RemoteStringList(public var listId: Float) { public operator fun get(value: Int): RemoteIntReference { val state = LocalRemoteComposeCreationState.current - val index = rememberRemoteIntValue { value }.id.toInt() + val index = with(state) { rememberRemoteIntValue { value }.id } return RemoteIntReference(state.document.textLookup(listId, index)) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteText.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteText.kt index 20adb45bb5b85..95d87680cbdf4 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteText.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/RemoteText.kt @@ -24,13 +24,14 @@ import androidx.compose.remote.core.operations.layout.managers.TextLayout import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.MutableRemoteString import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteIntReference import androidx.compose.remote.creation.compose.state.RemoteString -import androidx.compose.remote.creation.compose.state.rememberRemoteString import androidx.compose.remote.creation.compose.state.rf +import androidx.compose.remote.creation.compose.state.rs import androidx.compose.remote.creation.compose.v2.RemoteComposeApplierV2 import androidx.compose.remote.creation.compose.v2.RemoteTextV2 import androidx.compose.remote.creation.modifiers.RecordingModifier @@ -69,9 +70,8 @@ public fun RemoteText( maxLines: Int = Int.MAX_VALUE, style: TextStyle = LocalTextStyle.current, ) { - val remoteText = rememberRemoteString { text } RemoteText( - remoteText, + text.rs, modifier, color, fontSize, @@ -223,7 +223,7 @@ public fun RemoteText( if (useCoreTextComponent) { androidx.compose.foundation.layout.Box( RemoteComposeCoreTextComponentModifier( - modifier = modifier.toRemoteCompose(), + modifier = captureMode.toRecordingModifier(modifier), id = text, color = color, fontSize = fontSize, @@ -246,12 +246,12 @@ public fun RemoteText( } else { androidx.compose.foundation.layout.Box( RemoteComposeTextComponentModifier( - modifier = modifier.toRemoteCompose(), + modifier = captureMode.toRecordingModifier(modifier), id = RemoteIntReference(text.getIdForCreationState(captureMode)), color = color.constantValue?.toArgb() ?: color.getIdForCreationState(captureMode), isColorConstant = color.hasConstantValue, - fontSize = fontSize.id, + fontSize = with(LocalRemoteComposeCreationState.current) { fontSize.floatId }, fontStyle = fontStyle.encode(), fontWeight = fontWeight.constantValue ?: 400f, fontFamily = fontFamily, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/StateLayout.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/StateLayout.kt index 8733b0eed7730..322719c242255 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/StateLayout.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/layout/StateLayout.kt @@ -22,6 +22,7 @@ import androidx.collection.MutableObjectIntMap import androidx.compose.foundation.layout.Box import androidx.compose.remote.creation.compose.modifier.RemoteModifier import androidx.compose.remote.creation.compose.modifier.toComposeUiLayout +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteInt import androidx.compose.remote.creation.compose.state.rememberRemoteIntValue import androidx.compose.runtime.Composable @@ -32,8 +33,8 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class StateMachineSpec(public val currentState: RemoteInt, public var states: IntArray) { - public val statesNames: MutableObjectIntMap = MutableObjectIntMap() - public val values: HashMap = HashMap() + public val statesNames: MutableObjectIntMap = MutableObjectIntMap() + public val values: HashMap = HashMap() public operator fun component1(): Int { return states[0] @@ -137,7 +138,10 @@ public class RemoteComposeStateLayoutModifier( ) : DrawModifier { override fun ContentDrawScope.draw() { drawIntoRemoteCanvas { canvas -> - canvas.document.startStateLayout(modifier.toRemoteCompose(), currentState.getIntId()) + canvas.document.startStateLayout( + canvas.toRecordingModifier(modifier), + with(canvas) { currentState.id }, + ) this@draw.drawContent() canvas.document.endStateLayout() } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AlignByBaselineModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AlignByBaselineModifier.kt index a3a14324c0503..e55e853f5b706 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AlignByBaselineModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AlignByBaselineModifier.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.core.RemoteContext.FIRST_BASELINE +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.AlignByModifier import androidx.compose.remote.creation.modifiers.RecordingModifier @@ -33,7 +34,7 @@ import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class AlignByBaselineModifier() : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return AlignByModifier(FIRST_BASELINE) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AnimateSpecModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AnimateSpecModifier.kt index a1beab6ea62d9..2d781a706c225 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AnimateSpecModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/AnimateSpecModifier.kt @@ -20,6 +20,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.layout.animation.AnimationSpec.ANIMATION import androidx.compose.remote.core.operations.utilities.easing.GeneralEasing +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @@ -33,7 +34,7 @@ public class AnimateSpecModifier( public val enterAnimation: ANIMATION, public val exitAnimation: ANIMATION, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.AnimateSpecModifier( animationId, motionDuration, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BackgroundModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BackgroundModifier.kt index f0318e29d82bb..6f9387264301a 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BackgroundModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BackgroundModifier.kt @@ -23,6 +23,7 @@ import androidx.compose.remote.creation.compose.painter.painterRemoteColor import androidx.compose.remote.creation.compose.shaders.RemoteBrush import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rc import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.SolidBackgroundModifier @@ -31,8 +32,13 @@ import androidx.compose.ui.graphics.Color @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public data class BackgroundModifier(val color: RemoteColor) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return SolidBackgroundModifier(color.red.id, color.green.id, color.blue.id, color.alpha.id) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return SolidBackgroundModifier( + color.red.floatId, + color.green.floatId, + color.blue.floatId, + color.alpha.floatId, + ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BorderModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BorderModifier.kt index 0947a2fd35cff..a2c8d2a4cadb1 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BorderModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/BorderModifier.kt @@ -18,29 +18,28 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo +import androidx.compose.remote.core.operations.layout.modifiers.ShapeType +import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteDp import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope +import androidx.compose.remote.creation.modifiers.BorderModifier as CreationBorderModifier import androidx.compose.remote.creation.modifiers.RecordingModifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class BorderModifier(public val width: RemoteFloat, public val color: Color) : +public class BorderModifier(public val width: RemoteFloat, public val color: RemoteColor) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return androidx.compose.remote.creation.modifiers.BorderModifier( - width.internalAsFloat(), + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + // TODO use addModifierDynamicBorder + return CreationBorderModifier( + width.floatId, 0f, - color.toArgb(), - 0, + color.constantValue!!.toArgb(), + ShapeType.RECTANGLE, ) } } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public fun RemoteModifier.border(width: RemoteFloat, color: Color): RemoteModifier = - then(BorderModifier(width, color)) - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public fun RemoteModifier.border(width: RemoteDp, color: Color): RemoteModifier = - border(width.toPx(), color) +public fun RemoteModifier.border(width: RemoteDp, color: RemoteColor): RemoteModifier = + then(BorderModifier(width.toPx(), color)) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClickActionModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClickActionModifier.kt index b264ed16f5e7c..045bd88af91ea 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClickActionModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClickActionModifier.kt @@ -19,14 +19,15 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.action.Action +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.ui.semantics.Role @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ClickActionModifier(public val actions: List) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.ClickActionModifier( - @Suppress("ListIterator") actions.map { it.toRemoteAction() } + @Suppress("ListIterator") actions.map { action -> with(action) { toRemoteAction() } } ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClipModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClipModifier.kt index cf649bcf6666a..5982924a0d7a9 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClipModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ClipModifier.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.RoundedRectShape import androidx.compose.remote.creation.modifiers.UnsupportedModifier @@ -35,7 +36,7 @@ public class ClipModifier( public val size: DpSize, public val density: Density, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { val remoteShape = remoteShape() return if (remoteShape == null) { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/CollapsiblePriorityModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/CollapsiblePriorityModifier.kt index 19faf63065b43..f19ad00ff4fae 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/CollapsiblePriorityModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/CollapsiblePriorityModifier.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -27,10 +28,10 @@ public class CollapsiblePriorityModifier( public val priority: RemoteFloat, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.CollapsiblePriorityModifier( orientation, - priority.internalAsFloat(), + priority.floatId, ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/DrawWithContentModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/DrawWithContentModifier.kt index 659440aca6397..50dd02acbd8b2 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/DrawWithContentModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/DrawWithContentModifier.kt @@ -22,6 +22,7 @@ import androidx.compose.remote.creation.compose.capture.RecordingCanvas import androidx.compose.remote.creation.compose.layout.RemoteCanvas import androidx.compose.remote.creation.compose.layout.RemoteComposable import androidx.compose.remote.creation.compose.layout.RemoteDrawWithContentScope +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @@ -43,7 +44,7 @@ public fun RemoteModifier.drawWithContent( private class DrawWithContentModifier(val onDraw: RemoteDrawWithContentScope.() -> Unit) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.DrawWithContentModifier() } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/GraphicsLayerModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/GraphicsLayerModifier.kt index bca5e13dbc1db..15cc178179182 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/GraphicsLayerModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/GraphicsLayerModifier.kt @@ -21,6 +21,7 @@ import androidx.annotation.RestrictTo import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.remote.core.operations.layout.modifiers.GraphicsLayerModifierOperation import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.creation.modifiers.CircleShape import androidx.compose.remote.creation.modifiers.RecordingModifier @@ -35,69 +36,78 @@ import androidx.compose.ui.graphics.layer.CompositingStrategy.Companion.Offscree @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class GraphicsLayerModifier( - public val scaleX: Float, - public val scaleY: Float, - public val rotationX: Float, - public val rotationY: Float, - public val rotationZ: Float, - public val shadowElevation: Float, - public val transformOriginX: Float, - public val transformOriginY: Float, - public val translationX: Float, - public val translationY: Float, + public val scaleX: RemoteFloat, + public val scaleY: RemoteFloat, + public val rotationX: RemoteFloat, + public val rotationY: RemoteFloat, + public val rotationZ: RemoteFloat, + public val shadowElevation: RemoteFloat, + public val transformOriginX: RemoteFloat, + public val transformOriginY: RemoteFloat, + public val translationX: RemoteFloat, + public val translationY: RemoteFloat, public val shape: Shape, public val compositingStrategy: Int, - public val alpha: Float, - public val cameraDistance: Float, + public val alpha: RemoteFloat, + public val cameraDistance: RemoteFloat, public val renderEffect: RenderEffect?, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { val layer = androidx.compose.remote.creation.modifiers.GraphicsLayerModifier() - if (scaleX != 1f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.SCALE_X, scaleX) + if (scaleX.floatId != 1f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.SCALE_X, scaleX.floatId) } - if (scaleY != 1f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.SCALE_Y, scaleY) + if (scaleY.floatId != 1f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.SCALE_Y, scaleY.floatId) } - if (rotationX != 0f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_X, rotationX) + if (rotationX.floatId != 0f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_X, rotationX.floatId) } - if (rotationY != 0f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_Y, rotationY) + if (rotationY.floatId != 0f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_Y, rotationY.floatId) } - if (rotationZ != 0f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_Z, rotationZ) + if (rotationZ.floatId != 0f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.ROTATION_Z, rotationZ.floatId) } - if (shadowElevation != 0f) { + if (shadowElevation.floatId != 0f) { layer.setFloatAttribute( GraphicsLayerModifierOperation.SHADOW_ELEVATION, - shadowElevation, + shadowElevation.floatId, ) } - if (transformOriginX != 0.5f) { + if (transformOriginX.floatId != 0.5f) { layer.setFloatAttribute( GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_X, - transformOriginX, + transformOriginX.floatId, ) } - if (transformOriginY != 0.5f) { + if (transformOriginY.floatId != 0.5f) { layer.setFloatAttribute( GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_Y, - transformOriginY, + transformOriginY.floatId, ) } - if (translationX != 0f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.TRANSLATION_X, translationX) + if (translationX.floatId != 0f) { + layer.setFloatAttribute( + GraphicsLayerModifierOperation.TRANSLATION_X, + translationX.floatId, + ) } - if (translationY != 0f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.TRANSLATION_Y, translationY) + if (translationY.floatId != 0f) { + layer.setFloatAttribute( + GraphicsLayerModifierOperation.TRANSLATION_Y, + translationY.floatId, + ) } - if (alpha != 1f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.ALPHA, alpha) + if (alpha.floatId != 1f) { + layer.setFloatAttribute(GraphicsLayerModifierOperation.ALPHA, alpha.floatId) } - if (cameraDistance != 8f) { - layer.setFloatAttribute(GraphicsLayerModifierOperation.CAMERA_DISTANCE, cameraDistance) + if (cameraDistance.floatId != 8f) { + layer.setFloatAttribute( + GraphicsLayerModifierOperation.CAMERA_DISTANCE, + cameraDistance.floatId, + ) } if (compositingStrategy != 0) { layer.setIntAttribute( @@ -172,20 +182,20 @@ public fun RemoteModifier.graphicsLayer( } return then( GraphicsLayerModifier( - scaleX.id, - scaleY.id, - rotationX.id, - rotationY.id, - rotationZ.id, - shadowElevation.id, - transformOriginX.id, - transformOriginY.id, - translationX.id, - translationY.id, + scaleX, + scaleY, + rotationX, + rotationY, + rotationZ, + shadowElevation, + transformOriginX, + transformOriginY, + translationX, + translationY, shape, cS, - alpha.id, - cameraDistance.id, + alpha, + cameraDistance, renderEffect, ) ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightInModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightInModifier.kt index c7491ac553d90..dd13a7b55063b 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightInModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightInModifier.kt @@ -18,6 +18,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp @@ -27,7 +28,7 @@ public class HeightInModifier( public val min: Dp = Dp.Unspecified, public val max: Dp = Dp.Unspecified, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { var minValue = 0f var maxValue = Float.MAX_VALUE if (min != Dp.Unspecified) { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightModifier.kt index fec3e02e12e4f..11d9c79e4ed61 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/HeightModifier.kt @@ -22,17 +22,15 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifierOperation.Type import androidx.compose.remote.creation.compose.state.RemoteDp import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class HeightModifier(public val type: Type, public val value: RemoteFloat) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return androidx.compose.remote.creation.modifiers.HeightModifier( - type, - value.internalAsFloat(), - ) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return androidx.compose.remote.creation.modifiers.HeightModifier(type, value.floatId) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/MarqueeModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/MarqueeModifier.kt index b31a130717c96..871586d069a35 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/MarqueeModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/MarqueeModifier.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.foundation.MarqueeSpacing +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier /** @@ -49,7 +50,7 @@ public class MarqueeModifier( public val velocity: Float, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.MarqueeModifier( iterations, animationMode, diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/OffsetModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/OffsetModifier.kt index e658ffb9c67e4..1e6410ee1fd8f 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/OffsetModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/OffsetModifier.kt @@ -20,17 +20,15 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteDp import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class OffsetModifier(public val x: RemoteFloat, public val y: RemoteFloat) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return androidx.compose.remote.creation.modifiers.OffsetModifier( - x.internalAsFloat(), - y.internalAsFloat(), - ) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return androidx.compose.remote.creation.modifiers.OffsetModifier(x.floatId, y.floatId) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/PaddingModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/PaddingModifier.kt index 5ab0c69caaf68..2860bc1ca1d32 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/PaddingModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/PaddingModifier.kt @@ -20,6 +20,7 @@ import androidx.annotation.RestrictTo import androidx.compose.foundation.layout.padding import androidx.compose.remote.creation.compose.layout.RemotePaddingValues import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @@ -36,21 +37,21 @@ public class PaddingModifier( ) : RemoteModifier.Element { init { require( - (!left.hasConstantValue || left.toFloat() >= 0f) and - (!top.hasConstantValue || top.toFloat() >= 0f) and - (!right.hasConstantValue || right.toFloat() >= 0f) and - (!bottom.hasConstantValue || bottom.toFloat() >= 0f) + (!left.hasConstantValue || left.constantValue!! >= 0f) and + (!top.hasConstantValue || top.constantValue!! >= 0f) and + (!right.hasConstantValue || right.constantValue!! >= 0f) and + (!bottom.hasConstantValue || bottom.constantValue!! >= 0f) ) { "Padding must be non-negative" } } - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.PaddingModifier( - left.internalAsFloat(), - top.internalAsFloat(), - right.internalAsFloat(), - bottom.internalAsFloat(), + left.floatId, + top.floatId, + right.floatId, + bottom.floatId, ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RemoteModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RemoteModifier.kt index 5752e78503941..39802788b807d 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RemoteModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RemoteModifier.kt @@ -18,6 +18,7 @@ package androidx.compose.remote.creation.compose.modifier import android.annotation.SuppressLint import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -31,7 +32,8 @@ import androidx.compose.ui.Modifier @Stable public sealed interface RemoteModifier { - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public fun toRemoteCompose(): RecordingModifier + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public fun RemoteStateScope.toRecordingModifier(): RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Composable @@ -94,11 +96,11 @@ public sealed interface RemoteModifier { override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this) - override fun toRemoteCompose(): RecordingModifier { - return RecordingModifier().then(toRemoteComposeElement()) + override fun RemoteStateScope.toRecordingModifier(): RecordingModifier { + return RecordingModifier().then(toRecordingModifierElement()) } - public fun toRemoteComposeElement(): RecordingModifier.Element + public fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element } /** @@ -127,7 +129,7 @@ public sealed interface RemoteModifier { override fun toString(): String = "Modifier" @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - override fun toRemoteCompose(): RecordingModifier = RecordingModifier() + override fun RemoteStateScope.toRecordingModifier(): RecordingModifier = RecordingModifier() @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Composable @@ -143,6 +145,15 @@ public fun RemoteModifier.toComposeUi(): Modifier { return Modifier.toComposeUi() } +/** + * Converts a [RemoteModifier] to a [RecordingModifier] within a [RemoteStateScope]. + * + * This is the primary entry point for converting remote modifiers during document capture. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public fun RemoteStateScope.toRecordingModifier(modifier: RemoteModifier): RecordingModifier = + with(modifier) { toRecordingModifier() } + /** * Filter the Layout relevant [RemoteModifier.Element]s and then convert to Compose UI [Modifier]. */ @@ -163,18 +174,19 @@ public class CombinedRemoteModifier( private val inner: RemoteModifier, ) : RemoteModifier { - override fun toRemoteCompose(): RecordingModifier { + override fun RemoteStateScope.toRecordingModifier(): RecordingModifier { + val scope = this return RecordingModifier().apply { if (outer is RemoteModifier.Element) { - then(outer.toRemoteComposeElement()) + then(with(outer) { scope.toRecordingModifierElement() }) } else { - then(outer.toRemoteCompose()) + then(with(outer) { scope.toRecordingModifier() }) } if (inner is RemoteModifier.Element) { - then(inner.toRemoteComposeElement()) + then(with(inner) { scope.toRecordingModifierElement() }) } else { - then(inner.toRemoteCompose()) + then(with(inner) { scope.toRecordingModifier() }) } } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RippleModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RippleModifier.kt index 9ffe20b6326d2..0960b71c4e8e6 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RippleModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/RippleModifier.kt @@ -18,12 +18,13 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class RippleModifier() : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.RippleModifier() } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ScrollModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ScrollModifier.kt index 9a5aff10b832f..01150edc10829 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ScrollModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ScrollModifier.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.ScrollState import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.state.MutableRemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.ScrollModifier as CoreScrollModifier import androidx.compose.runtime.Composable @@ -52,8 +53,8 @@ public fun rememberRemoteScrollState(evenNotches: Int = 0): RemoteScrollState { @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public data class ScrollModifier(val direction: Int, val state: RemoteScrollState) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return CoreScrollModifier(direction, state.positionState.id, state.notches) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return CoreScrollModifier(direction, state.positionState.floatId, state.notches) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/SemanticsModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/SemanticsModifier.kt index 799eeb554003e..c9dea8076ba01 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/SemanticsModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/SemanticsModifier.kt @@ -24,7 +24,7 @@ import androidx.compose.remote.core.semantics.AccessibleComponent.Mode.CLEAR_AND import androidx.compose.remote.core.semantics.AccessibleComponent.Mode.MERGE import androidx.compose.remote.core.semantics.AccessibleComponent.Mode.SET import androidx.compose.remote.core.semantics.CoreSemantics -import androidx.compose.remote.creation.compose.state.FallbackCreationState +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.RemoteString import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.ui.semantics.Role @@ -37,17 +37,13 @@ import androidx.compose.ui.semantics.text @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public data class SemanticsModifier(val mergeMode: Mode, val semantics: AccessibilitySemantics) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.SemanticsModifier( CoreSemantics().apply { mMode = mergeMode - mTextId = semantics.text?.getIdForCreationState(FallbackCreationState.state) ?: 0 - mContentDescriptionId = - semantics.contentDescription?.getIdForCreationState(FallbackCreationState.state) - ?: 0 - mStateDescriptionId = - semantics.stateDescription?.getIdForCreationState(FallbackCreationState.state) - ?: 0 + mTextId = semantics.text?.id ?: 0 + mContentDescriptionId = semantics.contentDescription?.id ?: 0 + mStateDescriptionId = semantics.stateDescription?.id ?: 0 mEnabled = semantics.enabled ?: true mRole = fromRole(semantics.role) } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchCancelActionModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchCancelActionModifier.kt index 2d32a824adf3a..d11371d46882d 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchCancelActionModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchCancelActionModifier.kt @@ -19,15 +19,16 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.action.Action +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.TouchActionModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class TouchCancelActionModifier(public val actions: List) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.TouchActionModifier( TouchActionModifier.CANCEL, - @Suppress("ListIterator") actions.map { it.toRemoteAction() }, + @Suppress("ListIterator") actions.map { action -> with(action) { toRemoteAction() } }, ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchDownActionModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchDownActionModifier.kt index 401cb93f37aa8..fa052233d31ef 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchDownActionModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchDownActionModifier.kt @@ -19,15 +19,16 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.action.Action +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.TouchActionModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class TouchDownActionModifier(public val actions: List) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.TouchActionModifier( TouchActionModifier.DOWN, - @Suppress("ListIterator") actions.map { it.toRemoteAction() }, + @Suppress("ListIterator") actions.map { action -> with(action) { toRemoteAction() } }, ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchUpActionModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchUpActionModifier.kt index 6a54d74f769e5..672a512d14ef8 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchUpActionModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/TouchUpActionModifier.kt @@ -19,15 +19,16 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.action.Action +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.remote.creation.modifiers.TouchActionModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class TouchUpActionModifier(public val actions: List) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { return androidx.compose.remote.creation.modifiers.TouchActionModifier( TouchActionModifier.UP, - @Suppress("ListIterator") actions.map { it.toRemoteAction() }, + @Suppress("ListIterator") actions.map { action -> with(action) { toRemoteAction() } }, ) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/VisibilityModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/VisibilityModifier.kt index c550fd75af236..5d3f262e974f3 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/VisibilityModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/VisibilityModifier.kt @@ -19,13 +19,13 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteInt +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class VisibilityModifier(public val visible: RemoteInt) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - val id = visible.getIntId() - return androidx.compose.remote.creation.modifiers.VisibilityModifier(id) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return androidx.compose.remote.creation.modifiers.VisibilityModifier(visible.id.toInt()) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthInModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthInModifier.kt index 82de665dd20c8..e69499683b01f 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthInModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthInModifier.kt @@ -18,6 +18,7 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp @@ -27,7 +28,7 @@ public class WidthInModifier( public val min: Dp = Dp.Unspecified, public val max: Dp = Dp.Unspecified, ) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { var minValue = 0f var maxValue = Float.MAX_VALUE if (min != Dp.Unspecified) { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthModifier.kt index 4a6132f3ffb29..ad0e4d8ad48b0 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/WidthModifier.kt @@ -22,17 +22,15 @@ import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.remote.core.operations.layout.modifiers.DimensionModifierOperation.Type import androidx.compose.remote.creation.compose.state.RemoteDp import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier import androidx.compose.runtime.Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class WidthModifier(public val type: Type, public val value: RemoteFloat) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return androidx.compose.remote.creation.modifiers.WidthModifier( - type, - value.internalAsFloat(), - ) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return androidx.compose.remote.creation.modifiers.WidthModifier(type, value.floatId) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ZIndexModifier.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ZIndexModifier.kt index 59d16cab3f126..d0c3ffbf43104 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ZIndexModifier.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/modifier/ZIndexModifier.kt @@ -19,13 +19,14 @@ package androidx.compose.remote.creation.compose.modifier import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.modifiers.RecordingModifier @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ZIndexModifier(public val value: RemoteFloat) : RemoteModifier.Element { - override fun toRemoteComposeElement(): RecordingModifier.Element { - return androidx.compose.remote.creation.modifiers.ZIndexModifier(value.internalAsFloat()) + override fun RemoteStateScope.toRecordingModifierElement(): RecordingModifier.Element { + return androidx.compose.remote.creation.modifiers.ZIndexModifier(value.floatId) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteBrush.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteBrush.kt index 00e38cf511fe3..091f9d5273250 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteBrush.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteBrush.kt @@ -23,6 +23,7 @@ import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationSta import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteMatrix3x3 +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rc import androidx.compose.runtime.Immutable import androidx.compose.ui.geometry.Size @@ -46,7 +47,7 @@ public abstract class RemoteBrush { */ public val intrinsicSize: Size = Size.Unspecified - public abstract fun createShader(size: RemoteSize): Shader + public abstract fun RemoteStateScope.createShader(size: RemoteSize): Shader public open val hasShader: Boolean get() = true diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteLinearGradient.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteLinearGradient.kt index ffe6db10ed6de..0cadffd0da6c1 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteLinearGradient.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteLinearGradient.kt @@ -25,13 +25,16 @@ import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteMatrix3x3 +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Shader import androidx.compose.ui.graphics.TileMode as ComposeTileMode import androidx.compose.ui.graphics.toAndroidTileMode +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.util.fastMap +import kotlin.collections.toFloatArray /** * Creates a linear gradient with the provided colors along the given start and end coordinates. The @@ -309,7 +312,7 @@ public data class RemoteLinearGradient( private val tileMode: ComposeTileMode = ComposeTileMode.Clamp, ) : RemoteBrush() { - override fun createShader(size: RemoteSize): Shader { + override fun RemoteStateScope.createShader(size: RemoteSize): Shader { val realStart = start ?: RemoteOffset(0.0f.rf, 0.0f.rf) val realEnd = end ?: endVector(size) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteRadialGradient.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteRadialGradient.kt index 20f90d8ede450..a3ac377aec2f0 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteRadialGradient.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteRadialGradient.kt @@ -25,6 +25,7 @@ import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteMatrix3x3 +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.geometry.Offset @@ -121,7 +122,7 @@ public data class RemoteRadialGradient( private val tileMode: ComposeTileMode = ComposeTileMode.Clamp, ) : RemoteBrush() { - override fun createShader(size: RemoteSize): Shader { + override fun RemoteStateScope.createShader(size: RemoteSize): Shader { val realCenter = center ?: size.center val realRadius = radius ?: (size.width.min(size.height) / 2f) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSolidColor.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSolidColor.kt index 65c4ba27b7d75..a94e9c38c49c1 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSolidColor.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSolidColor.kt @@ -20,6 +20,7 @@ package androidx.compose.remote.creation.compose.shaders import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteColor +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Shader @@ -32,7 +33,7 @@ public fun RemoteBrush.Companion.solidColor(color: RemoteColor): RemoteSolidColo @Immutable public data class RemoteSolidColor(val color: RemoteColor) : RemoteBrush() { - override fun createShader(size: RemoteSize): Shader { + override fun RemoteStateScope.createShader(size: RemoteSize): Shader { throw UnsupportedOperationException( "SolidColor not supported for Shader, use Color directly" ) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSweepGradient.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSweepGradient.kt index c0306e6293297..4cd7c095f43fc 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSweepGradient.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shaders/RemoteSweepGradient.kt @@ -25,6 +25,7 @@ import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemoteMatrix3x3 +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Shader @@ -92,7 +93,7 @@ public data class RemoteSweepGradient( private val center: RemoteOffset? = null, ) : RemoteBrush() { - override fun createShader(size: RemoteSize): Shader { + override fun RemoteStateScope.createShader(size: RemoteSize): Shader { val realCenter = center ?: size.center val centerX = resolve(realCenter.x, size.width) val centerY = resolve(realCenter.y, size.height) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shapes/RemoteOutline.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shapes/RemoteOutline.kt index afe138407b55d..d611f935d7260 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shapes/RemoteOutline.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/shapes/RemoteOutline.kt @@ -22,9 +22,6 @@ import androidx.compose.remote.creation.RemotePath import androidx.compose.remote.creation.compose.layout.RemoteDrawScope import androidx.compose.remote.creation.compose.layout.RemoteOffset import androidx.compose.remote.creation.compose.layout.RemoteSize -import androidx.compose.remote.creation.compose.layout.conicTo -import androidx.compose.remote.creation.compose.layout.lineTo -import androidx.compose.remote.creation.compose.layout.moveTo import androidx.compose.remote.creation.compose.state.RemoteFloat import androidx.compose.remote.creation.compose.state.RemotePaint import androidx.compose.remote.creation.compose.state.rf @@ -84,34 +81,46 @@ public sealed class RemoteOutline { val circularArcWeight = 0.7071f.rf // Weight for a 90-degree circular arc // 1. Move to top edge - path.moveTo(topLeft, 0f.rf) + path.moveTo(topLeft.floatId, 0f.rf.floatId) // 2. Top Line & Top-Right Corner - path.lineTo(w - topRight, 0f.rf) - path.conicTo(x1 = w, y1 = 0f.rf, x2 = w, y2 = topRight, weight = circularArcWeight) + path.lineTo((w - topRight).floatId, 0f.rf.floatId) + path.conicTo( + x1 = w.floatId, + y1 = 0f.rf.floatId, + x2 = w.floatId, + y2 = topRight.floatId, + weight = circularArcWeight.floatId, + ) // 3. Right Line & Bottom-Right Corner - path.lineTo(w, h - bottomRight) - path.conicTo(x1 = w, y1 = h, x2 = w - bottomRight, y2 = h, weight = circularArcWeight) + path.lineTo(w.floatId, (h - bottomRight).floatId) + path.conicTo( + x1 = w.floatId, + y1 = h.floatId, + x2 = (w - bottomRight).floatId, + y2 = h.floatId, + weight = circularArcWeight.floatId, + ) // 4. Bottom Line & Bottom-Left Corner - path.lineTo(bottomLeft, h) + path.lineTo(bottomLeft.floatId, h.floatId) path.conicTo( - x1 = 0f.rf, - y1 = h, - x2 = 0f.rf, - y2 = h - bottomLeft, - weight = circularArcWeight, + x1 = 0f.rf.floatId, + y1 = h.floatId, + x2 = 0f.rf.floatId, + y2 = (h - bottomLeft).floatId, + weight = circularArcWeight.floatId, ) // 5. Start Line & Top-Left Corner - path.lineTo(0f.rf, topLeft) + path.lineTo(0f.rf.floatId, topLeft.floatId) path.conicTo( - x1 = 0f.rf, - y1 = 0f.rf, - x2 = topLeft, - y2 = 0f.rf, - weight = circularArcWeight, + x1 = 0f.rf.floatId, + y1 = 0f.rf.floatId, + x2 = topLeft.floatId, + y2 = 0f.rf.floatId, + weight = circularArcWeight.floatId, ) // 6. Close the path diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteBitmap.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteBitmap.kt index 6aa814b6e4241..9dc7b70f7fb5e 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteBitmap.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteBitmap.kt @@ -39,42 +39,32 @@ import androidx.compose.ui.graphics.asAndroidBitmap */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class RemoteBitmap -internal constructor( - public val state: RemoteComposeCreationState?, - public override val constantValue: Bitmap?, -) : BaseRemoteState() { - - // @Deprecated("Use getIdForCreationState directly") - // TODO: re-enable this asap - public val id: Int - get() { - // FallbackCreationState.state.platform.log( - // Platform.LogCategory.TODO, - // "Use RemoteBitmap.getIdForCreationState directly" - // ) - return getIdForCreationState(FallbackCreationState.state) - } +internal constructor(public override val constantValue: Bitmap?) : BaseRemoteState() { /** The width of the bitmap as represented in the remote document. */ public val width: RemoteFloat get() { - val width = - state?.document?.bitmapAttribute(id, IMAGE_WIDTH) - ?: throw IllegalStateException( - "Bitmap width is not available in the remote document." + return RemoteFloatExpression(null) { creationState -> + floatArrayOf( + creationState.document.bitmapAttribute( + getIdForCreationState(creationState), + IMAGE_WIDTH, ) - return RemoteFloat(width) + ) + } } /** The height of the bitmap as represented in the remote document. */ public val height: RemoteFloat get() { - val height = - state?.document?.bitmapAttribute(id, IMAGE_HEIGHT) - ?: throw IllegalStateException( - "Bitmap height is not available in the remote document." + return RemoteFloatExpression(null) { creationState -> + floatArrayOf( + creationState.document.bitmapAttribute( + getIdForCreationState(creationState), + IMAGE_HEIGHT, ) - return RemoteFloat(height) + ) + } } public companion object { @@ -83,18 +73,10 @@ internal constructor( * with or without an explicit [RemoteComposeCreationState]. * * @param v The [Bitmap] value. - * @param state An optional [RemoteComposeCreationState] to associate with this bitmap. If - * not provided, the bitmap will be added to the document when its ID is requested. * @return A [RemoteBitmap] representing the provided bitmap. */ - @JvmOverloads - public operator fun invoke( - v: Bitmap, - state: RemoteComposeCreationState? = null, - ): RemoteBitmap { - return MutableRemoteBitmap(state, v) { creationState -> - creationState.document.addBitmap(v) - } + public operator fun invoke(v: Bitmap): MutableRemoteBitmap { + return MutableRemoteBitmap(v) { creationState -> creationState.document.addBitmap(v) } } /** @@ -105,12 +87,8 @@ internal constructor( * @param initialValue The initial [Bitmap] value for the named remote bitmap. * @return A [RemoteBitmap] representing the named bitmap. */ - public fun createNamedRemoteBitmap( - name: String, - initialValue: Bitmap, - state: RemoteComposeCreationState, - ): RemoteBitmap = - MutableRemoteBitmap(state, constantValue = null) { creationState -> + public fun createNamedRemoteBitmap(name: String, initialValue: Bitmap): RemoteBitmap = + MutableRemoteBitmap(constantValue = null) { creationState -> creationState.document.addNamedBitmap(name, initialValue) } @@ -122,7 +100,7 @@ internal constructor( * @return A [RemoteBitmap] with the specified [width] and [height]. */ public fun createOffscreenRemoteBitmap(width: Int, height: Int): RemoteBitmap = - object : RemoteBitmap(null, null) { + object : RemoteBitmap(null) { public override fun writeToDocument( creationState: RemoteComposeCreationState ): Int = creationState.document.createBitmap(width, height) @@ -141,10 +119,9 @@ internal constructor( */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class MutableRemoteBitmap( - state: RemoteComposeCreationState?, constantValue: Bitmap?, private val idProvider: (creationState: RemoteComposeCreationState) -> Int, -) : RemoteBitmap(state, constantValue), MutableRemoteState { +) : RemoteBitmap(constantValue), MutableRemoteState { public override fun writeToDocument(creationState: RemoteComposeCreationState): Int = idProvider(creationState) @@ -168,7 +145,7 @@ public fun rememberRemoteBitmapValue( val state = LocalRemoteComposeCreationState.current return rememberNamedState(name, domain) { val initial = value() - MutableRemoteBitmap(state, constantValue = null) { creationState -> + MutableRemoteBitmap(constantValue = null) { creationState -> creationState.document.addNamedBitmap("$domain:$name", initial) } } @@ -182,12 +159,11 @@ public fun rememberRemoteBitmap( width: Int = 1, height: Int = 1, ): RemoteBitmap { - val state = LocalRemoteComposeCreationState.current return rememberNamedState(name, domain) { // We create a bitmap of the specified dimensions as a placeholder. The actual bitmap will // be loaded from the URL on the remote side. Providing accurate dimensions can prevent // unnecessary relayouts. - MutableRemoteBitmap(state, constantValue = null) { creationState -> + MutableRemoteBitmap(constantValue = null) { creationState -> creationState.document.addNamedBitmapUrl("$domain:$name", url) } } @@ -196,5 +172,7 @@ public fun rememberRemoteBitmap( /** Extension property to convert a [ImageBitmap] to a [RemoteBitmap]. */ public val ImageBitmap.rb: RemoteBitmap get() { - return RemoteBitmap(this.asAndroidBitmap()) + return MutableRemoteBitmap(this.asAndroidBitmap()) { creationState -> + creationState.document.addBitmap(this.asAndroidBitmap()) + } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteColor.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteColor.kt index 88b3a7d11af45..510d365dc1784 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteColor.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteColor.kt @@ -98,10 +98,10 @@ internal constructor( color: Color ) : this( constantValue = color, - alpha = color.alpha()?.rf, - red = color.red()?.rf, - green = color.green()?.rf, - blue = color.blue()?.rf, + alpha = color.alpha().rf, + red = color.red().rf, + green = color.green().rf, + blue = color.blue().rf, idProvider = { creationState -> creationState.document.addColor(color.toArgb()) }, ) @@ -126,17 +126,6 @@ internal constructor( public override fun writeToDocument(creationState: RemoteComposeCreationState): Int = idProvider(creationState) - // @Deprecated("Use getIdForCreationState directly") - // TODO: re-enable this asap - public val id: Int - get() { - // FallbackCreationState.state.platform.log( - // Platform.LogCategory.TODO, - // "Use RemoteColor.getIdForCreationState directly" - // ) - return getIdForCreationState(FallbackCreationState.state) - } - /** * Computes the pairwise product of this [RemoteColor] with [other]. * diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloat.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloat.kt index 802e53833475f..f02c9e034e734 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloat.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloat.kt @@ -87,32 +87,15 @@ public abstract class RemoteFloat : BaseRemoteState() { return array } - /** - * Deprecated property to get the ID of the remote float. It\'s recommended to use - * [getFloatIdForCreationState] directly for clarity and to pass the correct - * [RemoteComposeCreationState]. - */ - // TODO: re-enable asap - // @Deprecated("Use getIdForCreationState directly") - public open val id: Float - get() { - // FallbackCreationState.state.platform.log( - // Platform.LogCategory.TODO, - // "Use RemoteFloat.getIdForCreationState directly" - // ) - return getFloatIdForCreationState(FallbackCreationState.state) - } - override fun getFloatIdForCreationState(creationState: RemoteComposeCreationState): Float { - return constantValue ?: super.getFloatIdForCreationState(creationState) - } - - public fun internalAsFloat(): Float { - return id - } - - public fun toFloat(): Float { - return id + constantValue?.let { + return it + } + val array = arrayForCreationState(creationState) + if (array.size == 1) { + return array[0] + } + return super.getFloatIdForCreationState(creationState) } /** @@ -1069,16 +1052,6 @@ internal constructor( return Utils.idFromNan(creationState.document.floatExpression(*array)) } } - - public override val id: Float - get(): Float { - // Some of the callers expect RemoteFloat(123) to return 123 from this method. - val array = arrayForCreationState(FallbackCreationState.state) - if (array.size == 1) { - return array[0] - } - return getFloatIdForCreationState(FallbackCreationState.state) - } } /** @@ -1325,7 +1298,7 @@ public fun rememberRemoteFloat( * @return The created [RemoteFloat]. */ public fun remoteFloat( - state: RemoteComposeCreationState, + state: RemoteStateScope, content: RemoteFloatContext.() -> RemoteFloat, ): RemoteFloat { val context = RemoteFloatContext(state) diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloatArray.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloatArray.kt index 2b2064f743406..70ade2801a773 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloatArray.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteFloatArray.kt @@ -29,7 +29,7 @@ public class RemoteFloatArray(public override val constantValue: List>() { override fun writeToDocument(creationState: RemoteComposeCreationState): Int { - val asFloat = constantValue!!.fastMap { it.toFloat() }.toFloatArray() + val asFloat = with(creationState) { constantValue!!.fastMap { it.floatId }.toFloatArray() } return Utils.idFromNan(creationState.document.addFloatArray(asFloat)) } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteInt.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteInt.kt index db74875df805b..c1774ac086fb6 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteInt.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteInt.kt @@ -21,9 +21,6 @@ import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.TextFromFloat import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.core.operations.utilities.IntegerExpressionEvaluator -import androidx.compose.remote.creation.actions.Action -import androidx.compose.remote.creation.actions.ValueIntegerChange -import androidx.compose.remote.creation.actions.ValueIntegerExpressionChange import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.creation.compose.layout.RemoteComposable @@ -85,17 +82,6 @@ internal constructor( internal val arrayProvider: (creationState: RemoteComposeCreationState) -> LongArray, ) : BaseRemoteState() { - // @Deprecated("Use getLongIdForCreationState instead") - // TODO: re-enable asap - public val id: Long - get() { - // FallbackCreationState.state.platform.log( - // Platform.LogCategory.TODO, - // "Use RemoteInt.getLongIdForCreationState directly" - // ) - return getLongIdForCreationState(FallbackCreationState.state) - } - /** * Retrieves the [LongArray] representing this [RemoteInt]\'s expression using the provided * [creationState]. It utilizes a cache within the [creationState] to avoid redundant @@ -114,15 +100,6 @@ internal constructor( return array } - /** - * Retrieves the integer portion of the remote ID. - * - * @return The integer ID. - */ - public fun getIntId(): Int { - return Utils.idFromLong(id).toInt() - } - /** * Converts this [RemoteInt] to a [RemoteFloat]. If the [RemoteInt] is a literal, it\'s directly * converted to a float. Otherwise, a [RemoteFloatExpression] is created that references the @@ -923,16 +900,6 @@ public fun rememberRemoteInt( } } -public fun ValueChange(valueId: MutableRemoteInt, value: Int): Action { - return ValueIntegerChange(valueId.constantValue!!, value) -} - -public fun ValueChange(valueId: MutableRemoteInt, value: RemoteInt): Action { - val id1 = Utils.idFromLong(valueId.id) - val id2 = Utils.idFromLong(value.id) - return ValueIntegerExpressionChange(id1, id2) -} - /** Extension property to convert a [Int] to a [RemoteInt]. */ public val Int.ri: RemoteInt get() { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteLong.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteLong.kt index 961c77cd57ac2..9bc549abdb5f4 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteLong.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteLong.kt @@ -18,7 +18,6 @@ package androidx.compose.remote.creation.compose.state import androidx.annotation.RestrictTo -import androidx.compose.remote.core.RcPlatformServices import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.player.core.state.RemoteDomains import androidx.compose.runtime.Composable @@ -29,8 +28,6 @@ import androidx.compose.runtime.Composable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class RemoteLong : BaseRemoteState() { - public abstract val id: Int - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public companion object { /** @@ -86,16 +83,6 @@ public class MutableRemoteLong( public override fun writeToDocument(creationState: RemoteComposeCreationState): Int = idProvider(creationState) - @Deprecated("Use getIdForCreationState directly") - public override val id: Int - get() { - FallbackCreationState.state.platform.log( - RcPlatformServices.LogCategory.TODO, - "Use RemoteLong.getIdForCreationState directly", - ) - return getIdForCreationState(FallbackCreationState.state) - } - public override fun toString(): String { return "MutableRemoteLong@${this.hashCode()} =" + constantValue } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemotePaint.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemotePaint.kt index 876790a4e7cdd..8cd31b06999f4 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemotePaint.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemotePaint.kt @@ -148,9 +148,9 @@ public open class RemotePaint : Paint { super.setColor(color) } - public fun applyRemoteBrush(remoteBrush: RemoteBrush, size: RemoteSize) { + public fun RemoteStateScope.applyRemoteBrush(remoteBrush: RemoteBrush, size: RemoteSize) { if (remoteBrush.hasShader) { - shader = remoteBrush.createShader(size) + shader = with(remoteBrush) { createShader(size) } remoteColor = null } else if (remoteBrush is RemoteSolidColor) { remoteColor = remoteBrush.color diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteState.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteState.kt index 371a7e933045e..54b4ae20082b4 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteState.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteState.kt @@ -21,26 +21,12 @@ import androidx.annotation.RestrictTo import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.creation.RemoteComposeWriter import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState -import androidx.compose.remote.creation.compose.capture.NoRemoteCompose import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState import androidx.compose.remote.creation.compose.layout.RemoteComposable import androidx.compose.remote.player.core.state.RemoteDomains import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -// TODO: Remove this and APIs using it. -public object FallbackCreationState { - private var state_: RemoteComposeCreationState? = null - - /** The [RemoteComposeCreationState] to use when the state isn\'t passed in. */ - public var state: RemoteComposeCreationState - get() = state_ ?: NoRemoteCompose() - set(value) { - state_ = value - } -} - /** Common base interface for all Remote types. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class BaseRemoteState : RemoteState { diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteStateScope.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteStateScope.kt new file mode 100644 index 0000000000000..2b3628e83034b --- /dev/null +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteStateScope.kt @@ -0,0 +1,45 @@ +/* + * 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. + */ +@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + +package androidx.compose.remote.creation.compose.state + +import androidx.annotation.RestrictTo +import androidx.compose.remote.creation.RemoteComposeWriter +import androidx.compose.remote.creation.compose.capture.RemoteComposeCreationState + +/** Scope for accessing remote state IDs. */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public interface RemoteStateScope { + /** The [RemoteComposeCreationState] associated with the document being drawn into. */ + public val creationState: RemoteComposeCreationState + + /** The [RemoteComposeWriter] associated with the document being drawn into. */ + public val document: RemoteComposeWriter + get() = creationState.document + + /** Returns the ID for this state within the scope. */ + public val RemoteState<*>.id: Int + get() = (this as BaseRemoteState<*>).getIdForCreationState(creationState) + + /** Returns the float ID for this state within the scope. */ + public val RemoteState<*>.floatId: Float + get() = (this as BaseRemoteState<*>).getFloatIdForCreationState(creationState) + + /** Returns the long ID for this state within the scope. */ + public val RemoteState<*>.longId: Long + get() = (this as BaseRemoteState<*>).getLongIdForCreationState(creationState) +} diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteString.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteString.kt index e35506ea31c3f..28264f09f8240 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteString.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/state/RemoteString.kt @@ -811,6 +811,7 @@ internal constructor( private val lazyRemoteString: LazyRemoteString, ) : RemoteString(), MutableRemoteState { + /** Create a MutableRemoteString from an existing id. */ public constructor( id: Int ) : this( @@ -823,6 +824,20 @@ internal constructor( }, ) + /** Create a MutableRemoteString for a default value. */ + public constructor( + value: String + ) : this( + constantValue = null, + object : LazyRemoteString { + override fun reserveTextId(creationState: RemoteComposeCreationState) = + creationState.document.addText(value) + + override fun computeRequiredCodePointSet(creationState: RemoteComposeCreationState) = + null + }, + ) + public override fun writeToDocument(creationState: RemoteComposeCreationState): Int = lazyRemoteString.reserveTextId(creationState) @@ -849,8 +864,20 @@ public fun rememberRemoteString( val state = LocalRemoteComposeCreationState.current return rememberNamedState(name, domain) { val string = content() - val id = state.document.addNamedString("$domain:$name", string) - MutableRemoteString(id) + MutableRemoteString( + null, + object : LazyRemoteString { + override fun reserveTextId(creationState: RemoteComposeCreationState): Int { + return state.document.addNamedString("$domain:$name", string) + } + + override fun computeRequiredCodePointSet( + creationState: RemoteComposeCreationState + ): Set? { + return null + } + }, + ) } } @@ -862,11 +889,9 @@ public fun rememberRemoteString( */ @Composable public fun rememberRemoteString(content: () -> String): MutableRemoteString { - val state = LocalRemoteComposeCreationState.current return remember { val string = content() - val id = state.document.textCreateId(string) - MutableRemoteString(id) + MutableRemoteString(string) } } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/v2/RemoteComposeNodeV2.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/v2/RemoteComposeNodeV2.kt index 231a125dbb301..7ee2a7661d283 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/v2/RemoteComposeNodeV2.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/v2/RemoteComposeNodeV2.kt @@ -27,6 +27,7 @@ import androidx.compose.remote.creation.compose.layout.RemoteCanvas import androidx.compose.remote.creation.compose.layout.RemoteComposable import androidx.compose.remote.creation.compose.layout.RemoteDrawScope import androidx.compose.remote.creation.compose.modifier.RemoteModifier +import androidx.compose.remote.creation.compose.modifier.toRecordingModifier import androidx.compose.remote.creation.compose.state.RemoteBitmap import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.RemoteFloat @@ -67,7 +68,7 @@ internal class RemoteCanvasNodeV2 : RemoteComposeNodeV2() { setRemoteComposeCreationState(creationState) } - val recordingModifier = modifier.toRemoteCompose() + val recordingModifier = creationState.toRecordingModifier(modifier) creationState.document.startCanvas(recordingModifier) onDraw?.let { drawLambda -> val remoteCanvas = RemoteCanvas(recordingCanvas) @@ -96,11 +97,11 @@ internal class RemoteBoxNodeV2 : RemoteComposeNodeV2() { var verticalArrangement: RemoteArrangement.Vertical = RemoteArrangement.Top override fun render(creationState: RemoteComposeCreationState) { - val recordingModifier = modifier.toRemoteCompose() + val recordingModifier = creationState.toRecordingModifier(modifier) creationState.document.startBox( recordingModifier, - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) renderChildren(creationState) creationState.document.endBox() @@ -112,11 +113,11 @@ internal class RemoteRowNodeV2 : RemoteComposeNodeV2() { var verticalAlignment: RemoteAlignment.Vertical = RemoteAlignment.Top override fun render(creationState: RemoteComposeCreationState) { - val recordingModifier = modifier.toRemoteCompose() + val recordingModifier = creationState.toRecordingModifier(modifier) creationState.document.startRow( recordingModifier, - horizontalArrangement.toRemoteCompose(), - verticalAlignment.toRemoteCompose(), + horizontalArrangement.toRemote(), + verticalAlignment.toRemote(), ) renderChildren(creationState) creationState.document.endRow() @@ -128,11 +129,11 @@ internal class RemoteColumnNodeV2 : RemoteComposeNodeV2() { var horizontalAlignment: RemoteAlignment.Horizontal = RemoteAlignment.Start override fun render(creationState: RemoteComposeCreationState) { - val recordingModifier = modifier.toRemoteCompose() + val recordingModifier = creationState.toRecordingModifier(modifier) creationState.document.startColumn( recordingModifier, - horizontalAlignment.toRemoteCompose(), - verticalArrangement.toRemoteCompose(), + horizontalAlignment.toRemote(), + verticalArrangement.toRemote(), ) renderChildren(creationState) creationState.document.endColumn() @@ -189,18 +190,18 @@ internal class RemoteTextNodeV2 : RemoteComposeNodeV2() { val fontSizePx = fontSize.getFloatIdForCreationState(creationState) creationState.document.startTextComponent( - modifier.toRemoteCompose(), + with(modifier) { creationState.toRecordingModifier() }, textIdValue, colorInt, colorId, fontSizePx, minFontSize ?: -1f, maxFontSize ?: -1f, - fontStyle.toRemoteCompose(), + fontStyle.encode(), fontWeight.getFloatIdForCreationState(creationState), fontFamily, - textAlign.toRemoteCompose(), - overflow.toRemoteCompose(), + textAlign.encode(), + overflow.encode(), maxLines, letterSpacing ?: 0f, lineHeightAdd ?: 0f, @@ -243,16 +244,16 @@ internal class RemoteTextNodeV2 : RemoteComposeNodeV2() { val fontSizePx = fontSize.getFloatIdForCreationState(creationState) creationState.document.startTextComponent( - modifier.toRemoteCompose(), + with(modifier) { creationState.toRecordingModifier() }, textId, colorValue, fontSizePx, - fontStyle.toRemoteCompose(), + fontStyle.encode(), fontWeight.constantValue ?: 400f, fontFamily, flags, - textAlign.toRemoteCompose().toShort(), - overflow.toRemoteCompose(), + textAlign.encode().toShort(), + overflow.encode(), maxLines, ) creationState.document.endTextComponent() @@ -260,14 +261,14 @@ internal class RemoteTextNodeV2 : RemoteComposeNodeV2() { } } -private fun FontStyle?.toRemoteCompose(): Int = +private fun FontStyle?.encode(): Int = when (this) { FontStyle.Normal -> 0 FontStyle.Italic -> 1 else -> 0 } -private fun FontFamily?.toRemoteCompose(): String? = +private fun FontFamily?.encode(): String? = when (this) { null -> null FontFamily.Default -> "default" @@ -278,7 +279,7 @@ private fun FontFamily?.toRemoteCompose(): String? = else -> null } -private fun TextAlign?.toRemoteCompose(): Int = +private fun TextAlign?.encode(): Int = when (this) { TextAlign.Left -> 1 TextAlign.Right -> 2 @@ -289,7 +290,7 @@ private fun TextAlign?.toRemoteCompose(): Int = else -> 5 } -private fun TextOverflow.toRemoteCompose(): Int = +private fun TextOverflow.encode(): Int = when (this) { TextOverflow.Clip -> 0 TextOverflow.Ellipsis -> 1 @@ -309,7 +310,7 @@ internal class RemoteImageNodeV2 : RemoteComposeNodeV2() { ?: image?.let { creationState.document.addBitmap(it) } ?: 0 creationState.document.image( - modifier.toRemoteCompose(), + creationState.toRecordingModifier(modifier), bitmapId, contentScaleToInt(contentScale), alpha.getFloatIdForCreationState(creationState), diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemotePathBuilder.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemotePathBuilder.kt index 4d941471a2428..947d930378ef2 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemotePathBuilder.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemotePathBuilder.kt @@ -19,6 +19,7 @@ package androidx.compose.remote.creation.compose.vector import androidx.annotation.RestrictTo import androidx.compose.remote.creation.compose.state.RemoteFloat +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.ui.graphics.vector.PathNode import kotlin.collections.ArrayList @@ -27,7 +28,7 @@ import kotlin.collections.ArrayList * path. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class RemotePathBuilder { +public class RemotePathBuilder(scope: RemoteStateScope) : RemoteStateScope by scope { // 88% of Material icons use 32 or fewer path nodes private val _nodes = ArrayList(32) @@ -46,7 +47,7 @@ public class RemotePathBuilder { * @param y The y coordinate of the start of the new contour */ public fun moveTo(x: RemoteFloat, y: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.MoveTo(x.id, y.id)) + _nodes.add(PathNode.MoveTo(x.floatId, y.floatId)) } /** @@ -57,7 +58,7 @@ public class RemotePathBuilder { * @param dy The y offset of the start of the new contour, relative to the last path position */ public fun moveToRelative(dx: RemoteFloat, dy: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeMoveTo(dx.id, dy.id)) + _nodes.add(PathNode.RelativeMoveTo(dx.floatId, dy.floatId)) } /** @@ -69,7 +70,7 @@ public class RemotePathBuilder { * @param y The y coordinate of the end of the line */ public fun lineTo(x: RemoteFloat, y: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.LineTo(x.id, y.id)) + _nodes.add(PathNode.LineTo(x.floatId, y.floatId)) } /** @@ -81,7 +82,7 @@ public class RemotePathBuilder { * @param dy The y offset of the end of the line, relative to the last path position */ public fun lineToRelative(dx: RemoteFloat, dy: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeLineTo(dx.id, dy.id)) + _nodes.add(PathNode.RelativeLineTo(dx.floatId, dy.floatId)) } /** @@ -92,7 +93,7 @@ public class RemotePathBuilder { * @param x The x coordinate of the end of the line */ public fun horizontalLineTo(x: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.HorizontalTo(x.id)) + _nodes.add(PathNode.HorizontalTo(x.floatId)) } /** @@ -104,7 +105,7 @@ public class RemotePathBuilder { * @param dx The x offset of the end of the line, relative to the last path position */ public fun horizontalLineToRelative(dx: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeHorizontalTo(dx.id)) + _nodes.add(PathNode.RelativeHorizontalTo(dx.floatId)) } /** @@ -115,7 +116,7 @@ public class RemotePathBuilder { * @param y The y coordinate of the end of the line */ public fun verticalLineTo(y: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.VerticalTo(y.id)) + _nodes.add(PathNode.VerticalTo(y.floatId)) } /** @@ -127,7 +128,7 @@ public class RemotePathBuilder { * @param dy The y offset of the end of the line, relative to the last path position */ public fun verticalLineToRelative(dy: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeVerticalTo(dy.id)) + _nodes.add(PathNode.RelativeVerticalTo(dy.floatId)) } /** @@ -150,7 +151,9 @@ public class RemotePathBuilder { x3: RemoteFloat, y3: RemoteFloat, ): RemotePathBuilder = apply { - _nodes.add(PathNode.CurveTo(x1.id, y1.id, x2.id, y2.id, x3.id, y3.id)) + _nodes.add( + PathNode.CurveTo(x1.floatId, y1.floatId, x2.floatId, y2.floatId, x3.floatId, y3.floatId) + ) } /** @@ -179,7 +182,16 @@ public class RemotePathBuilder { dx3: RemoteFloat, dy3: RemoteFloat, ): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeCurveTo(dx1.id, dy1.id, dx2.id, dy2.id, dx3.id, dy3.id)) + _nodes.add( + PathNode.RelativeCurveTo( + dx1.floatId, + dy1.floatId, + dx2.floatId, + dy2.floatId, + dx3.floatId, + dy3.floatId, + ) + ) } /** @@ -201,7 +213,7 @@ public class RemotePathBuilder { x2: RemoteFloat, y2: RemoteFloat, ): RemotePathBuilder = apply { - _nodes.add(PathNode.ReflectiveCurveTo(x1.id, y1.id, x2.id, y2.id)) + _nodes.add(PathNode.ReflectiveCurveTo(x1.floatId, y1.floatId, x2.floatId, y2.floatId)) } /** @@ -225,7 +237,9 @@ public class RemotePathBuilder { dx2: RemoteFloat, dy2: RemoteFloat, ): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeReflectiveCurveTo(dx1.id, dy1.id, dx2.id, dy2.id)) + _nodes.add( + PathNode.RelativeReflectiveCurveTo(dx1.floatId, dy1.floatId, dx2.floatId, dy2.floatId) + ) } /** @@ -243,7 +257,9 @@ public class RemotePathBuilder { y1: RemoteFloat, x2: RemoteFloat, y2: RemoteFloat, - ): RemotePathBuilder = apply { _nodes.add(PathNode.QuadTo(x1.id, y1.id, x2.id, y2.id)) } + ): RemotePathBuilder = apply { + _nodes.add(PathNode.QuadTo(x1.floatId, y1.floatId, x2.floatId, y2.floatId)) + } /** * Add a quadratic Bézier by adding a [PathNode.RelativeQuadTo] to [nodes]. If no contour has @@ -265,7 +281,7 @@ public class RemotePathBuilder { dx2: RemoteFloat, dy2: RemoteFloat, ): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeQuadTo(dx1.id, dy1.id, dx2.id, dy2.id)) + _nodes.add(PathNode.RelativeQuadTo(dx1.floatId, dy1.floatId, dx2.floatId, dy2.floatId)) } /** @@ -279,7 +295,7 @@ public class RemotePathBuilder { * @param y1 The y coordinate of the end point of the quadratic curve */ public fun reflectiveQuadTo(x1: RemoteFloat, y1: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.ReflectiveQuadTo(x1.id, y1.id)) + _nodes.add(PathNode.ReflectiveQuadTo(x1.floatId, y1.floatId)) } /** @@ -295,7 +311,7 @@ public class RemotePathBuilder { */ public fun reflectiveQuadToRelative(dx1: RemoteFloat, dy1: RemoteFloat): RemotePathBuilder = apply { - _nodes.add(PathNode.RelativeReflectiveQuadTo(dx1.id, dy1.id)) + _nodes.add(PathNode.RelativeReflectiveQuadTo(dx1.floatId, dy1.floatId)) } /** @@ -336,13 +352,13 @@ public class RemotePathBuilder { ): RemotePathBuilder = apply { _nodes.add( PathNode.ArcTo( - horizontalEllipseRadius.id, - verticalEllipseRadius.id, - theta.id, + horizontalEllipseRadius.floatId, + verticalEllipseRadius.floatId, + theta.floatId, isMoreThanHalf, isPositiveArc, - x1.id, - y1.id, + x1.floatId, + y1.floatId, ) ) } @@ -385,13 +401,13 @@ public class RemotePathBuilder { ): RemotePathBuilder = apply { _nodes.add( PathNode.RelativeArcTo( - a.id, - b.id, - theta.id, + a.floatId, + b.floatId, + theta.floatId, isMoreThanHalf, isPositiveArc, - dx1.id, - dy1.id, + dx1.floatId, + dy1.floatId, ) ) } diff --git a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemoteVector.kt b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemoteVector.kt index 8d85315fb68ce..40c8d10a1cec5 100644 --- a/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemoteVector.kt +++ b/compose/remote/remote-creation-compose/src/main/java/androidx/compose/remote/creation/compose/vector/RemoteVector.kt @@ -24,6 +24,7 @@ import androidx.compose.remote.creation.compose.layout.RemoteDrawScope import androidx.compose.remote.creation.compose.layout.RemoteSize import androidx.compose.remote.creation.compose.state.RemoteColorFilter import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.compose.state.rf import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -55,8 +56,11 @@ import androidx.compose.ui.util.fastForEach /** DSL for building a vector with [RemotePathBuilder]. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public inline fun RemotePathData(block: RemotePathBuilder.() -> Unit): List = - with(RemotePathBuilder()) { +public inline fun RemotePathData( + scope: RemoteStateScope, + block: RemotePathBuilder.() -> Unit, +): List = + with(RemotePathBuilder(scope)) { block() nodes } diff --git a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/CombinedActionTest.kt b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/CombinedActionTest.kt index a1d7356645f89..28a627624f798 100644 --- a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/CombinedActionTest.kt +++ b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/CombinedActionTest.kt @@ -19,6 +19,8 @@ package androidx.compose.remote.creation.compose.action import androidx.compose.remote.creation.CreationDisplayInfo import androidx.compose.remote.creation.RemoteComposeWriter import androidx.compose.remote.creation.actions.Action as CoreAction +import androidx.compose.remote.creation.compose.capture.NoRemoteCompose +import androidx.compose.remote.creation.compose.state.RemoteStateScope import androidx.compose.remote.creation.profile.RcPlatformProfiles import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -27,26 +29,28 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class CombinedActionTest { + val testStateScope = NoRemoteCompose() + @Test fun toRemoteAction_delegatesToChildren() { val recordedWrites = mutableListOf() val action1 = object : Action { - override fun toRemoteAction(): CoreAction = CoreAction { + override fun RemoteStateScope.toRemoteAction(): CoreAction = CoreAction { recordedWrites.add("action1") } } val action2 = object : Action { - override fun toRemoteAction(): CoreAction = CoreAction { + override fun RemoteStateScope.toRemoteAction(): CoreAction = CoreAction { recordedWrites.add("action2") } } val combinedAction = CombinedAction(action1, action2) - val remoteAction = combinedAction.toRemoteAction() + val remoteAction = with(combinedAction) { testStateScope.toRemoteAction() } // Use a real writer val writer = diff --git a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/PendingIntentActionTest.kt b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/PendingIntentActionTest.kt index 87969c8ec1710..02c90f24c6c6e 100644 --- a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/PendingIntentActionTest.kt +++ b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/action/PendingIntentActionTest.kt @@ -51,9 +51,11 @@ class PendingIntentActionTest { fun toRemoteAction_withDefaultRemoteComposeWriter_throws() { val creationState = RemoteComposeCreationState(platform = AndroidxRcPlatformServices(), size = Size(1f, 1f)) - val testAction = PendingIntentAction(creationState, testPendingIntent) + val testAction = PendingIntentAction(testPendingIntent) - assertThrows(IllegalStateException::class.java) { testAction.toRemoteAction() } + assertThrows(IllegalStateException::class.java) { + with(testAction) { creationState.toRemoteAction() } + } } @Test @@ -66,8 +68,8 @@ class PendingIntentActionTest { writerEvents = writerEvents, ) - val testAction = PendingIntentAction(creationState, testPendingIntent) - val remoteAction = testAction.toRemoteAction() + val testAction = PendingIntentAction(testPendingIntent) + val remoteAction = with(testAction) { creationState.toRemoteAction() } val pendingIntents = writerEvents.pendingIntents assertThat(pendingIntents.size).isEqualTo(1) diff --git a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/state/RemoteFloatArrayTest.kt b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/state/RemoteFloatArrayTest.kt index 5110526c5c545..3f0bd7076ee1b 100644 --- a/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/state/RemoteFloatArrayTest.kt +++ b/compose/remote/remote-creation-compose/src/test/java/androidx/compose/remote/creation/compose/state/RemoteFloatArrayTest.kt @@ -53,8 +53,7 @@ class RemoteFloatArrayTest { @Test fun arrayDeref_fetchesVariableFromArray() { - val remoteFloatArray = - RemoteFloatArray(listOf(1.rf, RemoteFloat(2.rf.internalAsFloat()), 3.rf)) + val remoteFloatArray = RemoteFloatArray(listOf(1.rf, RemoteFloat(2f), 3.rf)) val result = remoteFloatArray[1.rf] val resultId = result.getIdForCreationState(creationState) @@ -67,7 +66,7 @@ class RemoteFloatArrayTest { @Test fun arrayDeref_variableIndexFetchesFromArray() { val remoteFloatArray = RemoteFloatArray(listOf(1.rf, 2.rf, 3.rf, 4.rf)) - val index = RemoteFloat(1.rf.internalAsFloat()) + val index = RemoteFloat(1f) val result = remoteFloatArray[index] val resultId = result.getIdForCreationState(creationState) diff --git a/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/capture/BlendModeTest.kt b/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/capture/BlendModeTest.kt index 3f00a1692e4d0..00f733d312405 100644 --- a/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/capture/BlendModeTest.kt +++ b/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/capture/BlendModeTest.kt @@ -18,7 +18,6 @@ package androidx.compose.remote.player.compose.creation.compose.capture import android.content.Context import android.graphics.BlendMode -import android.graphics.Color import android.graphics.Paint import android.util.Log import androidx.compose.remote.core.WireBuffer @@ -38,12 +37,15 @@ import androidx.compose.remote.creation.compose.modifier.border import androidx.compose.remote.creation.compose.modifier.padding import androidx.compose.remote.creation.compose.modifier.size import androidx.compose.remote.creation.compose.state.RemotePaint +import androidx.compose.remote.creation.compose.state.rc import androidx.compose.remote.creation.compose.state.rdp import androidx.compose.remote.creation.compose.state.rf import androidx.compose.remote.player.compose.SCREENSHOT_GOLDEN_DIRECTORY import androidx.compose.remote.player.compose.test.utils.screenshot.TargetPlayer import androidx.compose.remote.player.compose.test.utils.screenshot.rule.RemoteComposeScreenshotTestRule import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.test.core.app.ApplicationProvider @@ -122,9 +124,7 @@ class BlendModeTest { @Composable private fun RemoteBlendModeVisual(blendMode: BlendMode, name: String) { RemoteBox( - RemoteModifier.size(100.rdp) - .border(1.rdp, androidx.compose.ui.graphics.Color.Black) - .padding(8.dp), + RemoteModifier.size(100.rdp).border(1.rdp, Color.Black.rc).padding(8.dp), horizontalAlignment = RemoteAlignment.Start, verticalArrangement = RemoteArrangement.Top, ) { @@ -135,7 +135,7 @@ class BlendModeTest { val paint = RemotePaint().apply { style = Paint.Style.FILL - this.color = Color.MAGENTA + this.color = Color.Magenta.toArgb() } // Draw dst @@ -146,7 +146,7 @@ class BlendModeTest { ) // Draw src - paint.color = Color.BLUE + paint.color = Color.Blue.toArgb() paint.blendMode = blendMode drawRect( paint = paint, diff --git a/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/state/RemoteStateTest.kt b/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/state/RemoteStateTest.kt index e39190c69eb0f..4658cdb4a816c 100644 --- a/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/state/RemoteStateTest.kt +++ b/compose/remote/remote-player-compose/src/androidTest/java/androidx/compose/remote/player/compose/creation/compose/state/RemoteStateTest.kt @@ -16,7 +16,6 @@ package androidx.compose.remote.player.compose.creation.compose.state -import androidx.compose.remote.core.operations.Utils import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState import androidx.compose.remote.creation.compose.layout.RemoteColumn import androidx.compose.remote.creation.compose.layout.RemoteText @@ -56,6 +55,8 @@ class RemoteStateTest { composeTestRule.runTest { RemoteColumn(modifier = RemoteModifier.size(100.rdp)) { + val creationState = LocalRemoteComposeCreationState.current + val width = rememberRemoteFloat { componentWidth() } val configurableWidth = rememberRemoteFloat(name = "configurableWidth") { width } @@ -70,9 +71,11 @@ class RemoteStateTest { RemoteString("Configurable Width2: ") + configurableWidth2.toRemoteString(3, 0) ) - widthId = Utils.idFromNan(width.id) - configurableWidthId = Utils.idFromNan(configurableWidth.id) - configurableWidth2Id = Utils.idFromNan(configurableWidth2.id) + with(creationState) { + widthId = width.id + configurableWidthId = configurableWidth.id + configurableWidth2Id = configurableWidth2.id + } } } diff --git a/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetCaptureTest.kt b/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetCaptureTest.kt index 1263950ba6c0e..084321cc4014e 100644 --- a/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetCaptureTest.kt +++ b/glance/wear/wear/src/androidTest/java/androidx/glance/wear/parcel/WearWidgetCaptureTest.kt @@ -196,8 +196,8 @@ class WearWidgetCaptureTest { fun pendingIntentCollection_addToBundle() { val result = """ -DATA_TEXT<42> = "text-0" -DATA_TEXT<44> = "text-1" +DATA_TEXT<43> = "text-0" +DATA_TEXT<45> = "text-1" ROOT [-2:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE COLUMN [-3:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE MODIFIERS @@ -205,15 +205,15 @@ ROOT [-2:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE MODIFIERS WIDTH = 100.0 dp HEIGHT = 100.0 dp - TEXT_LAYOUT [-7:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE (42:"null") + TEXT_LAYOUT [-7:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE (43:"null") MODIFIERS CLICK_MODIFIER - HOST_NAMED_ACTION = 46 : 43 + HOST_NAMED_ACTION = 46 : 42 SEMANTICS = SEMANTICS BUTTON - TEXT_LAYOUT [-9:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE (44:"null") + TEXT_LAYOUT [-9:-1] = [0.0, 0.0, 0.0, 0.0] VISIBLE (45:"null") MODIFIERS CLICK_MODIFIER - HOST_NAMED_ACTION = 46 : 45 + HOST_NAMED_ACTION = 46 : 44 SEMANTICS = SEMANTICS BUTTON """ diff --git a/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/RemoteIconPreview.kt b/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/RemoteIconPreview.kt index 72076fd9275d3..dbd50f60a2e37 100644 --- a/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/RemoteIconPreview.kt +++ b/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/RemoteIconPreview.kt @@ -17,6 +17,7 @@ package androidx.wear.compose.remote.material3.previews +import androidx.compose.remote.creation.compose.capture.NoRemoteCompose import androidx.compose.remote.creation.compose.capture.RemoteImageVector import androidx.compose.remote.creation.compose.layout.RemoteAlignment import androidx.compose.remote.creation.compose.layout.RemoteArrangement @@ -90,15 +91,18 @@ private fun Container( ) } +val testRemoteStateScope = NoRemoteCompose() + private val VolumeUp = RemoteImageVector.Builder( + testRemoteStateScope, name = "Volume up", viewportWidth = 24.0f.rf, viewportHeight = 24.0f.rf, tintColor = RemoteColor(Color.White), ) .addPath( - RemotePathData { + RemotePathData(testRemoteStateScope) { moveTo(3.0f.rf, 9.0f.rf) verticalLineToRelative(6.0f.rf) horizontalLineToRelative(4.0f.rf) diff --git a/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/TestImageVectors.kt b/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/TestImageVectors.kt index 673b7dd1afc39..919d7f543a37e 100644 --- a/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/TestImageVectors.kt +++ b/wear/compose/remote/remote-material3/samples/src/main/java/androidx/wear/compose/remote/material3/previews/TestImageVectors.kt @@ -17,6 +17,7 @@ package androidx.wear.compose.remote.material3.previews +import androidx.compose.remote.creation.compose.capture.NoRemoteCompose import androidx.compose.remote.creation.compose.capture.RemoteImageVector import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.rf @@ -26,8 +27,11 @@ import androidx.compose.ui.graphics.SolidColor object TestImageVectors { + val testRemoteStateScope = NoRemoteCompose() + val VolumeUp = RemoteImageVector.Builder( + testRemoteStateScope, name = "Volume up", viewportWidth = 24.0f.rf, viewportHeight = 24.0f.rf, @@ -35,7 +39,7 @@ object TestImageVectors { autoMirror = true, ) .addPath( - RemotePathData { + RemotePathData(testRemoteStateScope) { moveTo(3.0f.rf, 9.0f.rf) verticalLineToRelative(6.0f.rf) horizontalLineToRelative(4.0f.rf) diff --git a/wear/compose/remote/remote-material3/src/androidTest/java/androidx/wear/compose/remote/material3/TestImageVectors.kt b/wear/compose/remote/remote-material3/src/androidTest/java/androidx/wear/compose/remote/material3/TestImageVectors.kt index 0f133db849d6e..ad0769360614d 100644 --- a/wear/compose/remote/remote-material3/src/androidTest/java/androidx/wear/compose/remote/material3/TestImageVectors.kt +++ b/wear/compose/remote/remote-material3/src/androidTest/java/androidx/wear/compose/remote/material3/TestImageVectors.kt @@ -16,6 +16,7 @@ package androidx.wear.compose.remote.material3 +import androidx.compose.remote.creation.compose.capture.NoRemoteCompose import androidx.compose.remote.creation.compose.capture.RemoteImageVector import androidx.compose.remote.creation.compose.state.RemoteColor import androidx.compose.remote.creation.compose.state.rf @@ -25,8 +26,11 @@ import androidx.compose.ui.graphics.SolidColor internal object TestImageVectors { + val testRemoteStateScope = NoRemoteCompose() + val VolumeUp = RemoteImageVector.Builder( + testRemoteStateScope, name = "Volume up", viewportWidth = 24.0f.rf, viewportHeight = 24.0f.rf, @@ -34,7 +38,7 @@ internal object TestImageVectors { autoMirror = true, ) .addPath( - RemotePathData { + RemotePathData(testRemoteStateScope) { moveTo(3.0f.rf, 9.0f.rf) verticalLineToRelative(6.0f.rf) horizontalLineToRelative(4.0f.rf) diff --git a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteButton.kt b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteButton.kt index 2f8c0d926c723..d3c32939ee806 100644 --- a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteButton.kt +++ b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteButton.kt @@ -915,7 +915,7 @@ private fun RemoteDrawScope.drawBorder( drawOutline( RemotePaint().apply { remoteColor = borderColor - strokeWidth = borderStrokeWidth.id + strokeWidth = borderStrokeWidth.floatId style = Paint.Style.STROKE } ) diff --git a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteImage.kt b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteImage.kt index aa1444288b150..3e45ee35a8243 100644 --- a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteImage.kt +++ b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteImage.kt @@ -46,10 +46,10 @@ internal class RemoteComposeImageModifier( override fun ContentDrawScope.draw() { drawIntoRemoteCanvas { canvas -> canvas.document.image( - modifier.toRemoteCompose(), + with(modifier) { canvas.toRecordingModifier() }, bitmapId, contentScale.toRemoteCompose(), - alpha.internalAsFloat(), + with(canvas) { alpha.floatId }, ) } } @@ -103,6 +103,15 @@ public fun RemoteImage( contentScale: ContentScale = ContentScale.Fit, alpha: RemoteFloat = DefaultAlpha.rf, ) { + val creationState = LocalRemoteComposeCreationState.current @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") // b/446706254 - Box(modifier = RemoteComposeImageModifier(modifier, remoteBitmap.id, contentScale, alpha)) + Box( + modifier = + RemoteComposeImageModifier( + modifier, + with(creationState) { remoteBitmap.id }, + contentScale, + alpha, + ) + ) } diff --git a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteTimeText.kt b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteTimeText.kt index c72538bc91180..292d0bcf0e68c 100644 --- a/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteTimeText.kt +++ b/wear/compose/remote/remote-material3/src/main/java/androidx/wear/compose/remote/material3/RemoteTimeText.kt @@ -133,7 +133,7 @@ private fun RemoteDrawScope.drawTimeText( val textPaint = RemotePaint().apply { - textSize = fontSize.id + textSize = fontSize.floatId typeface = fontTypeface remoteColor = textColor } From 394eb1f3e100cb97bc00eba96f0c6551ba38eecf Mon Sep 17 00:00:00 2001 From: Marc Richards Date: Tue, 13 Jan 2026 09:52:12 -0500 Subject: [PATCH 16/19] Fix disabled button in SceneCore test app for FoV test Bug: 436920446 Test: Test app fix only. Change-Id: I0efe230691a3ea533705d4ff89e3bc3cec0e278f --- .../fieldofviewvisibility/PerceivedResolutionManager.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/PerceivedResolutionManager.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/PerceivedResolutionManager.kt index 50083cdf7d1d6..48121481ec3d2 100644 --- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/PerceivedResolutionManager.kt +++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/PerceivedResolutionManager.kt @@ -182,8 +182,8 @@ class PerceivedResolutionManager( buttonDestroyPerceivedResolutionPanel.setOnClickListener { destroyPerceivedResolutionPanel() - updateButtonStates() } + updateButtonStates() } private fun updateButtonStates() { @@ -208,11 +208,13 @@ class PerceivedResolutionManager( mMovableComponent = MovableComponent.createSystemMovable(session) mPanelEntity!!.addComponent(mMovableComponent!!) } + updateButtonStates() } private fun destroyPerceivedResolutionPanel() { mPanelEntity?.dispose() mPanelEntity = null + updateButtonStates() } private fun distanceToCamera(cameraPose: ScenePose?, pose: ScenePose?): String { From d09eba2737a4a7c640f6f2baf82134d168806d4a Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Tue, 16 Dec 2025 17:03:39 -0500 Subject: [PATCH 17/19] Introduce internal `Monitor` class Relnote: N/A Test: MonitorTests Change-Id: I154601eac564c5e571b0e43c3106bf840942158f --- compose/runtime/runtime/build.gradle | 6 ++ .../runtime/platform/Synchronization.apple.kt | 19 +++++ .../runtime/platform/Synchronization.kt | 41 ++++++++++ .../compose/runtime/Synchronization.jvm.kt | 33 +++++++++ .../runtime/platform/Synchronization.linux.kt | 19 +++++ .../platform/Synchronization.mingwX64.kt | 69 +++++++++++++++++ .../platform/Synchronization.native.kt | 62 ++++++++++++++++ .../androidx/compose/runtime/MonitorTests.kt | 53 +++++++++++++ .../runtime/platform/Synchronization.unix.kt | 74 +++++++++++++++++++ .../runtime/platform/Synchronization.web.kt | 10 +++ 10 files changed, 386 insertions(+) create mode 100644 compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt create mode 100644 compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt create mode 100644 compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt create mode 100644 compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt create mode 100644 compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt create mode 100644 compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle index 8feff227072fb..1f94a4aebad99 100644 --- a/compose/runtime/runtime/build.gradle +++ b/compose/runtime/runtime/build.gradle @@ -115,6 +115,12 @@ androidXMultiplatform { wasmJsMain.dependencies { implementation(libs.kotlinXw3c) } + + unixMain.dependsOn(nativeMain) + + appleMain.dependsOn(unixMain) + + linuxMain.dependsOn(unixMain) } } diff --git a/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt b/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt new file mode 100644 index 0000000000000..7f7ad03813558 --- /dev/null +++ b/compose/runtime/runtime/src/appleMain/kotlin/androidx/compose/runtime/platform/Synchronization.apple.kt @@ -0,0 +1,19 @@ +/* + * 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.runtime.platform + +internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt index a8ef89988d187..90f807d072ef2 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/platform/Synchronization.kt @@ -28,3 +28,44 @@ internal expect inline fun makeSynchronizedObject(ref: Any? = null): Synchronize @PublishedApi @Suppress("LESS_VISIBLE_TYPE_ACCESS_IN_INLINE_WARNING") // b/446705238 internal expect inline fun synchronized(lock: SynchronizedObject, block: () -> R): R + +/** + * An implementation of the "monitor" synchronization construct. + * + * This class should only be used when [wait] and [notifyAll] are needed, because [synchronized] + * blocks that synchronize on [SynchronizedObject]s are more performant than ones that synchronize + * on [Monitor]s. + * + * Every method of this class is a no-op on Kotlin/JS and Kotlin/Wasm because they are + * single-threaded. + */ +internal expect class Monitor { + /** + * Causes the current thread to wait until another thread invokes the [notifyAll] method on this + * object. + * + * This method should only be called by a thread that is the owner of this monitor. A thread + * becomes the owner of a monitor by executing the body of a [synchronized] statement that + * synchronizes on the monitor. Calling this method relases the monitor. The monitor will be + * re-acquired before this thread resumes execution. + */ + fun wait() + + /** + * Wakes up all threads that are waiting on this monitor. + * + * This method should only be called by a thread that is the owner of this monitor. A thread + * becomes the owner of a monitor by executing the body of a [synchronized] statement that + * synchronizes on the monitor. + */ + fun notifyAll() +} + +/** + * Returns [ref] as a [Monitor] on platforms where [Any] is a valid [Monitor], or a new [Monitor] + * instance if [ref] is null or [ref] cannot be cast to [Monitor] on the current platform. + */ +internal expect inline fun makeMonitor(ref: Any? = null): Monitor + +/** Sequentially acquires [monitor], executes [block], and releases [monitor]. */ +internal expect inline fun synchronized(monitor: Monitor, block: () -> R): R diff --git a/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt b/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt new file mode 100644 index 0000000000000..8f741b459e876 --- /dev/null +++ b/compose/runtime/runtime/src/jvmAndAndroidMain/kotlin/androidx/compose/runtime/Synchronization.jvm.kt @@ -0,0 +1,33 @@ +/* + * 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.runtime.platform + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +internal actual typealias Monitor = Object + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun makeMonitor(ref: Any?) = if (ref == null) Monitor() else ref as Monitor + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return kotlin.synchronized(monitor, block) +} diff --git a/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt b/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt new file mode 100644 index 0000000000000..6cf84f07b3ba9 --- /dev/null +++ b/compose/runtime/runtime/src/linuxMain/kotlin/androidx/compose/runtime/platform/Synchronization.linux.kt @@ -0,0 +1,19 @@ +/* + * 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.runtime.platform + +internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt() diff --git a/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt b/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt new file mode 100644 index 0000000000000..a20ebef62f178 --- /dev/null +++ b/compose/runtime/runtime/src/mingwX64Main/kotlin/androidx/compose/runtime/platform/Synchronization.mingwX64.kt @@ -0,0 +1,69 @@ +/* + * 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.runtime.platform + +import kotlinx.cinterop.Arena +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.LongVarOf +import kotlinx.cinterop.UIntVarOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.ptr +import platform.posix.PTHREAD_MUTEX_RECURSIVE +import platform.posix.pthread_cond_broadcast +import platform.posix.pthread_cond_destroy +import platform.posix.pthread_cond_init +import platform.posix.pthread_cond_t +import platform.posix.pthread_cond_wait +import platform.posix.pthread_mutex_destroy +import platform.posix.pthread_mutex_init +import platform.posix.pthread_mutex_lock +import platform.posix.pthread_mutex_t +import platform.posix.pthread_mutex_unlock +import platform.posix.pthread_mutexattr_destroy +import platform.posix.pthread_mutexattr_init +import platform.posix.pthread_mutexattr_settype +import platform.posix.pthread_mutexattr_t + +@OptIn(ExperimentalForeignApi::class) +internal actual class NativeMonitor { + private val arena: Arena = Arena() + private val cond: LongVarOf = arena.alloc() + private val mutex: LongVarOf = arena.alloc() + private val attr: UIntVarOf = arena.alloc() + + init { + require(pthread_cond_init(cond.ptr, null) == 0) + require(pthread_mutexattr_init(attr.ptr) == 0) + require(pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) == 0) + require(pthread_mutex_init(mutex.ptr, attr.ptr) == 0) + } + + actual fun enter() = require(pthread_mutex_lock(mutex.ptr) == 0) + + actual fun exit() = require(pthread_mutex_unlock(mutex.ptr) == 0) + + actual fun wait() = require(pthread_cond_wait(cond.ptr, mutex.ptr) == 0) + + actual fun notifyAll() = require(pthread_cond_broadcast(cond.ptr) == 0) + + actual fun dispose() { + pthread_cond_destroy(cond.ptr) + pthread_mutex_destroy(mutex.ptr) + pthread_mutexattr_destroy(attr.ptr) + arena.clear() + } +} diff --git a/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt b/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt index b8d34c5cd9fe5..428f9e5299b22 100644 --- a/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt +++ b/compose/runtime/runtime/src/nativeMain/kotlin/androidx/compose/runtime/platform/Synchronization.native.kt @@ -19,6 +19,8 @@ package androidx.compose.runtime.platform import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.createCleaner @PublishedApi @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") @@ -34,3 +36,63 @@ internal actual inline fun synchronized(lock: SynchronizedObject, block: () contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return kotlinx.atomicfu.locks.synchronized(lock, block) } + +/** + * This class should never be used outside of this file. It is just a helper class that was added to + * minimize code duplication between the Unix and mingwX64 implementations of [Monitor]. + */ +internal expect class NativeMonitor() { + fun enter() + + fun exit() + + fun wait() + + fun notifyAll() + + fun dispose() +} + +private class MonitorWrapper { + val monitor: NativeMonitor = NativeMonitor() + + @OptIn(ExperimentalNativeApi::class) + val cleaner = createCleaner(monitor, NativeMonitor::dispose) +} + +internal actual class Monitor { + private val monitorWrapper: MonitorWrapper by lazy { MonitorWrapper() } + private val monitor: NativeMonitor + get() = monitorWrapper.monitor + + @PublishedApi + internal fun lock() { + monitor.enter() + } + + @PublishedApi + internal fun unlock() { + monitor.exit() + } + + actual fun wait() { + monitor.wait() + } + + actual fun notifyAll() { + monitor.notifyAll() + } +} + +@Suppress("NOTHING_TO_INLINE") internal actual inline fun makeMonitor(ref: Any?) = Monitor() + +internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R { + monitor.run { + lock() + return try { + block() + } finally { + unlock() + } + } +} diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt new file mode 100644 index 0000000000000..cf9c3b385ce59 --- /dev/null +++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MonitorTests.kt @@ -0,0 +1,53 @@ +/* + * 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.compose.runtime + +import androidx.compose.runtime.internal.AtomicInt +import androidx.compose.runtime.platform.makeMonitor +import androidx.compose.runtime.platform.synchronized +import kotlin.test.Test +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.test.IgnoreJsTarget +import kotlinx.test.IgnoreWasmTarget + +class MonitorTests { + @Test + @IgnoreJsTarget + @IgnoreWasmTarget + fun testWaitAndNotifyAll() = runTest { + val counter = AtomicInt(0) + val monitor = makeMonitor() + + withContext(Dispatchers.Default) { + val numJobs = 3 + repeat(numJobs) { + launch { + if (counter.add(1) == numJobs) { + synchronized(monitor) { monitor.notifyAll() } + } else { + synchronized(monitor) { monitor.wait() } + // If `wait` does not block as expected, `counter` will not be able to + // reach `numJobs` and the test will time out. + counter.add(-1) + } + } + } + } + } +} diff --git a/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt b/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.kt new file mode 100644 index 0000000000000..96fbc372e3f15 --- /dev/null +++ b/compose/runtime/runtime/src/unixMain/kotlin/androidx/compose/runtime/platform/Synchronization.unix.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.compose.runtime.platform + +import kotlinx.cinterop.Arena +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.ptr +import platform.posix.pthread_cond_broadcast +import platform.posix.pthread_cond_destroy +import platform.posix.pthread_cond_init +import platform.posix.pthread_cond_t +import platform.posix.pthread_cond_wait +import platform.posix.pthread_mutex_destroy +import platform.posix.pthread_mutex_init +import platform.posix.pthread_mutex_lock +import platform.posix.pthread_mutex_t +import platform.posix.pthread_mutex_unlock +import platform.posix.pthread_mutexattr_destroy +import platform.posix.pthread_mutexattr_init +import platform.posix.pthread_mutexattr_settype +import platform.posix.pthread_mutexattr_t + +/** + * Wrapper for `platform.posix.PTHREAD_MUTEX_RECURSIVE`, which is represented as `kotlin.Int` on + * Apple platforms and `kotlin.UInt` on linuxX64. + * + * See: [KT-41509](https://youtrack.jetbrains.com/issue/KT-41509) + */ +internal expect val PTHREAD_MUTEX_RECURSIVE: Int + +@OptIn(ExperimentalForeignApi::class) +internal actual class NativeMonitor { + private val arena: Arena = Arena() + private val cond: pthread_cond_t = arena.alloc() + private val mutex: pthread_mutex_t = arena.alloc() + private val attr: pthread_mutexattr_t = arena.alloc() + + init { + require(pthread_cond_init(cond.ptr, null) == 0) + require(pthread_mutexattr_init(attr.ptr) == 0) + require(pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) == 0) + require(pthread_mutex_init(mutex.ptr, attr.ptr) == 0) + } + + actual fun enter() = require(pthread_mutex_lock(mutex.ptr) == 0) + + actual fun exit() = require(pthread_mutex_unlock(mutex.ptr) == 0) + + actual fun wait() = require(pthread_cond_wait(cond.ptr, mutex.ptr) == 0) + + actual fun notifyAll() = require(pthread_cond_broadcast(cond.ptr) == 0) + + actual fun dispose() { + pthread_cond_destroy(cond.ptr) + pthread_mutex_destroy(mutex.ptr) + pthread_mutexattr_destroy(attr.ptr) + arena.clear() + } +} diff --git a/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt b/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt index ddc557f775e9e..39195d9d1db5b 100644 --- a/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt +++ b/compose/runtime/runtime/src/webMain/kotlin/androidx/compose/runtime/platform/Synchronization.web.kt @@ -23,3 +23,13 @@ internal actual inline fun makeSynchronizedObject(ref: Any?) = ref ?: Synchroniz @PublishedApi internal actual inline fun synchronized(lock: SynchronizedObject, block: () -> R): R = block() + +internal actual class Monitor { + actual fun wait() {} + + actual fun notifyAll() {} +} + +@Suppress("NOTHING_TO_INLINE") internal actual inline fun makeMonitor(ref: Any?) = Monitor() + +internal actual inline fun synchronized(monitor: Monitor, block: () -> R): R = block() From 4e7e5204184c3e9ac981758cf54838eed60c399a Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Thu, 30 Oct 2025 14:12:35 -0400 Subject: [PATCH 18/19] Prevent `Snapshot.sendApplyNotifications` calls from returning too early Relnote: "Ensured that `Snapshot.sendApplyNotifications` calls cannot return until the necessary apply notifications have been sent." Test: SnapshotTestsJvm.sendApplyNotificationsDoesNotReturnTooEarly Fixes: 418800424 Change-Id: I95f209dbc6c08e1a847b0fc4acde586618a7c1b3 --- .../compose/runtime/snapshots/Snapshot.kt | 117 ++++++++++++++++-- .../runtime/snapshots/SnapshotTests.kt | 81 ++++++++++++ 2 files changed, 187 insertions(+), 11 deletions(-) diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt index b7df95684aeb7..1abe5d488db58 100644 --- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt +++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.internal.JvmDefaultWithCompatibility import androidx.compose.runtime.internal.SnapshotThreadLocal import androidx.compose.runtime.internal.currentThreadId import androidx.compose.runtime.platform.SynchronizedObject +import androidx.compose.runtime.platform.makeMonitor import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.requirePrecondition @@ -301,7 +302,7 @@ public sealed class Snapshot( * changes to the global snapshot. */ public val isApplyObserverNotificationPending: Boolean - get() = pendingApplyObserverCount.get() > 0 + get() = isApplyObserverNotificationPendingImpl.get() > 0 /** * All new state objects initial state records should be [PreexistingSnapshotId] which then @@ -674,6 +675,28 @@ public sealed class Snapshot( */ public fun notifyObjectsInitialized(): Unit = currentSnapshot().notifyObjectsInitialized() + // This wrapper allows us to create flags that can be mutated from multiple threads. We use + // this class instead of [AtomicBoolean] because there are constraints forcing us to lock + // the code surrounding where the flags are accessed, meaning that [AtomicBoolean] would + // just add unnecessary overhead. + private class BooleanWrapper(var value: Boolean) + + /** + * A queue of [BooleanWrapper]s that are blocking corresponding [sendApplyNotifications] + * calls from returning until the necessary apply notifications have been sent. + */ + private var sendApplyNotificationsQueue = ArrayDeque() + private var threadWithRightToDrain: Long? = null + // A lock that must be taken before accessing [sendApplyNotificationsQueue] or + // [threadWithRightToDrain]. + private val sendApplyNotificationsQueueLock = makeSynchronizedObject() + + /** + * A monitor used to limit how often [sendApplyNotifications] calls re-inspect the values of + * the [BooleanWrapper]s in [sendApplyNotificationsQueue]. + */ + private val sendApplyNotificationsMonitor = makeMonitor() + /** * Send any pending apply notifications for state objects changed outside a snapshot. * @@ -685,8 +708,79 @@ public sealed class Snapshot( * observer registered with [registerGlobalWriteObserver]. */ public fun sendApplyNotifications() { - val changes = sync { globalSnapshot.hasPendingChanges() } - if (changes) advanceGlobalSnapshot() + val canProceed = BooleanWrapper(false) + + synchronized(sendApplyNotificationsQueueLock) { + sendApplyNotificationsQueue.add(canProceed) + if (sendApplyNotificationsQueue.size == 1) { + // A thread only gets the right to drain [sendApplyNotificationsQueue] upon + // encountering an empty queue. + threadWithRightToDrain = currentThreadId() + } + } + + if ( + synchronized(sendApplyNotificationsQueueLock) { threadWithRightToDrain } != + currentThreadId() + ) { + synchronized(sendApplyNotificationsMonitor) { + while (!canProceed.value) { + sendApplyNotificationsMonitor.wait() + } + } + } else { + val changes = sync { globalSnapshot.hasPendingChanges() } + if (changes) { + advanceGlobalSnapshot() + } + + synchronized(sendApplyNotificationsQueueLock) { + if (threadWithRightToDrain != currentThreadId()) { + // This call must return because [advanceGlobalSnapshot] led to a recursive + // [sendApplyNotifications] call that already drained the queue. + return + } + + sendApplyNotificationsQueue.removeFirst() + if (sendApplyNotificationsQueue.isEmpty()) { + // There are no other blocked threads, so we can take a fast path and skip + // the code that drains the queue. + threadWithRightToDrain = null + return + } + } + + while (true) { + val numCallsToUnblock = + synchronized(sendApplyNotificationsQueueLock) { + sendApplyNotificationsQueue.size + } + + val changes = sync { globalSnapshot.hasPendingChanges() } + if (changes) advanceGlobalSnapshot() + + synchronized(sendApplyNotificationsQueueLock) { + if (threadWithRightToDrain != currentThreadId()) { + // This call must return because [advanceGlobalSnapshot] led to a + // recursive [sendApplyNotifications] call that already drained the + // queue. + return + } + + (0 until numCallsToUnblock).forEach { _ -> + val first = sendApplyNotificationsQueue.removeFirst() + first.value = true + } + synchronized(sendApplyNotificationsMonitor) { + sendApplyNotificationsMonitor.notifyAll() + } + if (sendApplyNotificationsQueue.isEmpty()) { + threadWithRightToDrain = null + return + } + } + } + } } @InternalComposeApi public fun openSnapshotCount(): Int = openSnapshots.toList().size @@ -1527,7 +1621,10 @@ internal class GlobalSnapshot(snapshotId: SnapshotId, invalid: SnapshotIdSet) : snapshotId, invalid, null, - { state -> sync { globalWriteObservers.fastForEach { it(state) } } }, + { state -> + val observers = globalWriteObservers + observers.fastForEach { it(state) } + }, ) { @OptIn(ExperimentalComposeRuntimeApi::class) @@ -1997,11 +2094,9 @@ private fun resetGlobalSnapshotLocked( return result } -/** - * Counts the number of threads currently inside `advanceGlobalSnapshot`, notifying observers of - * changes to the global snapshot. - */ -private var pendingApplyObserverCount = AtomicInt(0) +// [advanceGlobalSnapshot] can only be called by one thread at a time, but can be called +// recursively, so this counts the number of [advanceGlobalSnapshot] calls on the callstack. +private var isApplyObserverNotificationPendingImpl = AtomicInt(0) private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { val globalSnapshot = globalSnapshot @@ -2010,7 +2105,7 @@ private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { val result = sync { modified = globalSnapshot.modified if (modified != null) { - pendingApplyObserverCount.add(1) + isApplyObserverNotificationPendingImpl.add(1) } resetGlobalSnapshotLocked(globalSnapshot, block) } @@ -2022,7 +2117,7 @@ private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { val observers = applyObservers observers.fastForEach { observer -> observer(it.wrapIntoSet(), globalSnapshot) } } finally { - pendingApplyObserverCount.add(-1) + isApplyObserverNotificationPendingImpl.add(-1) } } diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt index ba06a45ebf24e..308a3cff77e56 100644 --- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt +++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.SnapshotMutationPolicy import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.internal.AtomicBoolean import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -48,6 +49,11 @@ import kotlin.test.assertNotSame import kotlin.test.assertSame import kotlin.test.assertTrue import kotlin.test.fail +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext class SnapshotTests { @Test @@ -1472,6 +1478,81 @@ class SnapshotTests { ) } + @Test + fun recursiveSendApplyNotificationsCalls() { + val state = mutableIntStateOf(0) + + val observer1Handle = + Snapshot.registerApplyObserver { _, _ -> + if (state.intValue == 1) { + state.intValue = 2 + Snapshot.sendApplyNotifications() + if (state.intValue == 3) { + state.intValue = 4 + } + } + } + + val observer2Handle = + Snapshot.registerApplyObserver { _, _ -> + if (state.intValue == 2) { + state.intValue = 3 + } + } + + state.intValue = 1 + Snapshot.sendApplyNotifications() + + observer1Handle.dispose() + observer2Handle.dispose() + + assertEquals(4, state.intValue) + } + + @Test + fun sendApplyNotificationsDoesNotReturnTooEarly() = runTest { + (0 until 3000).forEach { _ -> + val state = mutableIntStateOf(0) + + val mainThreadIsDone = AtomicBoolean(false) + var bugWasDetected = false + val observerHandle = + Snapshot.registerApplyObserver { _, _ -> + // If [mainThreadIsDone] was already set to true before this apply observer is + // notified that [state.intValue] was changed to 1, it means that a bug caused + // [Snapshot.sendApplyNotifications] to return before the necessary apply + // notifications were sent. A detailed description of a possible manifestation + // of + // this bug is available at b/418800424. + if (state.intValue == 1 && mainThreadIsDone.get()) { + bugWasDetected = true + } + } + + val jobs = Array(3) { null } + withContext(Dispatchers.Default) { + jobs[0] = launch { + state.intValue = 1 + Snapshot.sendApplyNotifications() + mainThreadIsDone.set(true) + } + + jobs[1] = launch { Snapshot.sendApplyNotifications() } + + jobs[2] = launch { Snapshot.sendApplyNotifications() } + } + + jobs.forEach { it!!.join() } + observerHandle.dispose() + if (bugWasDetected) { + fail( + "sendApplyNotifications call returned before an appropriate set of apply " + + "notifications was sent" + ) + } + } + } + private fun usedRecords(state: StateObject): Int { var used = 0 var current: StateRecord? = state.firstStateRecord From 51e890915eee77a5519f3ff8d5c375f91f1000fc Mon Sep 17 00:00:00 2001 From: Tianming Xu Date: Wed, 7 Jan 2026 16:14:33 +0000 Subject: [PATCH 19/19] Fix rotary over-scroll animation lag in snap behavior Aligns the rotary snap over-scroll animation with the standard fling behavior to remove sluggishness. Previously, `snapToTargetItem` used a low-stiffness spring and executed a secondary settling animation even at the boundary, delaying the bounce-back. This change: 1. Uses the default (stiffer) spring spec during the first animation step when at the edge. 2. Skips the second animation step if still at the edge. This ensures `fling(0f)` is triggered immediately, allowing the `OverscrollEffect` to produce a responsive bounce-back. Bug: 474016470 Test: Manual test Relnote: "Fix rotary over-scroll animation lag in snap behavior" Change-Id: I7db89ce826fe42b2211db761d3c9f80a6eced994 --- .../foundation/rotary/RotaryScrollable.kt | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt index 1c95e1086b3c6..e25f814eef985 100644 --- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt +++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt @@ -956,7 +956,11 @@ internal class RotaryScrollHandler(private val scrollableState: ScrollableState) debugLog { "ScrollAnimation value before start: ${scrollAnimation.value}" } val animationSpec = - if (scrollableState.atTheEdge) spring(visibilityThreshold = 0.3f) else spring() + if (scrollableState.atTheEdge) { + spring(visibilityThreshold = EdgeVisibilityThreshold) + } else { + spring() + } scrollAnimation.animateTo( targetValue, @@ -1072,10 +1076,20 @@ internal class RotarySnapHandler( continueFirstScroll = false var prevPosition = anim.value + + val animationSpec = + if (scrollableState.atTheEdge) { + spring(visibilityThreshold = EdgeVisibilityThreshold) + } else { + spring( + stiffness = defaultStiffness, + visibilityThreshold = SnapVisibilityThreshold, + ) + } + anim.animateTo( prevPosition + expectedDistance, - animationSpec = - spring(stiffness = defaultStiffness, visibilityThreshold = 0.1f), + animationSpec = animationSpec, sequentialAnimation = (anim.velocity != 0f), ) { // Exit animation if snap target was updated @@ -1108,15 +1122,22 @@ internal class RotarySnapHandler( } } } - // Exit animation if snap target was updated - if (snapTargetUpdated) continue + + // Exit animation if snap target was updated. + // If we are at the edge, we also skip the second part of the animation. + // This allows us to immediately trigger fling(0f), handing control to the + // OverscrollEffect for a smooth bounce-back. + if (snapTargetUpdated || scrollableState.atTheEdge) continue // Second part of Animation - animating to the centre of target element. var prevPosition = anim.value anim.animateTo( prevPosition + expectedDistance, animationSpec = - SpringSpec(stiffness = defaultStiffness, visibilityThreshold = 0.1f), + SpringSpec( + stiffness = defaultStiffness, + visibilityThreshold = SnapVisibilityThreshold, + ), sequentialAnimation = (anim.velocity != 0f), ) { // Exit animation if snap target was updated @@ -1848,6 +1869,9 @@ private const val AxisScroll = MotionEvent.AXIS_SCROLL private const val RotaryInputSource = InputDevice.SOURCE_ROTARY_ENCODER +private const val EdgeVisibilityThreshold = 0.3f +private const val SnapVisibilityThreshold = 0.1f + /** Debug logging that can be enabled. */ private const val DEBUG = false