Merge "Update to ToT RemoteCompose" into main
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 86d8eb1..2cd3901 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -5826,7 +5826,7 @@
                 }
                 try (ByteArrayInputStream is = new ByteArrayInputStream(bytes.get(0))) {
                     player.setDocument(new RemoteComposeDocument(is));
-                    player.addClickListener((viewId, metadata) -> {
+                    player.addIdActionListener((viewId, metadata) -> {
                         mActions.forEach(action -> {
                             if (viewId == action.mViewId
                                     && action instanceof SetOnClickResponse setOnClickResponse) {
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java b/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java
index 410e021..1bdbaa4 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java
@@ -15,157 +15,72 @@
  */
 package com.android.internal.widget.remotecompose.accessibility;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.view.accessibility.AccessibilityNodeInfo;
 
-import com.android.internal.widget.remotecompose.core.operations.layout.Component;
-import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySemantics;
-import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent;
-import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics;
-
-import java.util.List;
-
 public class AndroidPlatformSemanticNodeApplier
-        implements SemanticNodeApplier<AccessibilityNodeInfo, Component, AccessibilitySemantics> {
+        extends BaseSemanticNodeApplier<AccessibilityNodeInfo> {
 
     private static final String ROLE_DESCRIPTION_KEY = "AccessibilityNodeInfo.roleDescription";
 
     @Override
-    public void applyComponent(
-            @NonNull
-                    RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                            remoteComposeAccessibility,
-            AccessibilityNodeInfo nodeInfo,
-            Component component,
-            List<AccessibilitySemantics> semantics) {
-        if (component instanceof AccessibleComponent) {
-            applyContentDescription(
-                    ((AccessibleComponent) component).getContentDescriptionId(),
-                    nodeInfo,
-                    remoteComposeAccessibility);
-
-            applyRole(((AccessibleComponent) component).getRole(), nodeInfo);
+    protected void setClickable(AccessibilityNodeInfo nodeInfo, boolean clickable) {
+        nodeInfo.setClickable(clickable);
+        if (clickable) {
+            nodeInfo.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
+        } else {
+            nodeInfo.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
         }
+    }
 
-        applySemantics(remoteComposeAccessibility, nodeInfo, semantics);
+    @Override
+    protected void setEnabled(AccessibilityNodeInfo nodeInfo, boolean enabled) {
+        nodeInfo.setEnabled(enabled);
+    }
 
-        float[] locationInWindow = new float[2];
-        component.getLocationInWindow(locationInWindow);
-        Rect bounds =
-                new Rect(
-                        (int) locationInWindow[0],
-                        (int) locationInWindow[1],
-                        (int) (locationInWindow[0] + component.getWidth()),
-                        (int) (locationInWindow[1] + component.getHeight()));
-        //noinspection deprecation
+    @Override
+    protected CharSequence getStateDescription(AccessibilityNodeInfo nodeInfo) {
+        return nodeInfo.getStateDescription();
+    }
+
+    @Override
+    protected void setStateDescription(AccessibilityNodeInfo nodeInfo, CharSequence description) {
+        nodeInfo.setStateDescription(description);
+    }
+
+    @Override
+    protected void setRoleDescription(AccessibilityNodeInfo nodeInfo, String description) {
+        nodeInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, description);
+    }
+
+    @Override
+    protected CharSequence getText(AccessibilityNodeInfo nodeInfo) {
+        return nodeInfo.getText();
+    }
+
+    @Override
+    protected void setText(AccessibilityNodeInfo nodeInfo, CharSequence text) {
+        nodeInfo.setText(text);
+    }
+
+    @Override
+    protected CharSequence getContentDescription(AccessibilityNodeInfo nodeInfo) {
+        return nodeInfo.getContentDescription();
+    }
+
+    @Override
+    protected void setContentDescription(AccessibilityNodeInfo nodeInfo, CharSequence description) {
+        nodeInfo.setContentDescription(description);
+    }
+
+    @Override
+    protected void setBoundsInScreen(AccessibilityNodeInfo nodeInfo, Rect bounds) {
         nodeInfo.setBoundsInParent(bounds);
         nodeInfo.setBoundsInScreen(bounds);
-
-        if (component instanceof AccessibleComponent) {
-            applyContentDescription(
-                    ((AccessibleComponent) component).getContentDescriptionId(),
-                    nodeInfo,
-                    remoteComposeAccessibility);
-
-            applyText(
-                    ((AccessibleComponent) component).getTextId(),
-                    nodeInfo,
-                    remoteComposeAccessibility);
-
-            applyRole(((AccessibleComponent) component).getRole(), nodeInfo);
-        }
-
-        applySemantics(remoteComposeAccessibility, nodeInfo, semantics);
-
-        if (nodeInfo.getText() == null && nodeInfo.getContentDescription() == null) {
-            nodeInfo.setContentDescription("");
-        }
     }
 
-    public void applySemantics(
-            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                    remoteComposeAccessibility,
-            AccessibilityNodeInfo nodeInfo,
-            List<AccessibilitySemantics> semantics) {
-        for (AccessibilitySemantics semantic : semantics) {
-            if (semantic.isInterestingForSemantics()) {
-                if (semantic instanceof CoreSemantics) {
-                    applyCoreSemantics(
-                            remoteComposeAccessibility, nodeInfo, (CoreSemantics) semantic);
-                } else if (semantic instanceof AccessibleComponent) {
-                    AccessibleComponent s = (AccessibleComponent) semantic;
-
-                    applyContentDescription(
-                            s.getContentDescriptionId(), nodeInfo, remoteComposeAccessibility);
-
-                    applyRole(s.getRole(), nodeInfo);
-
-                    applyText(s.getTextId(), nodeInfo, remoteComposeAccessibility);
-
-                    if (s.isClickable()) {
-                        nodeInfo.setClickable(true);
-                        nodeInfo.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
-                    }
-                }
-            }
-        }
-    }
-
-    private void applyCoreSemantics(
-            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                    remoteComposeAccessibility,
-            AccessibilityNodeInfo nodeInfo,
-            CoreSemantics semantics) {
-        applyContentDescription(
-                semantics.getContentDescriptionId(), nodeInfo, remoteComposeAccessibility);
-
-        applyRole(semantics.getRole(), nodeInfo);
-
-        applyText(semantics.getTextId(), nodeInfo, remoteComposeAccessibility);
-
-        applyStateDescription(
-                semantics.getStateDescriptionId(), nodeInfo, remoteComposeAccessibility);
-
-        nodeInfo.setEnabled(semantics.mEnabled);
-    }
-
-    void applyRole(@Nullable AccessibleComponent.Role role, AccessibilityNodeInfo nodeInfo) {
-        if (role != null) {
-            nodeInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, role.getDescription());
-        }
-    }
-
-    void applyContentDescription(
-            @Nullable Integer contentDescriptionId,
-            AccessibilityNodeInfo nodeInfo,
-            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                    remoteComposeAccessibility) {
-        if (contentDescriptionId != null) {
-            nodeInfo.setContentDescription(
-                    remoteComposeAccessibility.stringValue(contentDescriptionId));
-        }
-    }
-
-    void applyText(
-            @Nullable Integer textId,
-            AccessibilityNodeInfo nodeInfo,
-            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                    remoteComposeAccessibility) {
-        if (textId != null) {
-            nodeInfo.setText(remoteComposeAccessibility.stringValue(textId));
-        }
-    }
-
-    void applyStateDescription(
-            @Nullable Integer stateDescriptionId,
-            AccessibilityNodeInfo nodeInfo,
-            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
-                    remoteComposeAccessibility) {
-        if (stateDescriptionId != null) {
-            nodeInfo.setStateDescription(
-                    remoteComposeAccessibility.stringValue(stateDescriptionId));
-        }
+    @Override
+    protected void setUniqueId(AccessibilityNodeInfo nodeInfo, String id) {
+        nodeInfo.setUniqueId(id);
     }
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/BaseSemanticNodeApplier.java b/core/java/com/android/internal/widget/remotecompose/accessibility/BaseSemanticNodeApplier.java
new file mode 100644
index 0000000..228afb8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/BaseSemanticNodeApplier.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 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 com.android.internal.widget.remotecompose.accessibility;
+
+import android.graphics.Rect;
+
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySemantics;
+import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent;
+import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics;
+
+import java.util.List;
+
+/**
+ * Base class for applying semantic information to a node.
+ *
+ * <p>This class provides common functionality for applying semantic information extracted from
+ * Compose UI components to a node representation used for accessibility purposes. It handles
+ * applying properties like content description, text, role, clickability, and bounds.
+ *
+ * <p>Subclasses are responsible for implementing methods to actually set these properties on the
+ * specific node type they handle.
+ *
+ * @param <N> The type of node this applier works with.
+ */
+public abstract class BaseSemanticNodeApplier<N> implements SemanticNodeApplier<N> {
+    @Override
+    public void applyComponent(
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility,
+            N nodeInfo,
+            Component component,
+            List<AccessibilitySemantics> semantics) {
+        float[] locationInWindow = new float[2];
+        component.getLocationInWindow(locationInWindow);
+        Rect bounds =
+                new Rect(
+                        (int) locationInWindow[0],
+                        (int) locationInWindow[1],
+                        (int) (locationInWindow[0] + component.getWidth()),
+                        (int) (locationInWindow[1] + component.getHeight()));
+        setBoundsInScreen(nodeInfo, bounds);
+
+        setUniqueId(nodeInfo, String.valueOf(component.getComponentId()));
+
+        if (component instanceof AccessibleComponent) {
+            applyContentDescription(
+                    ((AccessibleComponent) component).getContentDescriptionId(),
+                    nodeInfo,
+                    remoteComposeAccessibility);
+
+            applyText(
+                    ((AccessibleComponent) component).getTextId(),
+                    nodeInfo,
+                    remoteComposeAccessibility);
+
+            applyRole(((AccessibleComponent) component).getRole(), nodeInfo);
+        }
+
+        applySemantics(remoteComposeAccessibility, nodeInfo, semantics);
+
+        if (getText(nodeInfo) == null && getContentDescription(nodeInfo) == null) {
+            setContentDescription(nodeInfo, "");
+        }
+    }
+
+    protected void applySemantics(
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility,
+            N nodeInfo,
+            List<AccessibilitySemantics> semantics) {
+        for (AccessibilitySemantics semantic : semantics) {
+            if (semantic.isInterestingForSemantics()) {
+                if (semantic instanceof CoreSemantics) {
+                    CoreSemantics coreSemantics = (CoreSemantics) semantic;
+                    applyCoreSemantics(remoteComposeAccessibility, nodeInfo, coreSemantics);
+                } else if (semantic instanceof AccessibleComponent) {
+                    AccessibleComponent accessibleComponent = (AccessibleComponent) semantic;
+                    if (accessibleComponent.isClickable()) {
+                        setClickable(nodeInfo, true);
+                    }
+
+                    if (accessibleComponent.getContentDescriptionId() != null) {
+                        applyContentDescription(
+                                accessibleComponent.getContentDescriptionId(),
+                                nodeInfo,
+                                remoteComposeAccessibility);
+                    }
+
+                    if (accessibleComponent.getTextId() != null) {
+                        applyText(
+                                accessibleComponent.getTextId(),
+                                nodeInfo,
+                                remoteComposeAccessibility);
+                    }
+
+                    applyRole(accessibleComponent.getRole(), nodeInfo);
+                }
+            }
+        }
+    }
+
+    protected void applyCoreSemantics(
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility,
+            N nodeInfo,
+            CoreSemantics coreSemantics) {
+        applyContentDescription(
+                coreSemantics.getContentDescriptionId(), nodeInfo, remoteComposeAccessibility);
+
+        applyRole(coreSemantics.getRole(), nodeInfo);
+
+        applyText(coreSemantics.getTextId(), nodeInfo, remoteComposeAccessibility);
+
+        applyStateDescription(
+                coreSemantics.getStateDescriptionId(), nodeInfo, remoteComposeAccessibility);
+
+        if (!coreSemantics.mEnabled) {
+            setEnabled(nodeInfo, false);
+        }
+    }
+
+    protected void applyStateDescription(
+            Integer stateDescriptionId,
+            N nodeInfo,
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility) {
+        if (stateDescriptionId != null) {
+            setStateDescription(
+                    nodeInfo,
+                    appendNullable(
+                            getStateDescription(nodeInfo),
+                            remoteComposeAccessibility.stringValue(stateDescriptionId)));
+        }
+    }
+
+    protected void applyRole(AccessibleComponent.Role role, N nodeInfo) {
+        if (role != null) {
+            setRoleDescription(nodeInfo, role.getDescription());
+        }
+    }
+
+    protected void applyText(
+            Integer textId,
+            N nodeInfo,
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility) {
+        if (textId != null) {
+            setText(
+                    nodeInfo,
+                    appendNullable(
+                            getText(nodeInfo), remoteComposeAccessibility.stringValue(textId)));
+        }
+    }
+
+    protected void applyContentDescription(
+            Integer contentDescriptionId,
+            N nodeInfo,
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility) {
+        if (contentDescriptionId != null) {
+            setContentDescription(
+                    nodeInfo,
+                    appendNullable(
+                            getContentDescription(nodeInfo),
+                            remoteComposeAccessibility.stringValue(contentDescriptionId)));
+        }
+    }
+
+    private CharSequence appendNullable(CharSequence contentDescription, String value) {
+        if (contentDescription == null) {
+            return value;
+        } else if (value == null) {
+            return contentDescription;
+        } else {
+            return contentDescription + " " + value;
+        }
+    }
+
+    protected abstract void setClickable(N nodeInfo, boolean b);
+
+    protected abstract void setEnabled(N nodeInfo, boolean b);
+
+    protected abstract CharSequence getStateDescription(N nodeInfo);
+
+    protected abstract void setStateDescription(N nodeInfo, CharSequence charSequence);
+
+    protected abstract void setRoleDescription(N nodeInfo, String description);
+
+    protected abstract CharSequence getText(N nodeInfo);
+
+    protected abstract void setText(N nodeInfo, CharSequence charSequence);
+
+    protected abstract CharSequence getContentDescription(N nodeInfo);
+
+    protected abstract void setContentDescription(N nodeInfo, CharSequence charSequence);
+
+    protected abstract void setBoundsInScreen(N nodeInfo, Rect bounds);
+
+    protected abstract void setUniqueId(N nodeInfo, String s);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
index 66a7f02..2cd4f03 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
@@ -17,10 +17,10 @@
 
 import android.annotation.Nullable;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.os.Bundle;
 
 import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.Operation;
 import com.android.internal.widget.remotecompose.core.operations.layout.ClickModifierOperation;
 import com.android.internal.widget.remotecompose.core.operations.layout.Component;
 import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
@@ -31,9 +31,9 @@
 import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent;
 import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -43,12 +43,9 @@
  * list of modifiers that must be tagged with {@link AccessibilitySemantics} either incidentally
  * (see {@link ClickModifierOperation}) or explicitly (see {@link CoreSemantics}).
  */
-public class CoreDocumentAccessibility
-        implements RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics> {
+public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibility {
     private final CoreDocument mDocument;
 
-    private final Rect mMissingBounds = new Rect(0, 0, 1, 1);
-
     public CoreDocumentAccessibility(CoreDocument document) {
         this.mDocument = document;
     }
@@ -74,17 +71,25 @@
     }
 
     @Override
-    public List<CoreSemantics.Mode> mergeMode(Component component) {
+    public CoreSemantics.Mode mergeMode(Component component) {
         if (!(component instanceof LayoutComponent)) {
-            return Collections.singletonList(CoreSemantics.Mode.SET);
+            return CoreSemantics.Mode.SET;
         }
 
-        return ((LayoutComponent) component)
-                .getComponentModifiers().getList().stream()
-                        .filter(i -> i instanceof AccessibleComponent)
-                        .map(i -> ((AccessibleComponent) i).getMode())
-                        .filter(Objects::nonNull)
-                        .collect(Collectors.toList());
+        CoreSemantics.Mode result = CoreSemantics.Mode.SET;
+
+        for (ModifierOperation modifier :
+                ((LayoutComponent) component).getComponentModifiers().getList()) {
+            if (modifier instanceof AccessibleComponent) {
+                AccessibleComponent semantics = (AccessibleComponent) modifier;
+
+                if (semantics.getMode().ordinal() > result.ordinal()) {
+                    result = semantics.getMode();
+                }
+            }
+        }
+
+        return result;
     }
 
     @Override
@@ -101,6 +106,7 @@
     @Override
     public String stringValue(int id) {
         Object value = mDocument.getRemoteComposeState().getFromId(id);
+
         return value != null ? String.valueOf(value) : null;
     }
 
@@ -124,12 +130,33 @@
     }
 
     @Override
-    public List<Integer> semanticallyRelevantChildComponents(Component component) {
-        return componentStream(component)
-                .filter(i -> i.getComponentId() != component.getComponentId())
-                .filter(CoreDocumentAccessibility::isInteresting)
-                .map(Component::getComponentId)
-                .collect(Collectors.toList());
+    public List<Integer> semanticallyRelevantChildComponents(
+            Component component, boolean useUnmergedTree) {
+        if (!component.isVisible()) {
+            return Collections.emptyList();
+        }
+
+        CoreSemantics.Mode mergeMode = mergeMode(component);
+        if (mergeMode == CoreSemantics.Mode.CLEAR_AND_SET
+                || (!useUnmergedTree && mergeMode == CoreSemantics.Mode.MERGE)) {
+            return Collections.emptyList();
+        }
+
+        ArrayList<Integer> result = new ArrayList<>();
+
+        for (Operation child : component.mList) {
+            if (child instanceof Component) {
+                if (isInteresting((Component) child)) {
+                    result.add(((Component) child).getComponentId());
+                } else {
+                    result.addAll(
+                            semanticallyRelevantChildComponents(
+                                    (Component) child, useUnmergedTree));
+                }
+            }
+        }
+
+        return result;
     }
 
     static Stream<Component> componentStream(Component root) {
@@ -153,12 +180,13 @@
     }
 
     static boolean isInteresting(Component component) {
-        boolean interesting =
-                isContainerWithSemantics(component)
-                        || modifiersStream(component)
-                                .anyMatch(CoreDocumentAccessibility::isModifierWithSemantics);
+        if (!component.isVisible()) {
+            return false;
+        }
 
-        return interesting && component.isVisible();
+        return isContainerWithSemantics(component)
+                || modifiersStream(component)
+                        .anyMatch(CoreDocumentAccessibility::isModifierWithSemantics);
     }
 
     static boolean isModifierWithSemantics(ModifierOperation modifier) {
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java
new file mode 100644
index 0000000..010253e
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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 com.android.internal.widget.remotecompose.accessibility;
+
+import android.annotation.NonNull;
+import android.view.View;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+
+/**
+ * Trivial wrapper for calling setAccessibilityDelegate on a View. This exists primarily because the
+ * RemoteDocumentPlayer is either running in the platform on a known API version, or outside in
+ * which case it must use the Androidx ViewCompat class.
+ */
+public class PlatformRemoteComposeAccessibilityRegistrar
+        implements RemoteComposeAccessibilityRegistrar {
+    public PlatformRemoteComposeTouchHelper forRemoteComposePlayer(
+            View player, @NonNull CoreDocument coreDocument) {
+        return new PlatformRemoteComposeTouchHelper(
+                player,
+                new CoreDocumentAccessibility(coreDocument),
+                new AndroidPlatformSemanticNodeApplier());
+    }
+
+    public void setAccessibilityDelegate(View remoteComposePlayer, CoreDocument document) {
+        remoteComposePlayer.setAccessibilityDelegate(
+                forRemoteComposePlayer(remoteComposePlayer, document));
+    }
+
+    public void clearAccessibilityDelegate(View remoteComposePlayer) {
+        remoteComposePlayer.setAccessibilityDelegate(null);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java
index c9ad28a..39a2ab3 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java
@@ -37,24 +37,23 @@
 import java.util.Set;
 import java.util.Stack;
 
-public class PlatformRemoteComposeTouchHelper<N, C, S> extends ExploreByTouchHelper {
-    private final RemoteComposeDocumentAccessibility<C, S> mRemoteDocA11y;
+public class PlatformRemoteComposeTouchHelper extends ExploreByTouchHelper {
+    private final RemoteComposeDocumentAccessibility mRemoteDocA11y;
 
-    private final SemanticNodeApplier<AccessibilityNodeInfo, C, S> mApplier;
+    private final SemanticNodeApplier<AccessibilityNodeInfo> mApplier;
 
     public PlatformRemoteComposeTouchHelper(
             View host,
-            RemoteComposeDocumentAccessibility<C, S> remoteDocA11y,
-            SemanticNodeApplier<AccessibilityNodeInfo, C, S> applier) {
+            RemoteComposeDocumentAccessibility remoteDocA11y,
+            SemanticNodeApplier<AccessibilityNodeInfo> applier) {
         super(host);
         this.mRemoteDocA11y = remoteDocA11y;
         this.mApplier = applier;
     }
 
-    public static PlatformRemoteComposeTouchHelper<
-                    AccessibilityNodeInfo, Component, AccessibilitySemantics>
-            forRemoteComposePlayer(View player, @NonNull CoreDocument coreDocument) {
-        return new PlatformRemoteComposeTouchHelper<>(
+    public static PlatformRemoteComposeTouchHelper forRemoteComposePlayer(
+            View player, @NonNull CoreDocument coreDocument) {
+        return new PlatformRemoteComposeTouchHelper(
                 player,
                 new CoreDocumentAccessibility(coreDocument),
                 new AndroidPlatformSemanticNodeApplier());
@@ -104,18 +103,21 @@
             Integer componentId = toVisit.remove(0);
 
             if (visited.add(componentId)) {
-                virtualViewIds.add(componentId);
+                Component component = mRemoteDocA11y.findComponentById(componentId);
 
-                C component = mRemoteDocA11y.findComponentById(componentId);
+                // Only include the root when it has semantics such as content description
+                if (!RootId.equals(componentId)
+                        || !mRemoteDocA11y.semanticModifiersForComponent(component).isEmpty()) {
+                    virtualViewIds.add(componentId);
+                }
 
                 if (component != null) {
-                    boolean allSet =
-                            mRemoteDocA11y.mergeMode(component).stream()
-                                    .allMatch(i -> i == Mode.SET);
+                    Mode mergeMode = mRemoteDocA11y.mergeMode(component);
 
-                    if (allSet) {
+                    if (mergeMode == Mode.SET) {
                         List<Integer> childViews =
-                                mRemoteDocA11y.semanticallyRelevantChildComponents(component);
+                                mRemoteDocA11y.semanticallyRelevantChildComponents(
+                                        component, false);
 
                         toVisit.addAll(childViews);
                     }
@@ -127,32 +129,34 @@
     @Override
     public void onPopulateNodeForVirtualView(
             int virtualViewId, @NonNull AccessibilityNodeInfo node) {
-        C component = mRemoteDocA11y.findComponentById(virtualViewId);
+        Component component = mRemoteDocA11y.findComponentById(virtualViewId);
 
-        List<Mode> mode = mRemoteDocA11y.mergeMode(component);
+        Mode mergeMode = mRemoteDocA11y.mergeMode(component);
 
-        if (mode.contains(Mode.MERGE)) {
+        // default to enabled
+        node.setEnabled(true);
+
+        if (mergeMode == Mode.MERGE) {
             List<Integer> childViews =
-                    mRemoteDocA11y.semanticallyRelevantChildComponents(component);
+                    mRemoteDocA11y.semanticallyRelevantChildComponents(component, true);
 
             for (Integer childView : childViews) {
                 onPopulateNodeForVirtualView(childView, node);
             }
         }
 
-        List<S> semantics = mRemoteDocA11y.semanticModifiersForComponent(component);
+        List<AccessibilitySemantics> semantics =
+                mRemoteDocA11y.semanticModifiersForComponent(component);
         mApplier.applyComponent(mRemoteDocA11y, node, component, semantics);
     }
 
     @Override
-    protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
-        // TODO
-    }
+    protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {}
 
     @Override
     protected boolean onPerformActionForVirtualView(
             int virtualViewId, int action, @Nullable Bundle arguments) {
-        C component = mRemoteDocA11y.findComponentById(virtualViewId);
+        Component component = mRemoteDocA11y.findComponentById(virtualViewId);
 
         if (component != null) {
             return mRemoteDocA11y.performAction(component, action, arguments);
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeAccessibilityRegistrar.java b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeAccessibilityRegistrar.java
new file mode 100644
index 0000000..7e8236b
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeAccessibilityRegistrar.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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 com.android.internal.widget.remotecompose.accessibility;
+
+import android.view.View;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+
+/**
+ * Interface for registering and clearing accessibility delegates for remote compose players.
+ *
+ * <p>This interface is responsible for managing the accessibility delegate associated with a remote
+ * compose player view. It allows for setting and clearing the delegate, which is used to handle
+ * accessibility events and provide accessibility information for the remote compose content.
+ */
+public interface RemoteComposeAccessibilityRegistrar {
+    void setAccessibilityDelegate(View remoteComposePlayer, CoreDocument document);
+
+    void clearAccessibilityDelegate(View remoteComposePlayer);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeDocumentAccessibility.java
index 14977be..50f75e4 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeDocumentAccessibility.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeDocumentAccessibility.java
@@ -20,6 +20,8 @@
 import android.os.Bundle;
 import android.view.View;
 
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySemantics;
 import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics;
 
 import java.util.List;
@@ -28,11 +30,8 @@
  * Interface for interacting with the accessibility features of a remote Compose UI. This interface
  * provides methods to perform actions, retrieve state, and query the accessibility tree of the
  * remote Compose UI.
- *
- * @param <C> The type of component in the remote Compose UI.
- * @param <S> The type representing semantic modifiers applied to components.
  */
-public interface RemoteComposeDocumentAccessibility<C, S> {
+public interface RemoteComposeDocumentAccessibility {
     // Matches ExploreByTouchHelper.HOST_ID
     Integer RootId = View.NO_ID;
 
@@ -47,7 +46,7 @@
      * @param arguments Optional arguments for the action.
      * @return {@code true} if the action was performed successfully, {@code false} otherwise.
      */
-    boolean performAction(C component, int action, Bundle arguments);
+    boolean performAction(Component component, int action, Bundle arguments);
 
     /**
      * Retrieves the string value associated with the given ID.
@@ -65,9 +64,10 @@
      *
      * @param component The component to retrieve child view IDs from, or [RootId] for the top
      *     level.
+     * @param useUnmergedTree Whether to include merged children
      * @return A list of integer IDs representing the child views of the component.
      */
-    List<Integer> semanticallyRelevantChildComponents(C component);
+    List<Integer> semanticallyRelevantChildComponents(Component component, boolean useUnmergedTree);
 
     /**
      * Retrieves the semantic modifiers associated with a given component.
@@ -75,16 +75,16 @@
      * @param component The component for which to retrieve semantic modifiers.
      * @return A list of semantic modifiers applicable to the component.
      */
-    List<S> semanticModifiersForComponent(C component);
+    List<AccessibilitySemantics> semanticModifiersForComponent(Component component);
 
     /**
      * Gets all applied merge modes of the given component. A Merge mode is one of Set, Merge or
      * Clear and describes how to apply and combine hierarchical semantics.
      *
      * @param component The component to merge the mode for.
-     * @return A list of merged modes, potentially conflicting but to be resolved by the caller.
+     * @return The effective merge modes, potentially conflicting but resolved to a single value.
      */
-    List<CoreSemantics.Mode> mergeMode(C component);
+    CoreSemantics.Mode mergeMode(Component component);
 
     /**
      * Finds a component by its ID.
@@ -93,7 +93,7 @@
      * @return the component with the given ID, or {@code null} if no such component exists
      */
     @Nullable
-    C findComponentById(int id);
+    Component findComponentById(int id);
 
     @Nullable
     Integer getComponentIdAt(PointF point);
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeTouchHelper.java b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeTouchHelper.java
index 4ff7892..1364102 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeTouchHelper.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/RemoteComposeTouchHelper.java
@@ -15,17 +15,11 @@
  */
 package com.android.internal.widget.remotecompose.accessibility;
 
-import android.annotation.NonNull;
-import android.view.View;
-
-import com.android.internal.widget.remotecompose.core.CoreDocument;
-
+/**
+ * This class is the entry point for finding the AccessibilityDelegate for a RemoteCompose document.
+ */
 public class RemoteComposeTouchHelper {
-    public static View.AccessibilityDelegate forRemoteComposePlayer(
-            View player, @NonNull CoreDocument coreDocument) {
-        return new PlatformRemoteComposeTouchHelper<>(
-                player,
-                new CoreDocumentAccessibility(coreDocument),
-                new AndroidPlatformSemanticNodeApplier());
-    }
+    /** Get the platform specific accessibility delegate registrar */
+    public static final RemoteComposeAccessibilityRegistrar REGISTRAR =
+            new PlatformRemoteComposeAccessibilityRegistrar();
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/SemanticNodeApplier.java b/core/java/com/android/internal/widget/remotecompose/accessibility/SemanticNodeApplier.java
index 4368329..832b542 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/SemanticNodeApplier.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/SemanticNodeApplier.java
@@ -15,6 +15,9 @@
  */
 package com.android.internal.widget.remotecompose.accessibility;
 
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySemantics;
+
 import java.util.List;
 
 /**
@@ -29,15 +32,13 @@
  *
  * @param <N> The type representing information about the node. This could be an Androidx
  *     `AccessibilityNodeInfoCompat`, or potentially a platform `AccessibilityNodeInfo`.
- * @param <C> The type of component in the remote Compose UI.
- * @param <S> The type representing a single semantic property or action.
  */
-public interface SemanticNodeApplier<N, C, S> {
+public interface SemanticNodeApplier<N> {
     void applyComponent(
-            RemoteComposeDocumentAccessibility<C, S> remoteComposeAccessibility,
+            RemoteComposeDocumentAccessibility remoteComposeAccessibility,
             N nodeInfo,
-            C component,
-            List<S> semantics);
+            Component component,
+            List<AccessibilitySemantics> semantics);
 
     String VIRTUAL_VIEW_ID_KEY = "VirtualViewId";
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 5bc3bca..f142886 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -441,11 +441,11 @@
         mActionListeners.clear();
     }
 
-    public interface ClickCallbacks {
-        void click(int id, @Nullable String metadata);
+    public interface IdActionCallback {
+        void onAction(int id, @Nullable String metadata);
     }
 
-    @NonNull HashSet<ClickCallbacks> mClickListeners = new HashSet<>();
+    @NonNull HashSet<IdActionCallback> mIdActionListeners = new HashSet<>();
     @NonNull HashSet<TouchListener> mTouchListeners = new HashSet<>();
     @NonNull HashSet<ClickAreaRepresentation> mClickAreas = new HashSet<>();
 
@@ -769,12 +769,12 @@
     }
 
     /**
-     * Add a click listener. This will get called when a click is detected on the document
+     * Add an id action listener. This will get called when e.g. a click is detected on the document
      *
-     * @param callback called when a click area has been hit, passing the click are id and metadata.
+     * @param callback called when an action is executed, passing the id and metadata.
      */
-    public void addClickListener(@NonNull ClickCallbacks callback) {
-        mClickListeners.add(callback);
+    public void addIdActionListener(@NonNull IdActionCallback callback) {
+        mIdActionListeners.add(callback);
     }
 
     /**
@@ -783,8 +783,8 @@
      * @return set of click listeners
      */
     @NonNull
-    public HashSet<CoreDocument.ClickCallbacks> getClickListeners() {
-        return mClickListeners;
+    public HashSet<IdActionCallback> getIdActionListeners() {
+        return mIdActionListeners;
     }
 
     /**
@@ -813,15 +813,15 @@
                 warnClickListeners(clickArea);
             }
         }
-        for (ClickCallbacks listener : mClickListeners) {
-            listener.click(id, "");
+        for (IdActionCallback listener : mIdActionListeners) {
+            listener.onAction(id, "");
         }
     }
 
     /** Warn click listeners when a click area is activated */
     private void warnClickListeners(@NonNull ClickAreaRepresentation clickArea) {
-        for (ClickCallbacks listener : mClickListeners) {
-            listener.click(clickArea.mId, clickArea.mMetadata);
+        for (IdActionCallback listener : mIdActionListeners) {
+            listener.onAction(clickArea.mId, clickArea.mMetadata);
         }
     }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
index f42abfc..14b72af 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java
@@ -366,6 +366,7 @@
 
     /**
      * Set the component the touch expression is in (if any)
+     *
      * @param component the component, or null if outside
      */
     public void setComponent(@Nullable Component component) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java
index 8b6d497..9588e99 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java
@@ -92,9 +92,7 @@
         return false;
     }
 
-    /**
-     * Reset the modifier
-     */
+    /** Reset the modifier */
     public void reset() {
         mLastTime = 0;
         mScrollX = 0f;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/semantics/CoreSemantics.java b/core/java/com/android/internal/widget/remotecompose/core/semantics/CoreSemantics.java
index b8166e6..4047dd2 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/semantics/CoreSemantics.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/semantics/CoreSemantics.java
@@ -48,6 +48,11 @@
     }
 
     @Override
+    public Mode getMode() {
+        return mMode;
+    }
+
+    @Override
     public void write(WireBuffer buffer) {
         buffer.writeInt(mContentDescriptionId);
         buffer.writeByte((mRole != null) ? mRole.ordinal() : -1);
@@ -78,6 +83,10 @@
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append("SEMANTICS");
+        if (mMode != Mode.SET) {
+            builder.append(" ");
+            builder.append(mMode);
+        }
         if (mRole != null) {
             builder.append(" ");
             builder.append(mRole);
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
index 19453a0..276a506 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -32,6 +32,7 @@
 import android.widget.HorizontalScrollView;
 import android.widget.ScrollView;
 
+import com.android.internal.widget.remotecompose.accessibility.RemoteComposeTouchHelper;
 import com.android.internal.widget.remotecompose.core.CoreDocument;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
 import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
@@ -61,6 +62,15 @@
     }
 
     /**
+     * Returns true if the document supports drag touch events
+     *
+     * @return true if draggable content, false otherwise
+     */
+    public boolean isDraggable() {
+        return mInner.isDraggable();
+    }
+
+    /**
      * Turn on debug information
      *
      * @param debugFlags 1 to set debug on
@@ -83,9 +93,12 @@
             } else {
                 Log.e("RemoteComposePlayer", "Unsupported document ");
             }
+
+            RemoteComposeTouchHelper.REGISTRAR.setAccessibilityDelegate(this, value.getDocument());
         } else {
             mInner.setDocument(null);
-            this.setAccessibilityDelegate(null);
+
+            RemoteComposeTouchHelper.REGISTRAR.clearAccessibilityDelegate(this);
         }
         mapColors();
         setupSensors();
@@ -236,22 +249,23 @@
         mInner.clearLocalString("SYSTEM:" + name);
     }
 
-    public interface ClickCallbacks {
-        void click(int id, String metadata);
+    /** Id action callback interface */
+    public interface IdActionCallbacks {
+        void onAction(int id, String metadata);
     }
 
     /**
-     * Add a callback for handling click events on the document
+     * Add a callback for handling id actions events on the document
      *
-     * @param callback the callback lambda that will be used when a click is detected
+     * @param callback the callback lambda that will be used when a action is executed
      *     <p>The parameter of the callback are:
      *     <ul>
-     *       <li>id : the id of the clicked area
-     *       <li>metadata: a client provided unstructured string associated with that area
+     *       <li>id : the id of the action
+     *       <li>metadata: a client provided unstructured string associated with that id action
      *     </ul>
      */
-    public void addClickListener(ClickCallbacks callback) {
-        mInner.addClickListener((id, metadata) -> callback.click(id, metadata));
+    public void addIdActionListener(IdActionCallbacks callback) {
+        mInner.addIdActionListener((id, metadata) -> callback.onAction(id, metadata));
     }
 
     /**
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
index 8da5b9d..5ba6283 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -209,15 +209,27 @@
         mARContext.loadFloat(id, value);
     }
 
+    /**
+     * Returns true if the document supports drag touch events
+     *
+     * @return true if draggable content, false otherwise
+     */
+    public boolean isDraggable() {
+        if (mDocument == null) {
+            return false;
+        }
+        return mDocument.getDocument().hasTouchListener();
+    }
+
     public interface ClickCallbacks {
         void click(int id, String metadata);
     }
 
-    public void addClickListener(ClickCallbacks callback) {
+    public void addIdActionListener(ClickCallbacks callback) {
         if (mDocument == null) {
             return;
         }
-        mDocument.getDocument().addClickListener((id, metadata) -> callback.click(id, metadata));
+        mDocument.getDocument().addIdActionListener((id, metadata) -> callback.click(id, metadata));
     }
 
     public int getTheme() {
@@ -267,6 +279,7 @@
                     invalidate();
                     return true;
                 }
+                return false;
 
             case MotionEvent.ACTION_UP:
                 mInActionDown = false;
@@ -280,6 +293,7 @@
                     invalidate();
                     return true;
                 }
+                return false;
 
             case MotionEvent.ACTION_MOVE:
                 if (mInActionDown) {
@@ -293,6 +307,7 @@
                     }
                     return true;
                 }
+                return false;
         }
         return false;
     }