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

Commit ec230436 authored by Nicolas Roard's avatar Nicolas Roard
Browse files

Update to ToT RemoteCompose

Bug: 339721781
Flag: EXEMPT External Libraries
Test: in GoB

Change-Id: I6a66651c026d7a58fdb461d54d4b453541d10439
parent 62c60503
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -5825,7 +5825,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