Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 471e6c9e authored by Nicolas Roard's avatar Nicolas Roard Committed by Android (Google) Code Review
Browse files

Merge "Update to ToT RemoteCompose" into main

parents b0bd2321 ec230436
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -5826,7 +5826,7 @@ public class RemoteViews implements Parcelable, Filter {
                }
                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) {
+42 −127
Original line number Diff line number Diff line
@@ -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);

        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
        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("");
        }
    @Override
    protected void setEnabled(AccessibilityNodeInfo nodeInfo, boolean enabled) {
        nodeInfo.setEnabled(enabled);
    }

    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);
                    }
                }
            }
        }
    @Override
    protected CharSequence getStateDescription(AccessibilityNodeInfo nodeInfo) {
        return nodeInfo.getStateDescription();
    }

    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);
    @Override
    protected void setStateDescription(AccessibilityNodeInfo nodeInfo, CharSequence description) {
        nodeInfo.setStateDescription(description);
    }

    void applyRole(@Nullable AccessibleComponent.Role role, AccessibilityNodeInfo nodeInfo) {
        if (role != null) {
            nodeInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, role.getDescription());
        }
    @Override
    protected void setRoleDescription(AccessibilityNodeInfo nodeInfo, String description) {
        nodeInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, description);
    }

    void applyContentDescription(
            @Nullable Integer contentDescriptionId,
            AccessibilityNodeInfo nodeInfo,
            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
                    remoteComposeAccessibility) {
        if (contentDescriptionId != null) {
            nodeInfo.setContentDescription(
                    remoteComposeAccessibility.stringValue(contentDescriptionId));
    @Override
    protected CharSequence getText(AccessibilityNodeInfo nodeInfo) {
        return nodeInfo.getText();
    }

    @Override
    protected void setText(AccessibilityNodeInfo nodeInfo, CharSequence text) {
        nodeInfo.setText(text);
    }

    void applyText(
            @Nullable Integer textId,
            AccessibilityNodeInfo nodeInfo,
            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
                    remoteComposeAccessibility) {
        if (textId != null) {
            nodeInfo.setText(remoteComposeAccessibility.stringValue(textId));
    @Override
    protected CharSequence getContentDescription(AccessibilityNodeInfo nodeInfo) {
        return nodeInfo.getContentDescription();
    }

    @Override
    protected void setContentDescription(AccessibilityNodeInfo nodeInfo, CharSequence description) {
        nodeInfo.setContentDescription(description);
    }

    void applyStateDescription(
            @Nullable Integer stateDescriptionId,
            AccessibilityNodeInfo nodeInfo,
            RemoteComposeDocumentAccessibility<Component, AccessibilitySemantics>
                    remoteComposeAccessibility) {
        if (stateDescriptionId != null) {
            nodeInfo.setStateDescription(
                    remoteComposeAccessibility.stringValue(stateDescriptionId));
    @Override
    protected void setBoundsInScreen(AccessibilityNodeInfo nodeInfo, Rect bounds) {
        nodeInfo.setBoundsInParent(bounds);
        nodeInfo.setBoundsInScreen(bounds);
    }

    @Override
    protected void setUniqueId(AccessibilityNodeInfo nodeInfo, String id) {
        nodeInfo.setUniqueId(id);
    }
}
+208 −0
Original line number Diff line number Diff line
/*
 * 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);
}
+53 −25
Original line number Diff line number Diff line
@@ -17,10 +17,10 @@ package com.android.internal.widget.remotecompose.accessibility;

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.AccessibilitySem
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 @@ import java.util.stream.Stream;
 * 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 @@ public class CoreDocumentAccessibility
    }

    @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 @@ public class CoreDocumentAccessibility
    @Override
    public String stringValue(int id) {
        Object value = mDocument.getRemoteComposeState().getFromId(id);

        return value != null ? String.valueOf(value) : null;
    }

@@ -124,12 +130,33 @@ public class CoreDocumentAccessibility
    }

    @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 @@ public class CoreDocumentAccessibility
    }

    static boolean isInteresting(Component component) {
        boolean interesting =
                isContainerWithSemantics(component)
        if (!component.isVisible()) {
            return false;
        }

        return isContainerWithSemantics(component)
                || modifiersStream(component)
                        .anyMatch(CoreDocumentAccessibility::isModifierWithSemantics);

        return interesting && component.isVisible();
    }

    static boolean isModifierWithSemantics(ModifierOperation modifier) {
+46 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
Loading