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. + 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/benchmark/benchmark-traceprocessor/build.gradle b/benchmark/benchmark-traceprocessor/build.gradle index d41b798e4fedc..9ccf940080dba 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 /** @@ -74,43 +74,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 { 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 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 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 227a50b72103e..d35fc2a515075 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 @@ -188,6 +190,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) @@ -416,6 +424,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. @@ -429,7 +438,14 @@ internal class ComposeViewAdapter : FrameLayout { LocalNavigationEventDispatcherOwner provides FakeOnBackPressedDispatcherOwner, LocalActivityResultRegistryOwner provides FakeActivityResultRegistryOwner, ) { - Inspectable(slotTableRecord, content) + if (lookaheadAnimationVisualDebuggingEnabled) { + LookaheadAnimationVisualDebugging( + isEnabled = true, + isShowKeyLabelEnabled = lookaheadAnimationVisualDebuggingKeyLabelEnabled, + ) { + Inspectable(slotTableRecord, content) + } + } else Inspectable(slotTableRecord, content) } } @@ -505,6 +521,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]. @@ -523,6 +543,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 = {}, @@ -533,6 +555,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 { @@ -611,6 +636,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, @@ -622,6 +668,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, 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 8276e1166a1b6..268e285105585 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 4b313c14c7390..1e1005f81b2e3 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)} 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() {