From 5f2d168cd4d4980b47db9fd20945815397e59c01 Mon Sep 17 00:00:00 2001 From: kseniia Date: Tue, 19 May 2026 17:46:38 +0100 Subject: [PATCH 1/7] Enable lookahead visualization in ui-tooling Test: ComposeViewAdapterTest Bug: 512395908 Change-Id: Ia62db452cf7a62c758034e28440d9e5a9421667a --- compose/ui/ui-tooling/lint-baseline.xml | 11 ++- .../ui/tooling/ComposeViewAdapterTest.kt | 59 +++++++++++++- .../ui/tooling/SharedTransitionPreview.kt | 79 +++++++++++++++++++ .../ui/tooling/ComposeViewAdapter.android.kt | 51 +++++++++++- 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 compose/ui/ui-tooling/src/androidDeviceTest/kotlin/androidx/compose/ui/tooling/SharedTransitionPreview.kt diff --git a/compose/ui/ui-tooling/lint-baseline.xml b/compose/ui/ui-tooling/lint-baseline.xml index b7899ecc85ada..45c7cbdfe3b47 100644 --- a/compose/ui/ui-tooling/lint-baseline.xml +++ b/compose/ui/ui-tooling/lint-baseline.xml @@ -1,5 +1,5 @@ - + + + + + ? = null, + lookaheadAnimationVisualDebuggingEnabled: Boolean = false, + lookaheadAnimationVisualDebuggingKeyLabelEnabled: Boolean = false, ): List { - initAndWaitForDraw(className, methodName, previewWrapperProvider = previewWrapperProvider) + initAndWaitForDraw( + className, + methodName, + previewWrapperProvider = previewWrapperProvider, + lookaheadAnimationVisualDebuggingEnabled = lookaheadAnimationVisualDebuggingEnabled, + lookaheadAnimationVisualDebuggingKeyLabelEnabled = + lookaheadAnimationVisualDebuggingKeyLabelEnabled, + ) activityTestRule.runOnUiThread { assertTrue(composeViewAdapter.viewInfos.isNotEmpty()) } return composeViewAdapter.viewInfos @@ -82,6 +91,8 @@ class ComposeViewAdapterTest { methodName: String, designInfoProvidersArgument: String? = null, previewWrapperProvider: Class? = null, + lookaheadAnimationVisualDebuggingEnabled: Boolean = false, + lookaheadAnimationVisualDebuggingKeyLabelEnabled: Boolean = false, ) { val committedAndDrawn = CountDownLatch(1) val committed = AtomicBoolean(false) @@ -99,6 +110,9 @@ class ComposeViewAdapterTest { committedAndDrawn.countDown() } }, + lookaheadAnimationVisualDebuggingEnabled = lookaheadAnimationVisualDebuggingEnabled, + lookaheadAnimationVisualDebuggingKeyLabelEnabled = + lookaheadAnimationVisualDebuggingKeyLabelEnabled, ) } @@ -110,6 +124,49 @@ class ComposeViewAdapterTest { committedAndDrawn.await() } + @Test + fun sharedTransitionWithDebuggingRendersCorrectly() { + val className = "androidx.compose.ui.tooling.SharedTransitionPreviewKt" + assertRendersCorrectly(className, "PreviewWithSharedElement") + + assertRendersCorrectly(className, "PreviewWithDebuggingEnabled") + + assertRendersCorrectly( + className, + "PreviewWithSharedElement", + lookaheadAnimationVisualDebuggingEnabled = true, + ) + + assertRendersCorrectly( + className, + "PreviewWithDebuggingEnabled", + lookaheadAnimationVisualDebuggingEnabled = true, + ) + + assertRendersCorrectly( + className, + "PreviewWithSharedElement", + lookaheadAnimationVisualDebuggingEnabled = true, + lookaheadAnimationVisualDebuggingKeyLabelEnabled = true, + ) + + assertRendersCorrectly( + className, + "PreviewWithDebuggingEnabled", + lookaheadAnimationVisualDebuggingEnabled = true, + lookaheadAnimationVisualDebuggingKeyLabelEnabled = true, + ) + } + + @Test + fun sharedTransitionRendersCorrectlyWithDebugging() { + assertRendersCorrectly( + "androidx.compose.ui.tooling.SharedTransitionPreviewKt", + "PreviewWithSharedElement", + lookaheadAnimationVisualDebuggingEnabled = true, + ) + } + @Test fun instantiateComposeViewAdapter() { val viewInfos = diff --git a/compose/ui/ui-tooling/src/androidDeviceTest/kotlin/androidx/compose/ui/tooling/SharedTransitionPreview.kt b/compose/ui/ui-tooling/src/androidDeviceTest/kotlin/androidx/compose/ui/tooling/SharedTransitionPreview.kt new file mode 100644 index 0000000000000..1f6e54474f0dd --- /dev/null +++ b/compose/ui/ui-tooling/src/androidDeviceTest/kotlin/androidx/compose/ui/tooling/SharedTransitionPreview.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.tooling + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalLookaheadAnimationVisualDebugApi +import androidx.compose.animation.LookaheadAnimationVisualDebugging +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview(widthDp = 300, heightDp = 300) +@Composable +@OptIn(ExperimentalLookaheadAnimationVisualDebugApi::class) +fun PreviewWithDebuggingEnabled() { + LookaheadAnimationVisualDebugging(isEnabled = true, isShowKeyLabelEnabled = true) { + PreviewWithSharedElement() + } +} + +@Preview(widthDp = 300, heightDp = 300) +@Composable +@OptIn(ExperimentalLookaheadAnimationVisualDebugApi::class) +fun PreviewWithSharedElement() { + var target by remember { mutableStateOf(false) } + SharedTransitionLayout { + Column { + Button(onClick = { target = !target }) { Text("Toggle State") } + AnimatedContent(targetState = target) { + if (it) { + Box( + Modifier.sharedBounds( + rememberSharedContentState("box"), + this@AnimatedContent, + ) + .size(100.dp) + .background(Color.Yellow) + ) + } else { + Box( + Modifier.sharedBounds( + rememberSharedContentState("box"), + this@AnimatedContent, + ) + .size(130.dp) + .background(Color.Green) + ) + } + } + } + } +} diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.android.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.android.kt index eaeb37765f970..bce401446755c 100644 --- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.android.kt +++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.android.kt @@ -33,6 +33,8 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.ActivityResultRegistryOwner import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.VisibleForTesting +import androidx.compose.animation.ExperimentalLookaheadAnimationVisualDebugApi +import androidx.compose.animation.LookaheadAnimationVisualDebugging import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition import androidx.compose.runtime.CompositionLocalProvider @@ -180,6 +182,12 @@ internal class ComposeViewAdapter : FrameLayout { /** Callback invoked when onDraw has been called. */ private var onDraw = {} + /** Boolean specifying whether to enable animation debugging. */ + private var lookaheadAnimationVisualDebuggingEnabled: Boolean = false + + /** Boolean specifying whether to print animated element keys */ + private var lookaheadAnimationVisualDebuggingKeyLabelEnabled: Boolean = false + private val debugBoundsPaint = Paint().apply { pathEffect = DashPathEffect(floatArrayOf(5f, 10f, 15f, 20f), 0f) @@ -408,6 +416,7 @@ internal class ComposeViewAdapter : FrameLayout { get() = this::clock.isInitialized /** Wraps a given [Preview] method an does any necessary setup. */ + @OptIn(ExperimentalLookaheadAnimationVisualDebugApi::class) @Composable private fun WrapPreview(content: @Composable () -> Unit) { // We need to replace the FontResourceLoader to avoid using ResourcesCompat. @@ -420,7 +429,14 @@ internal class ComposeViewAdapter : FrameLayout { LocalOnBackPressedDispatcherOwner provides FakeOnBackPressedDispatcherOwner, LocalActivityResultRegistryOwner provides FakeActivityResultRegistryOwner, ) { - Inspectable(slotTableRecord, content) + if (lookaheadAnimationVisualDebuggingEnabled) { + LookaheadAnimationVisualDebugging( + isEnabled = true, + isShowKeyLabelEnabled = lookaheadAnimationVisualDebuggingKeyLabelEnabled, + ) { + Inspectable(slotTableRecord, content) + } + } else Inspectable(slotTableRecord, content) } } @@ -496,6 +512,10 @@ internal class ComposeViewAdapter : FrameLayout { * @param debugViewInfos if true, it will generate the [ViewInfo] structures and will log it. * @param animationClockStartTime if positive, [clock] will be defined and will control the * animations defined in the context of the `@Composable` being previewed. + * @param lookaheadAnimationVisualDebuggingEnabled Boolean specifying whether to enable + * animation debugging. + * @param lookaheadAnimationVisualDebuggingKeyLabelEnabled Boolean specifying whether to print + * animated element keys * @param lookForDesignInfoProviders if true, it will try to populate [designInfoList]. * @param designInfoProvidersArgument String to use as an argument when populating * [designInfoList]. @@ -514,6 +534,8 @@ internal class ComposeViewAdapter : FrameLayout { debugPaintBounds: Boolean = false, debugViewInfos: Boolean = false, animationClockStartTime: Long = -1, + lookaheadAnimationVisualDebuggingEnabled: Boolean = false, + lookaheadAnimationVisualDebuggingKeyLabelEnabled: Boolean = false, lookForDesignInfoProviders: Boolean = false, designInfoProvidersArgument: String? = null, onCommit: () -> Unit = {}, @@ -524,6 +546,9 @@ internal class ComposeViewAdapter : FrameLayout { this.composableName = methodName this.lookForDesignInfoProviders = lookForDesignInfoProviders this.designInfoProvidersArgument = designInfoProvidersArgument ?: "" + this.lookaheadAnimationVisualDebuggingEnabled = lookaheadAnimationVisualDebuggingEnabled + this.lookaheadAnimationVisualDebuggingKeyLabelEnabled = + lookaheadAnimationVisualDebuggingKeyLabelEnabled this.onDraw = onDraw previewComposition = @Composable { @@ -602,6 +627,27 @@ internal class ComposeViewAdapter : FrameLayout { -1L } + val lookaheadAnimationVisualDebuggingEnabled = + try { + attrs + .getAttributeValue(TOOLS_NS_URI, "lookaheadAnimationVisualDebuggingEnabled") + .toBoolean() + } catch (e: Exception) { + false + } + + val lookaheadAnimationVisualDebuggingKeyLabelEnabled = + try { + attrs + .getAttributeValue( + TOOLS_NS_URI, + "lookaheadAnimationVisualDebuggingKeyLabelEnabled", + ) + .toBoolean() + } catch (e: Exception) { + false + } + init( className = className, methodName = methodName, @@ -613,6 +659,9 @@ internal class ComposeViewAdapter : FrameLayout { debugViewInfos = attrs.getAttributeBooleanValue(TOOLS_NS_URI, "printViewInfos", debugViewInfos), animationClockStartTime = animationClockStartTime, + lookaheadAnimationVisualDebuggingEnabled = lookaheadAnimationVisualDebuggingEnabled, + lookaheadAnimationVisualDebuggingKeyLabelEnabled = + lookaheadAnimationVisualDebuggingKeyLabelEnabled, lookForDesignInfoProviders = attrs.getAttributeBooleanValue( TOOLS_NS_URI, From 9d5af8af15a23596b4dce7ece9558ad047646225 Mon Sep 17 00:00:00 2001 From: samratdebroy Date: Tue, 23 Jun 2026 11:24:44 -0400 Subject: [PATCH 2/7] Disable head-locked panel by default in FieldOfView test app Disables the head-locked panel by default when launching the FieldOfViewVisibilityActivity. This mitigates the issue where the panel obstructs the user's field of view on startup. The user can still manually show the panel by clicking the "Show Panel" button. Test: FOV test app BUG: 508365961 Change-Id: I966e79633533d3035350a3c7b74055deeba67003 --- .../testapp/fieldofviewvisibility/HeadLockedUIManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/HeadLockedUIManager.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/HeadLockedUIManager.kt index 6f30f91cfef6a..89d2e8c3cf143 100644 --- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/HeadLockedUIManager.kt +++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/fieldofviewvisibility/HeadLockedUIManager.kt @@ -55,7 +55,7 @@ class HeadLockedUIManager( _mEnableHeadlockFlow.value = value } - private val _mUserForwardFlow = MutableStateFlow(Pose(Vector3(0f, 0.00f, -1.3f))) + private val _mUserForwardFlow = MutableStateFlow(Pose(Vector3(0f, 0.00f, -1.4f))) private var mUserForward: Pose get() = _mUserForwardFlow.value set(value) { @@ -69,7 +69,7 @@ class HeadLockedUIManager( _sliderPositionAlphaFlow.value = value } - private val _modelIsEnabledFlow = MutableStateFlow(true) + private val _modelIsEnabledFlow = MutableStateFlow(false) private var modelIsEnabled: Boolean get() = _modelIsEnabledFlow.value set(value) { @@ -126,6 +126,7 @@ class HeadLockedUIManager( parent = mSession.scene.activitySpace, ) this.mHeadLockedPanel.parent = mSession.scene.activitySpace + this.mHeadLockedPanel.setEnabled(false) } private fun updateUIState() { From 1ff8832e9cbacb8f17beb6d4f9678f98075cee62 Mon Sep 17 00:00:00 2001 From: Barkin Unal Date: Mon, 22 Jun 2026 11:29:20 +0100 Subject: [PATCH 3/7] [AW] Add Profile#enqueuePreconnect API Preconnecting to an Preconnecting to an origin speeds up future navigation by performing DNS lookup and completing TCP/TLS handshakes ahead of time. Unlike synchronous preconnect, enqueuePreconnect defers execution until WebView startup completes without triggering immediate initialization. This change introduces the Profile#enqueuePreconnect default interface API, maps it to the boundary interface in ProfileImpl, defines the ENQUEUE_PRECONNECT feature flag, and adds instrumentation test coverage. Test: Existing and newly created tests should work as expected. Bug: 511156405 Change-Id: Ia77e4fcd7fb7eeb5078f062195a464343f4d0e62 --- .../androidx/webkit/EnqueuePreconnectTest.kt | 61 +++++++++++++++++++ .../main/java/androidx/webkit/Profile.java | 31 +++++++++- .../java/androidx/webkit/WebViewFeature.java | 8 +++ .../androidx/webkit/internal/ProfileImpl.java | 12 ++++ .../internal/WebViewFeatureInternal.java | 10 +++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/EnqueuePreconnectTest.kt diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/EnqueuePreconnectTest.kt b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/EnqueuePreconnectTest.kt new file mode 100644 index 0000000000000..a1219683f3259 --- /dev/null +++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/EnqueuePreconnectTest.kt @@ -0,0 +1,61 @@ +/* + * 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.webkit + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.webkit.test.common.WebkitUtils +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Test for [Profile.enqueuePreconnect]. */ +@MediumTest +@RunWith(AndroidJUnit4::class) +class EnqueuePreconnectTest { + private lateinit var defaultProfile: Profile + + @Before + fun setUp() { + defaultProfile = + WebkitUtils.onMainThreadSync { + ProfileStore.getInstance().getProfile(Profile.DEFAULT_PROFILE_NAME) + } + } + + @Test + fun doesNotCrash() { + WebkitUtils.checkFeature(WebViewFeature.ENQUEUE_PRECONNECT) + WebkitUtils.onMainThreadSync { defaultProfile.enqueuePreconnect(EXAMPLE_URL) } + } + + @Test + fun throws_whenFeatureDisabledOrUnsupported() { + if (!WebViewFeature.isFeatureSupported(WebViewFeature.ENQUEUE_PRECONNECT)) { + WebkitUtils.onMainThreadSync { + Assert.assertThrows(UnsupportedOperationException::class.java) { + defaultProfile.enqueuePreconnect(EXAMPLE_URL) + } + } + } + } + + companion object { + private const val EXAMPLE_URL = "https://www.example.com" + } +} diff --git a/webkit/webkit/src/main/java/androidx/webkit/Profile.java b/webkit/webkit/src/main/java/androidx/webkit/Profile.java index 2a400aa631fc5..f3d688d46a422 100644 --- a/webkit/webkit/src/main/java/androidx/webkit/Profile.java +++ b/webkit/webkit/src/main/java/androidx/webkit/Profile.java @@ -586,7 +586,7 @@ default void clearAllCustomHeaders() { } /** - * Preconnects to the given origin, this can speed up future loads. + * Preconnect to the given origin to speed up future network requests. *

* Opens a connection to the provided origin, performing DNS lookup and TCP/TLS handshakes. This * can speed up future loads to the origin which could use the open connection. The connection @@ -623,6 +623,35 @@ default void preconnect(@NonNull String url) { throw new UnsupportedOperationException("Profile#preconnect is not implemented."); } + /** + * Enqueue a network preconnect to occur once WebView has started up. + *

+ * This method acts like {@link #preconnect(String)} but doesn't trigger WebView start up. + * Instead it enqueues that action for when WebView start up occurs. + * If the WebView has already started, this method acts exactly like {@link #preconnect(String)} + *

+ * This method should only be called if + * {@link WebViewFeature#isFeatureSupported(String)} returns {@code true} for + * {@link WebViewFeature#ENQUEUE_PRECONNECT}. + * + * @param url A url containing the origin to open a connection to. + * @throws UnsupportedOperationException if the + * {@link WebViewFeature#ENQUEUE_PRECONNECT} feature is not supported. + * @see Profile#preconnect(String) + */ + @RequiresFeature(name = WebViewFeature.ENQUEUE_PRECONNECT, + enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported") + @UiThread + @ExperimentalPreconnect + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + default void enqueuePreconnect(@NonNull String url) { + // We provide a default implementation of this method so that embedders extending the + // Profile (eg, for testing) don't have their build broken by the addition of this + // method. However, throw a runtime exception if this method is actually called, as + // that's better than silently no-oping. + throw new UnsupportedOperationException("Profile#enqueuePreconnect is not implemented."); + } + /** * Denotes that the Profile#addQuicHints API surface is experimental. * It may change without warning. diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java index 6f967f84ae761..bea46edc5cc5e 100644 --- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java +++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java @@ -927,6 +927,14 @@ private WebViewFeature() { @Profile.ExperimentalPreconnect public static final String PRECONNECT = "PRECONNECT"; + /** + * Feature for {@link #isFeatureSupported(String)}. + * This feature covers {@link Profile#enqueuePreconnect(String)} + */ + @Profile.ExperimentalPreconnect + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final String ENQUEUE_PRECONNECT = "ENQUEUE_PRECONNECT"; + /** * Feature for {@link Profile#addQuicHints(Set)}. */ diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java index 9fcb63983edcf..0aec22a515ce6 100644 --- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java +++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java @@ -352,6 +352,18 @@ public void preconnect(@NonNull String url) { } } + @Override + @ExperimentalPreconnect + public void enqueuePreconnect(@NonNull String url) { + ApiFeature.NoFramework feature = WebViewFeatureInternal.ENQUEUE_PRECONNECT; + if (feature.isSupportedByWebView()) { + mProfileImpl.enqueuePreconnect(url); + } else { + throw WebViewFeatureInternal.getUnsupportedOperationException(); + } + } + + @Override @ExperimentalAddQuicHints public void addQuicHints(@NonNull Set urls) { diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java index 22f272a02d7b1..a200d15bbb2b5 100644 --- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java +++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java @@ -1002,6 +1002,16 @@ public boolean isSupportedByWebView() { new ApiFeature.NoFramework(WebViewFeature.PRECONNECT, Features.PRECONNECT); + /** + * Feature for {@link WebViewFeature#isFeatureSupported(String)}. + * This feature covers {@link Profile#enqueuePreconnect(String)} + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final ApiFeature.NoFramework ENQUEUE_PRECONNECT = + new ApiFeature.NoFramework(WebViewFeature.ENQUEUE_PRECONNECT, + Features.ENQUEUE_PRECONNECT); + + /** * Feature for {@link WebViewFeature#isFeatureSupported(String)}. * This feature covers {@link Profile#addQuicHints(Set)} From a8b0b6153706d908f4c2e2654f9d39f5a9098afd Mon Sep 17 00:00:00 2001 From: diegoperez Date: Fri, 26 Jun 2026 15:03:22 +0000 Subject: [PATCH 4/7] Handle unanchored child compositions safely During dynamic updates or animations, a subcomposition might temporarily be in a transitive state where it has a parent but is not yet anchored in the parent's slot table. In this state, findContextGroup() returns null. Previously, the tree stitching logic assumed findContextGroup() was never null and asserted this with !!, causing a NullPointerException. This change safely handles null context groups by collecting these unanchored children and falling back to stitching them to the parent's first root group, preventing the crash and keeping the nodes in the tooling tree. Fixes: 507836071 Test: CompositionDataTreeTest Change-Id: I8b648ff44b270da16ea723b8b65af83782fb11a1 --- .../tooling/data/CompositionDataTreeTest.kt | 122 ++++++++++++++++++ .../ui/tooling/data/CompositionDataTree.kt | 27 +++- 2 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 compose/ui/ui-tooling-data/src/androidHostTest/kotlin/androidx/compose/ui/tooling/data/CompositionDataTreeTest.kt diff --git a/compose/ui/ui-tooling-data/src/androidHostTest/kotlin/androidx/compose/ui/tooling/data/CompositionDataTreeTest.kt b/compose/ui/ui-tooling-data/src/androidHostTest/kotlin/androidx/compose/ui/tooling/data/CompositionDataTreeTest.kt new file mode 100644 index 0000000000000..a4aaf49b3b001 --- /dev/null +++ b/compose/ui/ui-tooling-data/src/androidHostTest/kotlin/androidx/compose/ui/tooling/data/CompositionDataTreeTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.tooling.data + +import androidx.compose.runtime.tooling.CompositionData +import androidx.compose.runtime.tooling.CompositionGroup +import androidx.compose.runtime.tooling.CompositionInstance +import kotlin.test.assertEquals +import org.junit.Test + +class CompositionDataTreeTest { + + // A fake that implements both CompositionData and CompositionInstance + private class FakeCompositionInstance( + override val parent: CompositionInstance?, + val groups: List, + private val contextGroup: CompositionGroup?, + ) : CompositionInstance, CompositionData { + + override val data: CompositionData + get() = this + + override val compositionGroups: Iterable + get() = groups + + override val isEmpty: Boolean + get() = groups.isEmpty() + + override fun findContextGroup(): CompositionGroup? = contextGroup + + override fun find(identityToFind: Any): CompositionGroup? { + return groups.firstOrNull { it.identity == identityToFind } + } + } + + private class FakeCompositionGroup( + override val key: Any = 0, + override val node: Any? = null, + override val data: Iterable = emptyList(), + override val compositionGroups: Iterable = emptyList(), + override val identity: Any? = null, + ) : CompositionGroup { + override val sourceInfo: String? = null + override val isEmpty: Boolean + get() = compositionGroups.none() + } + + private data class TestNode(val name: String, val children: List) + + @OptIn(UiToolingDataApi::class) + @Test + fun testUnanchoredChildFallbackStitching() { + // Regression test for b/507836071 + // 1. Create Parent Composition + val parentGroup = FakeCompositionGroup(key = "parent_root") + val parentInstance = + FakeCompositionInstance( + parent = null, + groups = listOf(parentGroup), + contextGroup = null, + ) + + // 2. Create Child Composition (simulating transitive/unanchored state) + val childGroup = FakeCompositionGroup(key = "child_root") + val childInstance = + FakeCompositionInstance( + parent = parentInstance, + groups = listOf(childGroup), + contextGroup = null, // This is the bug condition: findContextGroup() returns null! + ) + + // 3. Run makeTree + val compositions = setOf(parentInstance, childInstance) + + val createNode = + { + group: CompositionGroup, + _: SourceContext, + children: List, + stitched: List -> + TestNode(name = "group_${group.key}", children = children + stitched) + } + + val createResult = + { _: CompositionInstance, node: TestNode?, _: List -> + node ?: TestNode("empty_instance", emptyList()) + } + + // Under the original code, this call would crash with NullPointerException. + // Under the fixed code, it should complete successfully. + val results = + compositions.makeTree( + prepareResult = {}, + createNode = createNode, + createResult = createResult, + ) + + // 4. Assertions + assertEquals(1, results.size, "Should return exactly one root result") + val rootResult = results.first() + + assertEquals("group_parent_root", rootResult.name) + assertEquals(1, rootResult.children.size, "Parent should have exactly one child stitched") + + val childResult = rootResult.children.first() + assertEquals("group_child_root", childResult.name) + } +} diff --git a/compose/ui/ui-tooling-data/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt b/compose/ui/ui-tooling-data/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt index cf3eae6f88f4b..cf76f566ed2ce 100644 --- a/compose/ui/ui-tooling-data/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt +++ b/compose/ui/ui-tooling-data/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt @@ -94,9 +94,30 @@ private class CompositionDataTree( } val childrenToAdd = mutableMapOf>() - children - .filter { it in processedNodes } - .groupByTo(childrenToAdd, { it.findContextGroup()!! }, { processedNodes[it]!! }) + val unanchoredChildren = mutableListOf() + + // Stitch children to their corresponding anchor groups in the parent. + // During dynamic updates or animations, a subcomposition might temporarily be in a + // transitive state where it has a parent but is not yet anchored in the parent's slot table + // (i.e., findContextGroup() returns null). To prevent crashes and avoid losing + // these nodes from the tooling tree, we collect them and fallback to stitching + // them to the parent's first root group. + children.forEach { child -> + processedNodes[child]?.let { value -> + val group = child.findContextGroup() + if (group != null) { + childrenToAdd.getOrPut(group) { mutableListOf() }.add(value) + } else { + unanchoredChildren.add(value) + } + } + } + + if (unanchoredChildren.isNotEmpty()) { + compositionData.compositionGroups.firstOrNull()?.let { fallbackGroup -> + childrenToAdd.getOrPut(fallbackGroup) { mutableListOf() }.addAll(unanchoredChildren) + } + } // Now, map the current tree, stitching the children's results. // The `mapTreeWithStitching` function is an assumed extension that handles the actual From 25d4e6411fd5c2e54d6a25d050cf1cb56dfed534 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 19 Jun 2026 04:30:49 -0700 Subject: [PATCH 5/7] Revert "Revert "Reland opt in (as opposed to opt out) proto filt..." Revert submission 4123353-revert-4120153-perfetto56again-VVKHKFHKJZ Reason for revert: upgrading AGP (b/528231490) contains fix to capture ag/40491519 Reverted changes: /q/submissionid:4123353-revert-4120153-perfetto56again-VVKHKFHKJZ Change-Id: Id262d6fe35f3786cd7f765bc6b48201540bc24cc --- .../benchmark-traceprocessor/build.gradle | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/benchmark/benchmark-traceprocessor/build.gradle b/benchmark/benchmark-traceprocessor/build.gradle index 412a265215176..8749fc2fc4294 100644 --- a/benchmark/benchmark-traceprocessor/build.gradle +++ b/benchmark/benchmark-traceprocessor/build.gradle @@ -14,10 +14,10 @@ * limitations under the License. */ + import androidx.build.AndroidXConfig -import androidx.build.SoftwareType import androidx.build.PlatformIdentifier -import androidx.build.ProjectLayoutType +import androidx.build.SoftwareType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile /** @@ -73,43 +73,18 @@ wire { include 'protos/perfetto/*/*.proto' } - prune 'perfetto.protos.AndroidBatteryMetric' - prune 'perfetto.protos.AndroidBinderMetric' - prune 'perfetto.protos.AndroidCameraMetric' - prune 'perfetto.protos.AndroidCameraUnaggregatedMetric' - prune 'perfetto.protos.AndroidCpuMetric' - prune 'perfetto.protos.AndroidDisplayMetrics' - prune 'perfetto.protos.AndroidDmaHeapMetric' - prune 'perfetto.protos.AndroidDvfsMetric' - prune 'perfetto.protos.AndroidFastrpcMetric' - prune 'perfetto.protos.AndroidFrameTimelineMetric' - prune 'perfetto.protos.AndroidGpuMetric' - prune 'perfetto.protos.AndroidHwcomposerMetrics' - prune 'perfetto.protos.AndroidHwuiMetric' - prune 'perfetto.protos.AndroidIonMetric' - prune 'perfetto.protos.AndroidIrqRuntimeMetric' - prune 'perfetto.protos.AndroidJankCujMetric' - prune 'perfetto.protos.AndroidLmkMetric' - prune 'perfetto.protos.AndroidLmkReasonMetric' - prune 'perfetto.protos.AndroidMemoryMetric' - prune 'perfetto.protos.AndroidMemoryUnaggregatedMetric' - prune 'perfetto.protos.AndroidMultiuserMetric' - prune 'perfetto.protos.AndroidNetworkMetric' - prune 'perfetto.protos.AndroidPackageList' - prune 'perfetto.protos.AndroidPowerRails' - prune 'perfetto.protos.AndroidRtRuntimeMetric' - prune 'perfetto.protos.AndroidSimpleperfMetric' - prune 'perfetto.protos.AndroidSurfaceflingerMetric' - prune 'perfetto.protos.AndroidTaskNames' - prune 'perfetto.protos.AndroidTraceQualityMetric' - prune 'perfetto.protos.G2dMetrics' - prune 'perfetto.protos.JavaHeapHistogram' - prune 'perfetto.protos.JavaHeapStats' - prune 'perfetto.protos.ProcessRenderInfo' - prune 'perfetto.protos.ProfilerSmaps' - prune 'perfetto.protos.TraceAnalysisStats' - prune 'perfetto.protos.TraceMetadata' - prune 'perfetto.protos.UnsymbolizedFrames' + // Only compile the protos used directly by traceprocessor (which are a subset of those defined + // by the necessary files). As new messages from within the above protos are needed, this list + // must be expanded. Note that there's no dynamic usage of protos outside the library, since we + // can't expose these protos as API. + root 'perfetto.protos.AndroidStartupMetric' + root 'perfetto.protos.AppendTraceDataResult' + root 'perfetto.protos.ComputeMetricArgs' + root 'perfetto.protos.ComputeMetricResult' + root 'perfetto.protos.QueryArgs' + root 'perfetto.protos.QueryResult' + root 'perfetto.protos.StatusResult' + root 'perfetto.protos.TraceMetrics' } androidx { From 6789e834b1b8e7544111ab99ab6a0a94c22f4225 Mon Sep 17 00:00:00 2001 From: Nimrod Parasol Date: Sun, 14 Jun 2026 12:44:22 +0300 Subject: [PATCH 6/7] Split protocol, catalog, platform and model packages from a2ui-core module to a2ui-model and a2ui-engine. Bug: 527907617 Test: ./gradlew :a2ui:a2ui-model:test :a2ui:a2ui-engine:test :a2ui:a2ui-core:test :a2ui:a2ui-compose:test :compose:runtime:runtime-a2ui:test :a2ui:integration-tests:testapp:test Relnote: N/A Change-Id: Ibe5a72c05838c4a61d50766f56af8329c0966ae6 --- a2ui/a2ui-compose/build.gradle | 3 +- a2ui/a2ui-core/api/current.txt | 167 ------------------ a2ui/a2ui-core/api/restricted_current.txt | 167 ------------------ a2ui/a2ui-engine/api/current.txt | 51 ++++++ a2ui/a2ui-engine/api/restricted_current.txt | 51 ++++++ a2ui/a2ui-engine/build.gradle | 6 + .../a2ui/engine}/catalog/A2uiCoreCatalog.kt | 2 +- .../engine}/internal/SynchronizedObject.kt | 2 +- .../model/A2uiCoreSurfaceGroupModel.kt} | 23 ++- .../engine/model/A2uiCoreSurfaceModel.kt} | 28 +-- .../platform/A2uiCoreComponentRegistry.kt} | 8 +- .../engine}/platform/A2uiCoreDataModel.kt | 4 +- .../internal/SynchronizedObjectTest.kt | 2 +- .../model/A2uiCoreSurfaceGroupModelTest.kt} | 62 +++---- .../engine/model/A2uiCoreSurfaceModelTest.kt} | 34 ++-- a2ui/a2ui-model/api/current.txt | 116 ++++++++++++ a2ui/a2ui-model/api/restricted_current.txt | 116 ++++++++++++ a2ui/a2ui-model/build.gradle | 3 + .../protocol/A2uiClientToServerMessage.kt | 2 +- .../model}/protocol/A2uiComponentPayload.kt | 2 +- .../a2ui/model}/protocol/A2uiDataPath.kt | 2 +- .../a2ui/model}/protocol/A2uiException.kt | 2 +- .../protocol/A2uiServerToClientMessage.kt | 2 +- .../a2ui/model}/protocol/A2UiDataPathTest.kt | 7 +- .../protocol/A2UiServerToClientMessageTest.kt | 2 +- .../protocol/A2uiClientToServerMessageTest.kt | 2 +- .../protocol/A2uiComponentPayloadTest.kt | 2 +- .../a2ui/model}/protocol/A2uiExceptionTest.kt | 2 +- a2ui/integration-tests/testapp/build.gradle | 3 +- compose/runtime/runtime-a2ui/build.gradle | 4 +- .../compose/runtime/a2ui/A2uiDataModelTest.kt | 4 +- .../compose/runtime/a2ui/A2uiDataModel.kt | 6 +- 32 files changed, 450 insertions(+), 437 deletions(-) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-engine/src/main/kotlin/androidx/a2ui/engine}/catalog/A2uiCoreCatalog.kt (95%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-engine/src/main/kotlin/androidx/a2ui/engine}/internal/SynchronizedObject.kt (96%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModel.kt => a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModel.kt} (82%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceModel.kt => a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModel.kt} (87%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiComponentRegistry.kt => a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreComponentRegistry.kt} (88%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-engine/src/main/kotlin/androidx/a2ui/engine}/platform/A2uiCoreDataModel.kt (94%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-engine/src/test/kotlin/androidx/a2ui/engine}/internal/SynchronizedObjectTest.kt (98%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModelTest.kt => a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModelTest.kt} (83%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceModelTest.kt => a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModelTest.kt} (93%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-model/src/main/kotlin/androidx/a2ui/model}/protocol/A2uiClientToServerMessage.kt (99%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-model/src/main/kotlin/androidx/a2ui/model}/protocol/A2uiComponentPayload.kt (97%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-model/src/main/kotlin/androidx/a2ui/model}/protocol/A2uiDataPath.kt (98%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-model/src/main/kotlin/androidx/a2ui/model}/protocol/A2uiException.kt (98%) rename a2ui/{a2ui-core/src/main/kotlin/androidx/a2ui/core => a2ui-model/src/main/kotlin/androidx/a2ui/model}/protocol/A2uiServerToClientMessage.kt (99%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-model/src/test/kotlin/androidx/a2ui/model}/protocol/A2UiDataPathTest.kt (97%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-model/src/test/kotlin/androidx/a2ui/model}/protocol/A2UiServerToClientMessageTest.kt (99%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-model/src/test/kotlin/androidx/a2ui/model}/protocol/A2uiClientToServerMessageTest.kt (99%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-model/src/test/kotlin/androidx/a2ui/model}/protocol/A2uiComponentPayloadTest.kt (98%) rename a2ui/{a2ui-core/src/test/kotlin/androidx/a2ui/core => a2ui-model/src/test/kotlin/androidx/a2ui/model}/protocol/A2uiExceptionTest.kt (99%) diff --git a/a2ui/a2ui-compose/build.gradle b/a2ui/a2ui-compose/build.gradle index 504a909e1af30..c54331643269f 100644 --- a/a2ui/a2ui-compose/build.gradle +++ b/a2ui/a2ui-compose/build.gradle @@ -30,7 +30,8 @@ plugins { } dependencies { - api(project(":a2ui:a2ui-core")) + api(project(":a2ui:a2ui-model")) + implementation(project(":a2ui:a2ui-engine")) api("androidx.compose.runtime:runtime:1.10.0") diff --git a/a2ui/a2ui-core/api/current.txt b/a2ui/a2ui-core/api/current.txt index 6db3ad125ad0e..dc3e53a8f0c35 100644 --- a/a2ui/a2ui-core/api/current.txt +++ b/a2ui/a2ui-core/api/current.txt @@ -1,171 +1,4 @@ // Signature format: 4.0 -package androidx.a2ui.core.catalog { - - public interface A2uiCoreCatalog { - } - -} - -package androidx.a2ui.core.model { - - public final class A2uiSurfaceGroupModel { - method @InaccessibleFromKotlin public kotlinx.coroutines.flow.StateFlow> getActiveSurfaces(); - property public kotlinx.coroutines.flow.StateFlow> activeSurfaces; - } - - public final class A2uiSurfaceModel { - ctor @BytecodeOnly public A2uiSurfaceModel(String!, androidx.a2ui.core.catalog.A2uiCoreCatalog!, androidx.a2ui.core.platform.A2uiCoreDataModel!, androidx.a2ui.core.platform.A2uiComponentRegistry!, kotlin.jvm.functions.Function1!, kotlin.jvm.functions.Function1!, java.util.Map!, boolean, kotlin.jvm.functions.Function0!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiSurfaceModel(String id, androidx.a2ui.core.catalog.A2uiCoreCatalog catalog, androidx.a2ui.core.platform.A2uiCoreDataModel dataModel, androidx.a2ui.core.platform.A2uiComponentRegistry componentRegistry, kotlin.jvm.functions.Function1 onDispatchAction, kotlin.jvm.functions.Function1 onDispatchError, optional java.util.Map theme, optional boolean shouldSendDataModel, optional kotlin.jvm.functions.Function0 timeProvider); - method public void dispatchAction(String componentId, java.util.Map actionDefinition); - method public void dispatchError(String componentId, androidx.a2ui.core.protocol.A2uiException exception); - method @InaccessibleFromKotlin public androidx.a2ui.core.catalog.A2uiCoreCatalog getCatalog(); - method @InaccessibleFromKotlin public androidx.a2ui.core.platform.A2uiComponentRegistry getComponentRegistry(); - method @InaccessibleFromKotlin public androidx.a2ui.core.platform.A2uiCoreDataModel getDataModel(); - method @InaccessibleFromKotlin public String getId(); - method @InaccessibleFromKotlin public java.util.Map getTheme(); - method @InaccessibleFromKotlin public boolean shouldSendDataModel(); - property public androidx.a2ui.core.catalog.A2uiCoreCatalog catalog; - property public androidx.a2ui.core.platform.A2uiComponentRegistry componentRegistry; - property public androidx.a2ui.core.platform.A2uiCoreDataModel dataModel; - property public String id; - property public boolean shouldSendDataModel; - property public java.util.Map theme; - } - -} - -package androidx.a2ui.core.platform { - - public interface A2uiComponentRegistry { - method public void dispose(); - method public void reportError(String id, androidx.a2ui.core.protocol.A2uiException exception); - method public void update(java.util.List components); - } - - public interface A2uiCoreDataModel { - method public void dispose(); - method public operator Object? get(androidx.a2ui.core.protocol.A2uiDataPath path); - method public void update(androidx.a2ui.core.protocol.A2uiDataPath path, Object? value); - } - -} - -package androidx.a2ui.core.protocol { - - public final class A2uiClientError implements androidx.a2ui.core.protocol.A2uiClientToServerMessage { - ctor @BytecodeOnly public A2uiClientError(String!, String!, String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiClientError(String code, String surfaceId, String message, optional java.util.Map context); - method @InaccessibleFromKotlin public String getCode(); - method @InaccessibleFromKotlin public java.util.Map getContext(); - method @InaccessibleFromKotlin public String getMessage(); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public String code; - property public java.util.Map context; - property public String message; - property public String surfaceId; - } - - public sealed exhaustive interface A2uiClientToServerMessage { - } - - public final class A2uiComponentPayload { - ctor public A2uiComponentPayload(String id, String type, java.util.Map properties); - method @InaccessibleFromKotlin public String getId(); - method @InaccessibleFromKotlin public java.util.Map getProperties(); - method @InaccessibleFromKotlin public String getType(); - property public String id; - property public java.util.Map properties; - property public String type; - } - - public final class A2uiCreateSurfaceMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor @BytecodeOnly public A2uiCreateSurfaceMessage(String!, String!, java.util.Map!, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiCreateSurfaceMessage(String surfaceId, String catalogId, optional java.util.Map theme, optional boolean shouldSendDataModel); - method @InaccessibleFromKotlin public String getCatalogId(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public java.util.Map getTheme(); - method @InaccessibleFromKotlin public boolean shouldSendDataModel(); - property public String catalogId; - property public boolean shouldSendDataModel; - property public String surfaceId; - property public java.util.Map theme; - } - - public final class A2uiDataPath { - ctor public A2uiDataPath(String path); - method @InaccessibleFromKotlin public String getNormalizedPath(); - method @InaccessibleFromKotlin public String getPath(); - method @InaccessibleFromKotlin public java.util.List getSegments(); - method @InaccessibleFromKotlin public boolean isAbsolute(); - property public boolean isAbsolute; - property public String normalizedPath; - property public String path; - property public java.util.List segments; - } - - public final class A2uiDeleteSurfaceMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiDeleteSurfaceMessage(String surfaceId); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public String surfaceId; - } - - public abstract sealed exhaustive class A2uiException extends java.lang.Exception { - method @InaccessibleFromKotlin public final String getCode(); - method @InaccessibleFromKotlin public final java.util.Map getContext(); - property public final String code; - property public final java.util.Map context; - } - - public static final class A2uiException.A2uiRuntimeException extends androidx.a2ui.core.protocol.A2uiException { - ctor @BytecodeOnly public A2uiException.A2uiRuntimeException(String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiException.A2uiRuntimeException(String message, optional java.util.Map context); - } - - public static final class A2uiException.A2uiValidationException extends androidx.a2ui.core.protocol.A2uiException { - ctor public A2uiException.A2uiValidationException(String message, String path); - } - - public sealed nonexhaustive interface A2uiServerToClientMessage { - method @InaccessibleFromKotlin public String getSurfaceId(); - property public abstract String surfaceId; - } - - public final class A2uiUpdateComponentsMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiUpdateComponentsMessage(String surfaceId, java.util.List components); - method @InaccessibleFromKotlin public java.util.List getComponents(); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public java.util.List components; - property public String surfaceId; - } - - public final class A2uiUpdateDataModelMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiUpdateDataModelMessage(String surfaceId, optional String path, optional Object? value); - ctor @BytecodeOnly public A2uiUpdateDataModelMessage(String!, String!, Object!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - method @InaccessibleFromKotlin public String getPath(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public Object? getValue(); - property public String path; - property public String surfaceId; - property public Object? value; - } - - public final class A2uiUserAction implements androidx.a2ui.core.protocol.A2uiClientToServerMessage { - ctor @BytecodeOnly public A2uiUserAction(String!, String!, String!, long, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiUserAction(String type, String surfaceId, String componentId, long timestamp, optional java.util.Map context); - method @InaccessibleFromKotlin public String getComponentId(); - method @InaccessibleFromKotlin public java.util.Map getContext(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public long getTimestamp(); - method @InaccessibleFromKotlin public String getType(); - property public String componentId; - property public java.util.Map context; - property public String surfaceId; - property public long timestamp; - property public String type; - } - -} - package androidx.a2ui.core.schema { public final class A2uiAllOfSchema extends androidx.a2ui.core.schema.A2uiSchema { diff --git a/a2ui/a2ui-core/api/restricted_current.txt b/a2ui/a2ui-core/api/restricted_current.txt index 6db3ad125ad0e..dc3e53a8f0c35 100644 --- a/a2ui/a2ui-core/api/restricted_current.txt +++ b/a2ui/a2ui-core/api/restricted_current.txt @@ -1,171 +1,4 @@ // Signature format: 4.0 -package androidx.a2ui.core.catalog { - - public interface A2uiCoreCatalog { - } - -} - -package androidx.a2ui.core.model { - - public final class A2uiSurfaceGroupModel { - method @InaccessibleFromKotlin public kotlinx.coroutines.flow.StateFlow> getActiveSurfaces(); - property public kotlinx.coroutines.flow.StateFlow> activeSurfaces; - } - - public final class A2uiSurfaceModel { - ctor @BytecodeOnly public A2uiSurfaceModel(String!, androidx.a2ui.core.catalog.A2uiCoreCatalog!, androidx.a2ui.core.platform.A2uiCoreDataModel!, androidx.a2ui.core.platform.A2uiComponentRegistry!, kotlin.jvm.functions.Function1!, kotlin.jvm.functions.Function1!, java.util.Map!, boolean, kotlin.jvm.functions.Function0!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiSurfaceModel(String id, androidx.a2ui.core.catalog.A2uiCoreCatalog catalog, androidx.a2ui.core.platform.A2uiCoreDataModel dataModel, androidx.a2ui.core.platform.A2uiComponentRegistry componentRegistry, kotlin.jvm.functions.Function1 onDispatchAction, kotlin.jvm.functions.Function1 onDispatchError, optional java.util.Map theme, optional boolean shouldSendDataModel, optional kotlin.jvm.functions.Function0 timeProvider); - method public void dispatchAction(String componentId, java.util.Map actionDefinition); - method public void dispatchError(String componentId, androidx.a2ui.core.protocol.A2uiException exception); - method @InaccessibleFromKotlin public androidx.a2ui.core.catalog.A2uiCoreCatalog getCatalog(); - method @InaccessibleFromKotlin public androidx.a2ui.core.platform.A2uiComponentRegistry getComponentRegistry(); - method @InaccessibleFromKotlin public androidx.a2ui.core.platform.A2uiCoreDataModel getDataModel(); - method @InaccessibleFromKotlin public String getId(); - method @InaccessibleFromKotlin public java.util.Map getTheme(); - method @InaccessibleFromKotlin public boolean shouldSendDataModel(); - property public androidx.a2ui.core.catalog.A2uiCoreCatalog catalog; - property public androidx.a2ui.core.platform.A2uiComponentRegistry componentRegistry; - property public androidx.a2ui.core.platform.A2uiCoreDataModel dataModel; - property public String id; - property public boolean shouldSendDataModel; - property public java.util.Map theme; - } - -} - -package androidx.a2ui.core.platform { - - public interface A2uiComponentRegistry { - method public void dispose(); - method public void reportError(String id, androidx.a2ui.core.protocol.A2uiException exception); - method public void update(java.util.List components); - } - - public interface A2uiCoreDataModel { - method public void dispose(); - method public operator Object? get(androidx.a2ui.core.protocol.A2uiDataPath path); - method public void update(androidx.a2ui.core.protocol.A2uiDataPath path, Object? value); - } - -} - -package androidx.a2ui.core.protocol { - - public final class A2uiClientError implements androidx.a2ui.core.protocol.A2uiClientToServerMessage { - ctor @BytecodeOnly public A2uiClientError(String!, String!, String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiClientError(String code, String surfaceId, String message, optional java.util.Map context); - method @InaccessibleFromKotlin public String getCode(); - method @InaccessibleFromKotlin public java.util.Map getContext(); - method @InaccessibleFromKotlin public String getMessage(); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public String code; - property public java.util.Map context; - property public String message; - property public String surfaceId; - } - - public sealed exhaustive interface A2uiClientToServerMessage { - } - - public final class A2uiComponentPayload { - ctor public A2uiComponentPayload(String id, String type, java.util.Map properties); - method @InaccessibleFromKotlin public String getId(); - method @InaccessibleFromKotlin public java.util.Map getProperties(); - method @InaccessibleFromKotlin public String getType(); - property public String id; - property public java.util.Map properties; - property public String type; - } - - public final class A2uiCreateSurfaceMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor @BytecodeOnly public A2uiCreateSurfaceMessage(String!, String!, java.util.Map!, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiCreateSurfaceMessage(String surfaceId, String catalogId, optional java.util.Map theme, optional boolean shouldSendDataModel); - method @InaccessibleFromKotlin public String getCatalogId(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public java.util.Map getTheme(); - method @InaccessibleFromKotlin public boolean shouldSendDataModel(); - property public String catalogId; - property public boolean shouldSendDataModel; - property public String surfaceId; - property public java.util.Map theme; - } - - public final class A2uiDataPath { - ctor public A2uiDataPath(String path); - method @InaccessibleFromKotlin public String getNormalizedPath(); - method @InaccessibleFromKotlin public String getPath(); - method @InaccessibleFromKotlin public java.util.List getSegments(); - method @InaccessibleFromKotlin public boolean isAbsolute(); - property public boolean isAbsolute; - property public String normalizedPath; - property public String path; - property public java.util.List segments; - } - - public final class A2uiDeleteSurfaceMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiDeleteSurfaceMessage(String surfaceId); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public String surfaceId; - } - - public abstract sealed exhaustive class A2uiException extends java.lang.Exception { - method @InaccessibleFromKotlin public final String getCode(); - method @InaccessibleFromKotlin public final java.util.Map getContext(); - property public final String code; - property public final java.util.Map context; - } - - public static final class A2uiException.A2uiRuntimeException extends androidx.a2ui.core.protocol.A2uiException { - ctor @BytecodeOnly public A2uiException.A2uiRuntimeException(String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiException.A2uiRuntimeException(String message, optional java.util.Map context); - } - - public static final class A2uiException.A2uiValidationException extends androidx.a2ui.core.protocol.A2uiException { - ctor public A2uiException.A2uiValidationException(String message, String path); - } - - public sealed nonexhaustive interface A2uiServerToClientMessage { - method @InaccessibleFromKotlin public String getSurfaceId(); - property public abstract String surfaceId; - } - - public final class A2uiUpdateComponentsMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiUpdateComponentsMessage(String surfaceId, java.util.List components); - method @InaccessibleFromKotlin public java.util.List getComponents(); - method @InaccessibleFromKotlin public String getSurfaceId(); - property public java.util.List components; - property public String surfaceId; - } - - public final class A2uiUpdateDataModelMessage implements androidx.a2ui.core.protocol.A2uiServerToClientMessage { - ctor public A2uiUpdateDataModelMessage(String surfaceId, optional String path, optional Object? value); - ctor @BytecodeOnly public A2uiUpdateDataModelMessage(String!, String!, Object!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - method @InaccessibleFromKotlin public String getPath(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public Object? getValue(); - property public String path; - property public String surfaceId; - property public Object? value; - } - - public final class A2uiUserAction implements androidx.a2ui.core.protocol.A2uiClientToServerMessage { - ctor @BytecodeOnly public A2uiUserAction(String!, String!, String!, long, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); - ctor public A2uiUserAction(String type, String surfaceId, String componentId, long timestamp, optional java.util.Map context); - method @InaccessibleFromKotlin public String getComponentId(); - method @InaccessibleFromKotlin public java.util.Map getContext(); - method @InaccessibleFromKotlin public String getSurfaceId(); - method @InaccessibleFromKotlin public long getTimestamp(); - method @InaccessibleFromKotlin public String getType(); - property public String componentId; - property public java.util.Map context; - property public String surfaceId; - property public long timestamp; - property public String type; - } - -} - package androidx.a2ui.core.schema { public final class A2uiAllOfSchema extends androidx.a2ui.core.schema.A2uiSchema { diff --git a/a2ui/a2ui-engine/api/current.txt b/a2ui/a2ui-engine/api/current.txt index e6f50d0d0fd11..529673a57d864 100644 --- a/a2ui/a2ui-engine/api/current.txt +++ b/a2ui/a2ui-engine/api/current.txt @@ -1 +1,52 @@ // Signature format: 4.0 +package androidx.a2ui.engine.catalog { + + public interface A2uiCoreCatalog { + } + +} + +package androidx.a2ui.engine.model { + + public final class A2uiCoreSurfaceGroupModel { + method @InaccessibleFromKotlin public kotlinx.coroutines.flow.StateFlow> getActiveSurfaces(); + property public kotlinx.coroutines.flow.StateFlow> activeSurfaces; + } + + public final class A2uiCoreSurfaceModel { + ctor @BytecodeOnly public A2uiCoreSurfaceModel(String!, androidx.a2ui.engine.catalog.A2uiCoreCatalog!, androidx.a2ui.engine.platform.A2uiCoreDataModel!, androidx.a2ui.engine.platform.A2uiCoreComponentRegistry!, kotlin.jvm.functions.Function1!, kotlin.jvm.functions.Function1!, java.util.Map!, boolean, kotlin.jvm.functions.Function0!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiCoreSurfaceModel(String id, androidx.a2ui.engine.catalog.A2uiCoreCatalog catalog, androidx.a2ui.engine.platform.A2uiCoreDataModel dataModel, androidx.a2ui.engine.platform.A2uiCoreComponentRegistry componentRegistry, kotlin.jvm.functions.Function1 onDispatchAction, kotlin.jvm.functions.Function1 onDispatchError, optional java.util.Map theme, optional boolean shouldSendDataModel, optional kotlin.jvm.functions.Function0 timeProvider); + method public void dispatchAction(String componentId, java.util.Map actionDefinition); + method public void dispatchError(String componentId, androidx.a2ui.model.protocol.A2uiException exception); + method @InaccessibleFromKotlin public androidx.a2ui.engine.catalog.A2uiCoreCatalog getCatalog(); + method @InaccessibleFromKotlin public androidx.a2ui.engine.platform.A2uiCoreComponentRegistry getComponentRegistry(); + method @InaccessibleFromKotlin public androidx.a2ui.engine.platform.A2uiCoreDataModel getDataModel(); + method @InaccessibleFromKotlin public String getId(); + method @InaccessibleFromKotlin public java.util.Map getTheme(); + method @InaccessibleFromKotlin public boolean shouldSendDataModel(); + property public androidx.a2ui.engine.catalog.A2uiCoreCatalog catalog; + property public androidx.a2ui.engine.platform.A2uiCoreComponentRegistry componentRegistry; + property public androidx.a2ui.engine.platform.A2uiCoreDataModel dataModel; + property public String id; + property public boolean shouldSendDataModel; + property public java.util.Map theme; + } + +} + +package androidx.a2ui.engine.platform { + + public interface A2uiCoreComponentRegistry { + method public void dispose(); + method public void reportError(String id, androidx.a2ui.model.protocol.A2uiException exception); + method public void update(java.util.List components); + } + + public interface A2uiCoreDataModel { + method public void dispose(); + method public operator Object? get(androidx.a2ui.model.protocol.A2uiDataPath path); + method public void update(androidx.a2ui.model.protocol.A2uiDataPath path, Object? value); + } + +} + diff --git a/a2ui/a2ui-engine/api/restricted_current.txt b/a2ui/a2ui-engine/api/restricted_current.txt index e6f50d0d0fd11..529673a57d864 100644 --- a/a2ui/a2ui-engine/api/restricted_current.txt +++ b/a2ui/a2ui-engine/api/restricted_current.txt @@ -1 +1,52 @@ // Signature format: 4.0 +package androidx.a2ui.engine.catalog { + + public interface A2uiCoreCatalog { + } + +} + +package androidx.a2ui.engine.model { + + public final class A2uiCoreSurfaceGroupModel { + method @InaccessibleFromKotlin public kotlinx.coroutines.flow.StateFlow> getActiveSurfaces(); + property public kotlinx.coroutines.flow.StateFlow> activeSurfaces; + } + + public final class A2uiCoreSurfaceModel { + ctor @BytecodeOnly public A2uiCoreSurfaceModel(String!, androidx.a2ui.engine.catalog.A2uiCoreCatalog!, androidx.a2ui.engine.platform.A2uiCoreDataModel!, androidx.a2ui.engine.platform.A2uiCoreComponentRegistry!, kotlin.jvm.functions.Function1!, kotlin.jvm.functions.Function1!, java.util.Map!, boolean, kotlin.jvm.functions.Function0!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiCoreSurfaceModel(String id, androidx.a2ui.engine.catalog.A2uiCoreCatalog catalog, androidx.a2ui.engine.platform.A2uiCoreDataModel dataModel, androidx.a2ui.engine.platform.A2uiCoreComponentRegistry componentRegistry, kotlin.jvm.functions.Function1 onDispatchAction, kotlin.jvm.functions.Function1 onDispatchError, optional java.util.Map theme, optional boolean shouldSendDataModel, optional kotlin.jvm.functions.Function0 timeProvider); + method public void dispatchAction(String componentId, java.util.Map actionDefinition); + method public void dispatchError(String componentId, androidx.a2ui.model.protocol.A2uiException exception); + method @InaccessibleFromKotlin public androidx.a2ui.engine.catalog.A2uiCoreCatalog getCatalog(); + method @InaccessibleFromKotlin public androidx.a2ui.engine.platform.A2uiCoreComponentRegistry getComponentRegistry(); + method @InaccessibleFromKotlin public androidx.a2ui.engine.platform.A2uiCoreDataModel getDataModel(); + method @InaccessibleFromKotlin public String getId(); + method @InaccessibleFromKotlin public java.util.Map getTheme(); + method @InaccessibleFromKotlin public boolean shouldSendDataModel(); + property public androidx.a2ui.engine.catalog.A2uiCoreCatalog catalog; + property public androidx.a2ui.engine.platform.A2uiCoreComponentRegistry componentRegistry; + property public androidx.a2ui.engine.platform.A2uiCoreDataModel dataModel; + property public String id; + property public boolean shouldSendDataModel; + property public java.util.Map theme; + } + +} + +package androidx.a2ui.engine.platform { + + public interface A2uiCoreComponentRegistry { + method public void dispose(); + method public void reportError(String id, androidx.a2ui.model.protocol.A2uiException exception); + method public void update(java.util.List components); + } + + public interface A2uiCoreDataModel { + method public void dispose(); + method public operator Object? get(androidx.a2ui.model.protocol.A2uiDataPath path); + method public void update(androidx.a2ui.model.protocol.A2uiDataPath path, Object? value); + } + +} + diff --git a/a2ui/a2ui-engine/build.gradle b/a2ui/a2ui-engine/build.gradle index 72de8c2847ea5..3c45bf1075082 100644 --- a/a2ui/a2ui-engine/build.gradle +++ b/a2ui/a2ui-engine/build.gradle @@ -26,8 +26,14 @@ plugins { id("com.android.library") } dependencies { + api(project(":a2ui:a2ui-model")) api(libs.kotlinStdlib) + api(libs.kotlinCoroutinesCore) + implementation(libs.kotlinSerializationJson) testImplementation(libs.kotlinTest) + testImplementation(libs.truth) + testImplementation(libs.kotlinCoroutinesTest) + testImplementation(libs.guavaTestlib) } android { namespace = "androidx.a2ui.engine" diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/catalog/A2uiCoreCatalog.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/catalog/A2uiCoreCatalog.kt similarity index 95% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/catalog/A2uiCoreCatalog.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/catalog/A2uiCoreCatalog.kt index a9307891485e5..355cbe98cadea 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/catalog/A2uiCoreCatalog.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/catalog/A2uiCoreCatalog.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.catalog +package androidx.a2ui.engine.catalog /** TODO(nimrodp): Replace this placeholder with an actual interface defining the catalog. */ public interface A2uiCoreCatalog diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/internal/SynchronizedObject.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/internal/SynchronizedObject.kt similarity index 96% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/internal/SynchronizedObject.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/internal/SynchronizedObject.kt index 76be58475b3d8..d0c2f6239bddd 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/internal/SynchronizedObject.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/internal/SynchronizedObject.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.internal +package androidx.a2ui.engine.internal /** A [SynchronizedObject] provides a mechanism for thread coordination. */ internal class SynchronizedObject diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModel.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModel.kt similarity index 82% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModel.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModel.kt index 50a7422763503..211acffe3de25 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModel.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModel.kt @@ -14,10 +14,9 @@ * limitations under the License. */ -package androidx.a2ui.core.model +package androidx.a2ui.engine.model -import androidx.a2ui.core.internal.SynchronizedObject -import androidx.a2ui.core.internal.synchronized +import androidx.a2ui.engine.internal.SynchronizedObject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,7 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow /** * The central manager for all active surfaces on the client. * - * It exposes an observable list of active [A2uiSurfaceModel]s and acts as a registry to resolve + * It exposes an observable list of active [A2uiCoreSurfaceModel]s and acts as a registry to resolve * active surfaces by their unique ID. * * Concurrency design: This class is thread-safe for concurrent operations across different surface @@ -33,13 +32,13 @@ import kotlinx.coroutines.flow.asStateFlow * sequentialization (such as sequential actors or single-threaded queues) to ensure that operations * targeting the same surface ID are executed sequentially. */ -public class A2uiSurfaceGroupModel internal constructor() { +public class A2uiCoreSurfaceGroupModel internal constructor() { // TODO(annabelo): reconsider concurrency and disposal handling (including A2uiSurfaceModel) private val lock = SynchronizedObject() - private val _activeSurfaces = MutableStateFlow>(emptyList()) + private val _activeSurfaces = MutableStateFlow>(emptyList()) /** Exposes the currently active surfaces to the host UI framework. */ - public val activeSurfaces: StateFlow> = _activeSurfaces.asStateFlow() + public val activeSurfaces: StateFlow> = _activeSurfaces.asStateFlow() // Guarded by lock private var isDisposed = false @@ -48,9 +47,9 @@ public class A2uiSurfaceGroupModel internal constructor() { * Resolves a specific surface model by its ID. * * @param id The unique identifier of the surface. - * @return The active [A2uiSurfaceModel], or `null` if not found. + * @return The active [A2uiCoreSurfaceModel], or `null` if not found. */ - internal fun getSurface(id: String): A2uiSurfaceModel? { + internal fun getSurface(id: String): A2uiCoreSurfaceModel? { return _activeSurfaces.value.find { it.id == id } } @@ -58,11 +57,11 @@ public class A2uiSurfaceGroupModel internal constructor() { * Adds a surface model. If a surface with the same ID already exists, it will be replaced and * disposed. * - * @param surface The [A2uiSurfaceModel] to add. + * @param surface The [A2uiCoreSurfaceModel] to add. * @return `true` if the surface was successfully added, `false` otherwise (e.g., if the group * is already disposed). */ - internal fun add(surface: A2uiSurfaceModel): Boolean { + internal fun add(surface: A2uiCoreSurfaceModel): Boolean { synchronized(lock) { if (isDisposed) return false val current = _activeSurfaces.value @@ -104,7 +103,7 @@ public class A2uiSurfaceGroupModel internal constructor() { override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is A2uiSurfaceGroupModel) return false + if (other !is A2uiCoreSurfaceGroupModel) return false return _activeSurfaces.value == other._activeSurfaces.value } diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceModel.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModel.kt similarity index 87% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceModel.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModel.kt index ccb7c17f24b00..c8b006e12afd7 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/model/A2uiSurfaceModel.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModel.kt @@ -14,22 +14,22 @@ * limitations under the License. */ -package androidx.a2ui.core.model +package androidx.a2ui.engine.model -import androidx.a2ui.core.catalog.A2uiCoreCatalog -import androidx.a2ui.core.platform.A2uiComponentRegistry -import androidx.a2ui.core.platform.A2uiCoreDataModel -import androidx.a2ui.core.protocol.A2uiClientError -import androidx.a2ui.core.protocol.A2uiComponentPayload -import androidx.a2ui.core.protocol.A2uiDataPath -import androidx.a2ui.core.protocol.A2uiException -import androidx.a2ui.core.protocol.A2uiUserAction +import androidx.a2ui.engine.catalog.A2uiCoreCatalog +import androidx.a2ui.engine.platform.A2uiCoreComponentRegistry +import androidx.a2ui.engine.platform.A2uiCoreDataModel +import androidx.a2ui.model.protocol.A2uiClientError +import androidx.a2ui.model.protocol.A2uiComponentPayload +import androidx.a2ui.model.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiException +import androidx.a2ui.model.protocol.A2uiUserAction /** * The root domain model for a single active surface. * - * It acts as the owner of that surface's [A2uiCoreDataModel] and [A2uiComponentRegistry], managing - * updates to these registries and propagating user actions and validation/runtime errors. + * It acts as the owner of that surface's [A2uiCoreDataModel] and [A2uiCoreComponentRegistry], + * managing updates to these registries and propagating user actions and validation/runtime errors. * * @param id The unique identifier of this surface. * @param catalog The catalog used in this surface. @@ -42,11 +42,11 @@ import androidx.a2ui.core.protocol.A2uiUserAction * as metadata to outgoing messages to the server. * @param timeProvider Provider that returns the current epoch time in milliseconds. */ -public class A2uiSurfaceModel( +public class A2uiCoreSurfaceModel( public val id: String, public val catalog: A2uiCoreCatalog, public val dataModel: A2uiCoreDataModel, - public val componentRegistry: A2uiComponentRegistry, + public val componentRegistry: A2uiCoreComponentRegistry, private val onDispatchAction: (A2uiUserAction) -> Unit, private val onDispatchError: (A2uiClientError) -> Unit, public val theme: Map = emptyMap(), @@ -124,7 +124,7 @@ public class A2uiSurfaceModel( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is A2uiSurfaceModel) return false + if (other !is A2uiCoreSurfaceModel) return false return (id == other.id) && (catalog == other.catalog) && (dataModel == other.dataModel) && diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiComponentRegistry.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreComponentRegistry.kt similarity index 88% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiComponentRegistry.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreComponentRegistry.kt index f51dd50e6fe85..ab095f3ed7506 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiComponentRegistry.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreComponentRegistry.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package androidx.a2ui.core.platform +package androidx.a2ui.engine.platform -import androidx.a2ui.core.protocol.A2uiComponentPayload -import androidx.a2ui.core.protocol.A2uiException +import androidx.a2ui.model.protocol.A2uiComponentPayload +import androidx.a2ui.model.protocol.A2uiException /** * A registry for storing UI components. Host frameworks back this with native reactive states * (e.g., SnapshotStateMap in Compose). */ -public interface A2uiComponentRegistry { +public interface A2uiCoreComponentRegistry { /** * Inserts or replaces UI components in the registry. * diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiCoreDataModel.kt b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreDataModel.kt similarity index 94% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiCoreDataModel.kt rename to a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreDataModel.kt index f8c6507b558a5..fc0a297c04808 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/platform/A2uiCoreDataModel.kt +++ b/a2ui/a2ui-engine/src/main/kotlin/androidx/a2ui/engine/platform/A2uiCoreDataModel.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package androidx.a2ui.core.platform +package androidx.a2ui.engine.platform -import androidx.a2ui.core.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiDataPath /** A storage interface for the JSON data tree. */ public interface A2uiCoreDataModel { diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/internal/SynchronizedObjectTest.kt b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/internal/SynchronizedObjectTest.kt similarity index 98% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/internal/SynchronizedObjectTest.kt rename to a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/internal/SynchronizedObjectTest.kt index 4b959d00d83e2..714d41bd91783 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/internal/SynchronizedObjectTest.kt +++ b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/internal/SynchronizedObjectTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.internal +package androidx.a2ui.engine.internal import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModelTest.kt b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModelTest.kt similarity index 83% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModelTest.kt rename to a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModelTest.kt index d8e0824493144..4782bc50124b5 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceGroupModelTest.kt +++ b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceGroupModelTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package androidx.a2ui.core.model - -import androidx.a2ui.core.catalog.A2uiCoreCatalog -import androidx.a2ui.core.platform.A2uiComponentRegistry -import androidx.a2ui.core.platform.A2uiCoreDataModel -import androidx.a2ui.core.protocol.A2uiComponentPayload -import androidx.a2ui.core.protocol.A2uiDataPath -import androidx.a2ui.core.protocol.A2uiException +package androidx.a2ui.engine.model + +import androidx.a2ui.engine.catalog.A2uiCoreCatalog +import androidx.a2ui.engine.platform.A2uiCoreComponentRegistry +import androidx.a2ui.engine.platform.A2uiCoreDataModel +import androidx.a2ui.model.protocol.A2uiComponentPayload +import androidx.a2ui.model.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiException import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -33,7 +33,7 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 @RunWith(JUnit4::class) -class A2uiSurfaceGroupModelTest { +class A2uiCoreSurfaceGroupModelTest { private companion object { const val SURFACE_ID_1 = "surf-1" @@ -41,13 +41,13 @@ class A2uiSurfaceGroupModelTest { const val SURFACE_PREFIX = "surf" const val NON_EXISTENT_ID = "non-existent" - val emptyActionHandler: (androidx.a2ui.core.protocol.A2uiUserAction) -> Unit = {} - val emptyErrorHandler: (androidx.a2ui.core.protocol.A2uiClientError) -> Unit = {} + val emptyActionHandler: (androidx.a2ui.model.protocol.A2uiUserAction) -> Unit = {} + val emptyErrorHandler: (androidx.a2ui.model.protocol.A2uiClientError) -> Unit = {} } @Test fun activeSurfaces_initially_isEmpty() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val activeSurfaces = group.activeSurfaces.value @@ -56,7 +56,7 @@ class A2uiSurfaceGroupModelTest { @Test fun add_newSurface_addsSurfaceAndUpdatesFlow() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val surface = createTestSurface(SURFACE_ID_1) val added = group.add(surface) @@ -67,7 +67,7 @@ class A2uiSurfaceGroupModelTest { @Test fun add_existingId_replacesAndDisposesOldSurface() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val dataModel1 = TestDataModel() val registry1 = TestComponentRegistry() val surface1 = createTestSurface(SURFACE_ID_1, dataModel1, registry1) @@ -85,7 +85,7 @@ class A2uiSurfaceGroupModelTest { @Test fun addAndDelete_concurrentDifferentIds_completesWithoutCorruption() = runBlocking { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val numCoroutines = 50 val numOperationsPerCoroutine = 100 @@ -109,7 +109,7 @@ class A2uiSurfaceGroupModelTest { @Test fun add_afterDispose_returnsFalse() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val surface = createTestSurface(SURFACE_ID_1) group.dispose() @@ -121,7 +121,7 @@ class A2uiSurfaceGroupModelTest { @Test fun delete_existingId_removesAndDisposesSurface() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val dataModel = TestDataModel() val registry = TestComponentRegistry() val surface = createTestSurface(SURFACE_ID_1, dataModel, registry) @@ -137,7 +137,7 @@ class A2uiSurfaceGroupModelTest { @Test fun delete_nonExistentId_doesNothing() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() group.delete(NON_EXISTENT_ID) @@ -146,7 +146,7 @@ class A2uiSurfaceGroupModelTest { @Test fun delete_afterDispose_doesNothing() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val dataModel = TestDataModel() val registry = TestComponentRegistry() val surface = createTestSurface(SURFACE_ID_1, dataModel, registry) @@ -160,7 +160,7 @@ class A2uiSurfaceGroupModelTest { @Test fun getSurface_existingId_returnsSurface() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val surface = createTestSurface(SURFACE_ID_1) group.add(surface) @@ -171,7 +171,7 @@ class A2uiSurfaceGroupModelTest { @Test fun getSurface_nonExistentId_returnsNull() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val result = group.getSurface(NON_EXISTENT_ID) @@ -180,7 +180,7 @@ class A2uiSurfaceGroupModelTest { @Test fun dispose_activeSurfaces_clearsAndDisposesAllSurfaces() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val dataModel1 = TestDataModel() val registry1 = TestComponentRegistry() val surface1 = createTestSurface(SURFACE_ID_1, dataModel1, registry1) @@ -201,7 +201,7 @@ class A2uiSurfaceGroupModelTest { @Test fun dispose_calledMultipleTimes_isIdempotent() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val dataModel = TestDataModel() val registry = TestComponentRegistry() val surface = createTestSurface(SURFACE_ID_1, dataModel, registry) @@ -217,9 +217,9 @@ class A2uiSurfaceGroupModelTest { @Test fun equalsAndHashCode_differentInstances_behavesCorrectly() { - val group1 = A2uiSurfaceGroupModel() - val group2 = A2uiSurfaceGroupModel() - val group3 = A2uiSurfaceGroupModel() + val group1 = A2uiCoreSurfaceGroupModel() + val group2 = A2uiCoreSurfaceGroupModel() + val group3 = A2uiCoreSurfaceGroupModel() val surface = createTestSurface(SURFACE_ID_1) group1.add(surface) group2.add(surface) @@ -231,7 +231,7 @@ class A2uiSurfaceGroupModelTest { @Test fun toString_withActiveSurfaces_containsSurfaceIds() { - val group = A2uiSurfaceGroupModel() + val group = A2uiCoreSurfaceGroupModel() val surface = createTestSurface(SURFACE_ID_1) group.add(surface) @@ -243,9 +243,9 @@ class A2uiSurfaceGroupModelTest { private fun createTestSurface( id: String, dataModel: A2uiCoreDataModel = TestDataModel(), - componentRegistry: A2uiComponentRegistry = TestComponentRegistry(), - ): A2uiSurfaceModel { - return A2uiSurfaceModel( + componentRegistry: A2uiCoreComponentRegistry = TestComponentRegistry(), + ): A2uiCoreSurfaceModel { + return A2uiCoreSurfaceModel( id = id, catalog = TestCatalog(), theme = emptyMap(), @@ -277,7 +277,7 @@ class A2uiSurfaceGroupModelTest { } } - private class TestComponentRegistry : A2uiComponentRegistry { + private class TestComponentRegistry : A2uiCoreComponentRegistry { var isDisposed = false override fun update(components: List) {} diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceModelTest.kt b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModelTest.kt similarity index 93% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceModelTest.kt rename to a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModelTest.kt index 1c615d1b91080..233b57967266c 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/model/A2uiSurfaceModelTest.kt +++ b/a2ui/a2ui-engine/src/test/kotlin/androidx/a2ui/engine/model/A2uiCoreSurfaceModelTest.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package androidx.a2ui.core.model - -import androidx.a2ui.core.catalog.A2uiCoreCatalog -import androidx.a2ui.core.platform.A2uiComponentRegistry -import androidx.a2ui.core.platform.A2uiCoreDataModel -import androidx.a2ui.core.protocol.A2uiClientError -import androidx.a2ui.core.protocol.A2uiComponentPayload -import androidx.a2ui.core.protocol.A2uiDataPath -import androidx.a2ui.core.protocol.A2uiException -import androidx.a2ui.core.protocol.A2uiUserAction +package androidx.a2ui.engine.model + +import androidx.a2ui.engine.catalog.A2uiCoreCatalog +import androidx.a2ui.engine.platform.A2uiCoreComponentRegistry +import androidx.a2ui.engine.platform.A2uiCoreDataModel +import androidx.a2ui.model.protocol.A2uiClientError +import androidx.a2ui.model.protocol.A2uiComponentPayload +import androidx.a2ui.model.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiException +import androidx.a2ui.model.protocol.A2uiUserAction import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -32,7 +32,7 @@ import org.junit.runners.JUnit4 @Suppress("MoveLambdaArgumentOutOfParentheses") @RunWith(JUnit4::class) -class A2uiSurfaceModelTest { +class A2uiCoreSurfaceModelTest { private companion object { const val SURFACE_ID_1 = "surf-1" @@ -51,7 +51,7 @@ class A2uiSurfaceModelTest { val registry = TestComponentRegistry() val surface = - A2uiSurfaceModel( + A2uiCoreSurfaceModel( id = SURFACE_ID_1, catalog = catalog, theme = theme, @@ -76,7 +76,7 @@ class A2uiSurfaceModelTest { val registry = TestComponentRegistry() val surface = - A2uiSurfaceModel( + A2uiCoreSurfaceModel( id = SURFACE_ID_1, catalog = TestCatalog(), dataModel = dataModel, @@ -283,12 +283,12 @@ class A2uiSurfaceModelTest { theme: Map = emptyMap(), shouldSendDataModel: Boolean = false, dataModel: A2uiCoreDataModel = TestDataModel(), - componentRegistry: A2uiComponentRegistry = TestComponentRegistry(), + componentRegistry: A2uiCoreComponentRegistry = TestComponentRegistry(), onDispatchAction: (A2uiUserAction) -> Unit = emptyActionHandler, onDispatchError: (A2uiClientError) -> Unit = emptyErrorHandler, timeProvider: () -> Long = { 0L }, - ): A2uiSurfaceModel { - return A2uiSurfaceModel( + ): A2uiCoreSurfaceModel { + return A2uiCoreSurfaceModel( id = id, catalog = catalog, theme = theme, @@ -326,7 +326,7 @@ class A2uiSurfaceModelTest { } } - private class TestComponentRegistry : A2uiComponentRegistry { + private class TestComponentRegistry : A2uiCoreComponentRegistry { val components = mutableMapOf() val reportedErrors = mutableMapOf() var isDisposed = false diff --git a/a2ui/a2ui-model/api/current.txt b/a2ui/a2ui-model/api/current.txt index e6f50d0d0fd11..bb1aafd185291 100644 --- a/a2ui/a2ui-model/api/current.txt +++ b/a2ui/a2ui-model/api/current.txt @@ -1 +1,117 @@ // Signature format: 4.0 +package androidx.a2ui.model.protocol { + + public final class A2uiClientError implements androidx.a2ui.model.protocol.A2uiClientToServerMessage { + ctor @BytecodeOnly public A2uiClientError(String!, String!, String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiClientError(String code, String surfaceId, String message, optional java.util.Map context); + method @InaccessibleFromKotlin public String getCode(); + method @InaccessibleFromKotlin public java.util.Map getContext(); + method @InaccessibleFromKotlin public String getMessage(); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public String code; + property public java.util.Map context; + property public String message; + property public String surfaceId; + } + + public sealed exhaustive interface A2uiClientToServerMessage { + } + + public final class A2uiComponentPayload { + ctor public A2uiComponentPayload(String id, String type, java.util.Map properties); + method @InaccessibleFromKotlin public String getId(); + method @InaccessibleFromKotlin public java.util.Map getProperties(); + method @InaccessibleFromKotlin public String getType(); + property public String id; + property public java.util.Map properties; + property public String type; + } + + public final class A2uiCreateSurfaceMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor @BytecodeOnly public A2uiCreateSurfaceMessage(String!, String!, java.util.Map!, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiCreateSurfaceMessage(String surfaceId, String catalogId, optional java.util.Map theme, optional boolean shouldSendDataModel); + method @InaccessibleFromKotlin public String getCatalogId(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public java.util.Map getTheme(); + method @InaccessibleFromKotlin public boolean shouldSendDataModel(); + property public String catalogId; + property public boolean shouldSendDataModel; + property public String surfaceId; + property public java.util.Map theme; + } + + public final class A2uiDataPath { + ctor public A2uiDataPath(String path); + method @InaccessibleFromKotlin public String getNormalizedPath(); + method @InaccessibleFromKotlin public String getPath(); + method @InaccessibleFromKotlin public java.util.List getSegments(); + method @InaccessibleFromKotlin public boolean isAbsolute(); + property public boolean isAbsolute; + property public String normalizedPath; + property public String path; + property public java.util.List segments; + } + + public final class A2uiDeleteSurfaceMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiDeleteSurfaceMessage(String surfaceId); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public String surfaceId; + } + + public abstract sealed exhaustive class A2uiException extends java.lang.Exception { + method @InaccessibleFromKotlin public final String getCode(); + method @InaccessibleFromKotlin public final java.util.Map getContext(); + property public final String code; + property public final java.util.Map context; + } + + public static final class A2uiException.A2uiRuntimeException extends androidx.a2ui.model.protocol.A2uiException { + ctor @BytecodeOnly public A2uiException.A2uiRuntimeException(String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiException.A2uiRuntimeException(String message, optional java.util.Map context); + } + + public static final class A2uiException.A2uiValidationException extends androidx.a2ui.model.protocol.A2uiException { + ctor public A2uiException.A2uiValidationException(String message, String path); + } + + public sealed nonexhaustive interface A2uiServerToClientMessage { + method @InaccessibleFromKotlin public String getSurfaceId(); + property public abstract String surfaceId; + } + + public final class A2uiUpdateComponentsMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiUpdateComponentsMessage(String surfaceId, java.util.List components); + method @InaccessibleFromKotlin public java.util.List getComponents(); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public java.util.List components; + property public String surfaceId; + } + + public final class A2uiUpdateDataModelMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiUpdateDataModelMessage(String surfaceId, optional String path, optional Object? value); + ctor @BytecodeOnly public A2uiUpdateDataModelMessage(String!, String!, Object!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + method @InaccessibleFromKotlin public String getPath(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public Object? getValue(); + property public String path; + property public String surfaceId; + property public Object? value; + } + + public final class A2uiUserAction implements androidx.a2ui.model.protocol.A2uiClientToServerMessage { + ctor @BytecodeOnly public A2uiUserAction(String!, String!, String!, long, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiUserAction(String type, String surfaceId, String componentId, long timestamp, optional java.util.Map context); + method @InaccessibleFromKotlin public String getComponentId(); + method @InaccessibleFromKotlin public java.util.Map getContext(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public long getTimestamp(); + method @InaccessibleFromKotlin public String getType(); + property public String componentId; + property public java.util.Map context; + property public String surfaceId; + property public long timestamp; + property public String type; + } + +} + diff --git a/a2ui/a2ui-model/api/restricted_current.txt b/a2ui/a2ui-model/api/restricted_current.txt index e6f50d0d0fd11..bb1aafd185291 100644 --- a/a2ui/a2ui-model/api/restricted_current.txt +++ b/a2ui/a2ui-model/api/restricted_current.txt @@ -1 +1,117 @@ // Signature format: 4.0 +package androidx.a2ui.model.protocol { + + public final class A2uiClientError implements androidx.a2ui.model.protocol.A2uiClientToServerMessage { + ctor @BytecodeOnly public A2uiClientError(String!, String!, String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiClientError(String code, String surfaceId, String message, optional java.util.Map context); + method @InaccessibleFromKotlin public String getCode(); + method @InaccessibleFromKotlin public java.util.Map getContext(); + method @InaccessibleFromKotlin public String getMessage(); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public String code; + property public java.util.Map context; + property public String message; + property public String surfaceId; + } + + public sealed exhaustive interface A2uiClientToServerMessage { + } + + public final class A2uiComponentPayload { + ctor public A2uiComponentPayload(String id, String type, java.util.Map properties); + method @InaccessibleFromKotlin public String getId(); + method @InaccessibleFromKotlin public java.util.Map getProperties(); + method @InaccessibleFromKotlin public String getType(); + property public String id; + property public java.util.Map properties; + property public String type; + } + + public final class A2uiCreateSurfaceMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor @BytecodeOnly public A2uiCreateSurfaceMessage(String!, String!, java.util.Map!, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiCreateSurfaceMessage(String surfaceId, String catalogId, optional java.util.Map theme, optional boolean shouldSendDataModel); + method @InaccessibleFromKotlin public String getCatalogId(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public java.util.Map getTheme(); + method @InaccessibleFromKotlin public boolean shouldSendDataModel(); + property public String catalogId; + property public boolean shouldSendDataModel; + property public String surfaceId; + property public java.util.Map theme; + } + + public final class A2uiDataPath { + ctor public A2uiDataPath(String path); + method @InaccessibleFromKotlin public String getNormalizedPath(); + method @InaccessibleFromKotlin public String getPath(); + method @InaccessibleFromKotlin public java.util.List getSegments(); + method @InaccessibleFromKotlin public boolean isAbsolute(); + property public boolean isAbsolute; + property public String normalizedPath; + property public String path; + property public java.util.List segments; + } + + public final class A2uiDeleteSurfaceMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiDeleteSurfaceMessage(String surfaceId); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public String surfaceId; + } + + public abstract sealed exhaustive class A2uiException extends java.lang.Exception { + method @InaccessibleFromKotlin public final String getCode(); + method @InaccessibleFromKotlin public final java.util.Map getContext(); + property public final String code; + property public final java.util.Map context; + } + + public static final class A2uiException.A2uiRuntimeException extends androidx.a2ui.model.protocol.A2uiException { + ctor @BytecodeOnly public A2uiException.A2uiRuntimeException(String!, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiException.A2uiRuntimeException(String message, optional java.util.Map context); + } + + public static final class A2uiException.A2uiValidationException extends androidx.a2ui.model.protocol.A2uiException { + ctor public A2uiException.A2uiValidationException(String message, String path); + } + + public sealed nonexhaustive interface A2uiServerToClientMessage { + method @InaccessibleFromKotlin public String getSurfaceId(); + property public abstract String surfaceId; + } + + public final class A2uiUpdateComponentsMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiUpdateComponentsMessage(String surfaceId, java.util.List components); + method @InaccessibleFromKotlin public java.util.List getComponents(); + method @InaccessibleFromKotlin public String getSurfaceId(); + property public java.util.List components; + property public String surfaceId; + } + + public final class A2uiUpdateDataModelMessage implements androidx.a2ui.model.protocol.A2uiServerToClientMessage { + ctor public A2uiUpdateDataModelMessage(String surfaceId, optional String path, optional Object? value); + ctor @BytecodeOnly public A2uiUpdateDataModelMessage(String!, String!, Object!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + method @InaccessibleFromKotlin public String getPath(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public Object? getValue(); + property public String path; + property public String surfaceId; + property public Object? value; + } + + public final class A2uiUserAction implements androidx.a2ui.model.protocol.A2uiClientToServerMessage { + ctor @BytecodeOnly public A2uiUserAction(String!, String!, String!, long, java.util.Map!, int, kotlin.jvm.internal.DefaultConstructorMarker!); + ctor public A2uiUserAction(String type, String surfaceId, String componentId, long timestamp, optional java.util.Map context); + method @InaccessibleFromKotlin public String getComponentId(); + method @InaccessibleFromKotlin public java.util.Map getContext(); + method @InaccessibleFromKotlin public String getSurfaceId(); + method @InaccessibleFromKotlin public long getTimestamp(); + method @InaccessibleFromKotlin public String getType(); + property public String componentId; + property public java.util.Map context; + property public String surfaceId; + property public long timestamp; + property public String type; + } + +} + diff --git a/a2ui/a2ui-model/build.gradle b/a2ui/a2ui-model/build.gradle index ed4044c06e289..0905dc0722f0f 100644 --- a/a2ui/a2ui-model/build.gradle +++ b/a2ui/a2ui-model/build.gradle @@ -27,7 +27,10 @@ plugins { } dependencies { api(libs.kotlinStdlib) + implementation(libs.kotlinSerializationJson) testImplementation(libs.kotlinTest) + testImplementation(libs.truth) + testImplementation(libs.guavaTestlib) } android { namespace = "androidx.a2ui.model" diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessage.kt b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessage.kt similarity index 99% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessage.kt rename to a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessage.kt index 694e81786cdf5..d1bc7cde3487a 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessage.kt +++ b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import java.text.SimpleDateFormat import java.util.Date diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayload.kt b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayload.kt similarity index 97% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayload.kt rename to a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayload.kt index 060753faaa1fa..b6779480d423d 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayload.kt +++ b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayload.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol /** * Represents a single UI component's payload. diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiDataPath.kt b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiDataPath.kt similarity index 98% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiDataPath.kt rename to a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiDataPath.kt index e31d929159206..549229c61f3a0 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiDataPath.kt +++ b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiDataPath.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol /** * Parses JSON pointer strings (RFC 6901) into segments. diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiException.kt b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiException.kt similarity index 98% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiException.kt rename to a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiException.kt index 3bc718cb9f246..7f514689254a9 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiException.kt +++ b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiException.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol /** * Base class for all A2UI protocol and execution errors. diff --git a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiServerToClientMessage.kt b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiServerToClientMessage.kt similarity index 99% rename from a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiServerToClientMessage.kt rename to a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiServerToClientMessage.kt index 78dff4da48ede..01144129c6bf8 100644 --- a/a2ui/a2ui-core/src/main/kotlin/androidx/a2ui/core/protocol/A2uiServerToClientMessage.kt +++ b/a2ui/a2ui-model/src/main/kotlin/androidx/a2ui/model/protocol/A2uiServerToClientMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol /** The unified interface for all messages sent from the A2A server to the A2UI client. */ public sealed interface A2uiServerToClientMessage { diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiDataPathTest.kt b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiDataPathTest.kt similarity index 97% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiDataPathTest.kt rename to a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiDataPathTest.kt index 12eff9a083bfb..6c2adfe3903f9 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiDataPathTest.kt +++ b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiDataPathTest.kt @@ -10,10 +10,11 @@ * 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 License file. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat @@ -64,7 +65,7 @@ class A2UiDataPathTest { } @Test - fun constructor_pathWithSpacesAndSpecialCharacters_segmentsArePreserved() { + fun constructor_pathWithSpacesAndSpecialCharacters_segmentsAreCorrect() { val dataPath = A2uiDataPath("/foo bar/baz!@#") assertThat(dataPath.normalizedPath).isEqualTo("/foo bar/baz!@#") assertThat(dataPath.segments).containsExactly("foo bar", "baz!@#").inOrder() diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiServerToClientMessageTest.kt b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiServerToClientMessageTest.kt similarity index 99% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiServerToClientMessageTest.kt rename to a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiServerToClientMessageTest.kt index 2976741534763..dc30a466137b5 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2UiServerToClientMessageTest.kt +++ b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2UiServerToClientMessageTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessageTest.kt b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessageTest.kt similarity index 99% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessageTest.kt rename to a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessageTest.kt index 27e51be87f6aa..9c60a5fd82ca5 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiClientToServerMessageTest.kt +++ b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiClientToServerMessageTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayloadTest.kt b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayloadTest.kt similarity index 98% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayloadTest.kt rename to a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayloadTest.kt index 4a51d9251fd65..31177f1709850 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiComponentPayloadTest.kt +++ b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiComponentPayloadTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat diff --git a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiExceptionTest.kt b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiExceptionTest.kt similarity index 99% rename from a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiExceptionTest.kt rename to a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiExceptionTest.kt index d21267abe5022..ce810c6f1798b 100644 --- a/a2ui/a2ui-core/src/test/kotlin/androidx/a2ui/core/protocol/A2uiExceptionTest.kt +++ b/a2ui/a2ui-model/src/test/kotlin/androidx/a2ui/model/protocol/A2uiExceptionTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.a2ui.core.protocol +package androidx.a2ui.model.protocol import com.google.common.testing.EqualsTester import com.google.common.truth.Truth.assertThat diff --git a/a2ui/integration-tests/testapp/build.gradle b/a2ui/integration-tests/testapp/build.gradle index 7133edfc93c6e..0accce1411adb 100644 --- a/a2ui/integration-tests/testapp/build.gradle +++ b/a2ui/integration-tests/testapp/build.gradle @@ -20,7 +20,8 @@ plugins { } dependencies { - implementation(project(":a2ui:a2ui-core")) + implementation(project(":a2ui:a2ui-model")) + implementation(project(":a2ui:a2ui-engine")) } android { diff --git a/compose/runtime/runtime-a2ui/build.gradle b/compose/runtime/runtime-a2ui/build.gradle index aedff34565099..32d952abee405 100644 --- a/compose/runtime/runtime-a2ui/build.gradle +++ b/compose/runtime/runtime-a2ui/build.gradle @@ -31,8 +31,9 @@ plugins { dependencies { api(project(":compose:runtime:runtime")) + api(project(":a2ui:a2ui-model")) - implementation(project(":a2ui:a2ui-core")) + implementation(project(":a2ui:a2ui-engine")) implementation(project(":annotation:annotation")) androidTestImplementation(project(":compose:foundation:foundation")) @@ -42,6 +43,7 @@ dependencies { androidTestImplementation(libs.testRunner) androidTestImplementation(libs.testExtJunit) androidTestImplementation(libs.truth) + androidTestImplementation(libs.guavaTestlib) } android { diff --git a/compose/runtime/runtime-a2ui/src/androidTest/kotlin/androidx/compose/runtime/a2ui/A2uiDataModelTest.kt b/compose/runtime/runtime-a2ui/src/androidTest/kotlin/androidx/compose/runtime/a2ui/A2uiDataModelTest.kt index b601748c75477..324213f8b5bb2 100644 --- a/compose/runtime/runtime-a2ui/src/androidTest/kotlin/androidx/compose/runtime/a2ui/A2uiDataModelTest.kt +++ b/compose/runtime/runtime-a2ui/src/androidTest/kotlin/androidx/compose/runtime/a2ui/A2uiDataModelTest.kt @@ -16,8 +16,8 @@ package androidx.compose.runtime.a2ui -import androidx.a2ui.core.protocol.A2uiDataPath -import androidx.a2ui.core.protocol.A2uiException.A2uiRuntimeException +import androidx.a2ui.model.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiException.A2uiRuntimeException import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.test.ExperimentalTestApi diff --git a/compose/runtime/runtime-a2ui/src/main/kotlin/androidx/compose/runtime/a2ui/A2uiDataModel.kt b/compose/runtime/runtime-a2ui/src/main/kotlin/androidx/compose/runtime/a2ui/A2uiDataModel.kt index 94e76d30e0071..9db89ddff4fa6 100644 --- a/compose/runtime/runtime-a2ui/src/main/kotlin/androidx/compose/runtime/a2ui/A2uiDataModel.kt +++ b/compose/runtime/runtime-a2ui/src/main/kotlin/androidx/compose/runtime/a2ui/A2uiDataModel.kt @@ -16,9 +16,9 @@ package androidx.compose.runtime.a2ui -import androidx.a2ui.core.platform.A2uiCoreDataModel -import androidx.a2ui.core.protocol.A2uiDataPath -import androidx.a2ui.core.protocol.A2uiException.A2uiRuntimeException +import androidx.a2ui.engine.platform.A2uiCoreDataModel +import androidx.a2ui.model.protocol.A2uiDataPath +import androidx.a2ui.model.protocol.A2uiException.A2uiRuntimeException import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf From 874fd8423eb432d9dc32045ea7715467544b202b Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Fri, 26 Jun 2026 14:45:29 +0000 Subject: [PATCH 7/7] docs: Update run_tests skill with screenshot emulator requirements Add a dedicated screenshot / visual tests section to run_tests skill. Detail the requirement for using a Medium Phone API 35 emulator for local screenshot tests. Reference and link the global android-cli skill to help developers manage and run this emulator. Test: N/A TAG=agy CONV=fd615678-2a4f-4764-8f00-7b420328122e Change-Id: I78d227708076e253b040c3cd69c4a1f881c0fbdf --- .agents/skills/run_tests/SKILL.md | 53 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/.agents/skills/run_tests/SKILL.md b/.agents/skills/run_tests/SKILL.md index d513b4ed6a62b..22abbf8656581 100644 --- a/.agents/skills/run_tests/SKILL.md +++ b/.agents/skills/run_tests/SKILL.md @@ -13,7 +13,7 @@ If you have a specific failing test (e.g., `BasicTextFieldTest#longText_doesNotC 1. **Find the Test File**: Use code search, `find_declaration`, or `find_files` (e.g., search for `BasicTextFieldTest.kt`). 2. **Identify the Module**: The file path determines the Gradle project (e.g., `compose/foundation/foundation/src/...` belongs to `:compose:foundation:foundation`). -3. **Identify Test Type**: +3. **Identify Test Type**: * If the test is in a `test`, `androidHostTest`, or `jvmTest` folder, it is a Unit Test. * If the test is in `androidTest` or `androidDeviceTest`, it is an Instrumentation Test. 4. **Identify Module Type**: Check if the module is KMP or standard Android (see details below) by inspecting `build.gradle` for `androidXMultiplatform {` or running `./gradlew :tasks | grep "test"`. @@ -104,7 +104,7 @@ To verify a flaky test, you can run it multiple times on Firebase Test Lab using * Add the companion object for data generation: ```kotlin import org.junit.runners.Parameterized - + companion object { private const val RUNS = 100 // Adjust as needed @JvmStatic @@ -121,4 +121,51 @@ To verify a flaky test, you can run it multiple times on Firebase Test Lab using ```bash # Example for KMP (append 'releaseAndroidTest' instead of 'androidDeviceTest' for standard Android) ./gradlew :ftlOnApisandroidDeviceTest --testTimeout=1h --api 28 --api 30 --className=androidx.example.MyTest - ``` \ No newline at end of file + ``` + +## 6. Screenshot / Visual Tests + +Screenshot tests (e.g., using `AndroidXScreenshotTestRule`) compare the rendered UI against approved "golden" reference images to detect visual regressions. + +* **Emulator Requirement**: Screenshot tests must be executed on a specific emulator configuration to ensure consistent rendering. Locally, you **must** use a **Medium Phone API 35** emulator. +* **Managing the Emulator**: Use the [android-cli](https://developer.android.com/tools/agents/android-cli) skill to manage and run the emulator. + * To list available virtual devices: + ```bash + android emulator list + ``` + * To launch the required emulator: + ```bash + android emulator start + ``` +* **Running the Tests**: Once the emulator is running and connected, execute the screenshot tests as standard instrumentation tests using Gradle (see Section 4). +* **Updating Screenshot Goldens**: + If a screenshot test fails due to intentional UI changes, you must update the golden reference images in the `support-goldens` repository (which is checked out as a sibling `golden` directory to `frameworks/support`). + + 1. **Run Tests (Trigger Failure)**: Execute the screenshot tests locally on the **Medium Phone API 35** emulator. If you are introducing a new screenshot or expecting a change, the test must fail to write output files. If the test passes but you want to recreate the golden, delete the local golden image first to trigger a `MISSING_REFERENCE` failure. + 2. **Pull Test Outputs**: When a test fails, `ScreenshotTestRule` writes the actual screenshot, expected screenshot, and a mapping `.textproto` file to the device. The output directory is defined in `ScreenshotTestRule.kt` as: + ```kotlin + val deviceOutputDirectory + get() = + File( + InstrumentationRegistry.getInstrumentation().getContext().externalCacheDir, + "androidx_screenshots", + ) + ``` + To pull these output files to your workstation, run: + ```bash + adb pull /sdcard/Android/data//cache/androidx_screenshots/ + ``` + *(Note: `` is the identifier of the test APK, e.g., `androidx.compose.material.test`)* + 3. **Map and Copy to Goldens Repo**: Locate the generated `*_diffResult_goldResult.textproto` file for the failed test. It explicitly maps the actual screenshot filename on the device to its repo destination. For example: + - `image_location_test`: `"androidx.compose.material.CheckboxScreenshotTest_checkBoxTest_checked_emulator_b294d33ecd4f4764_actual.png"` + - `image_location_golden`: `"compose/material/material/checkbox_checked_emulator.png"` + + Rename the pulled `*_actual.png` image to match the filename in `image_location_golden` (e.g., `checkbox_checked_emulator.png`) and copy it to its location in the sibling `golden` project repository (e.g., `../../golden/compose/material/material/`). + 4. **Submit Linked CLs via Shared Topic**: + To submit your code changes and golden image updates together (ensuring presubmit runs them as a unit), upload them to Gerrit using a **shared topic**: + - Code CL (under `frameworks/support`): `repo upload --cbr -t ` + - Golden CL (under `golden/`): `repo upload --cbr -t ` + + > [!NOTE] + > Some modules may use custom wrapper rules (such as `RemoteScreenshotTestRule` in `wear/compose/remote/remote-material3`). These custom rules delegate to the same core `ScreenshotTestRule` library under the hood. They write their outputs to the same `androidx_screenshots` cache directory on the device and generate identical `.textproto` mapping files. +