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;
}