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

Commit 71f38092 authored by Yara Hassan's avatar Yara Hassan
Browse files

Add utils to process a11y check results

Also moved a11ychecker from core/... to services/accessibility

NO_IFTTT=New IFTTT

Bug: 350530488
Bug: 341926585
Test: unit tests
Flag: com.android.server.accessibility.enable_a11y_checker_logging
Change-Id: Ibc0cb4eb2d207032ef21a46196d4b2cc04cf7e95
parent a9b54179
Loading
Loading
Loading
Loading
+0 −7
Original line number Diff line number Diff line
java_library_static {
    name: "A11yChecker",
    srcs: [
        "*.java",
    ],
    visibility: ["//visibility:public"],
}
+0 −1
Original line number Diff line number Diff line
@@ -63,7 +63,6 @@ android_test {
        "-c fa",
    ],
    static_libs: [
        "A11yChecker",
        "collector-device-lib-platform",
        "frameworks-base-testutils",
        "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
+0 −5
Original line number Diff line number Diff line
# Android Accessibility Framework owners
include /core/java/android/view/accessibility/a11ychecker/OWNERS
include /services/accessibility/OWNERS

yaraabdullatif@google.com
+13 −0
Original line number Diff line number Diff line
@@ -29,10 +29,12 @@ java_library_static {
        "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
    ],
    libs: [
        "aatf",
        "services.core",
        "androidx.annotation_annotation",
    ],
    static_libs: [
        "a11ychecker-protos-java-proto-lite",
        "com_android_server_accessibility_flags_lib",
        "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",

@@ -68,3 +70,14 @@ java_aconfig_library {
    name: "com_android_server_accessibility_flags_lib",
    aconfig_declarations: "com_android_server_accessibility_flags",
}

java_library_static {
    name: "a11ychecker-protos-java-proto-lite",
    proto: {
        type: "lite",
        canonical_path_from_root: false,
    },
    srcs: [
        "java/**/a11ychecker/proto/*.proto",
    ],
}
+218 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.server.accessibility.a11ychecker;


import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;

import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TextSizeCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck;

import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Util class to process a11y checker results for logging.
 *
 * @hide
 */
public class AccessibilityCheckerUtils {

    private static final String LOG_TAG = "AccessibilityCheckerUtils";
    @VisibleForTesting
    // LINT.IfChange
    static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass>
            CHECK_CLASS_TO_ENUM_MAP =
            Map.ofEntries(
                    classMapEntry(ClassNameCheck.class, AccessibilityCheckClass.CLASS_NAME_CHECK),
                    classMapEntry(ClickableSpanCheck.class,
                            AccessibilityCheckClass.CLICKABLE_SPAN_CHECK),
                    classMapEntry(DuplicateClickableBoundsCheck.class,
                            AccessibilityCheckClass.DUPLICATE_CLICKABLE_BOUNDS_CHECK),
                    classMapEntry(DuplicateSpeakableTextCheck.class,
                            AccessibilityCheckClass.DUPLICATE_SPEAKABLE_TEXT_CHECK),
                    classMapEntry(EditableContentDescCheck.class,
                            AccessibilityCheckClass.EDITABLE_CONTENT_DESC_CHECK),
                    classMapEntry(ImageContrastCheck.class,
                            AccessibilityCheckClass.IMAGE_CONTRAST_CHECK),
                    classMapEntry(LinkPurposeUnclearCheck.class,
                            AccessibilityCheckClass.LINK_PURPOSE_UNCLEAR_CHECK),
                    classMapEntry(RedundantDescriptionCheck.class,
                            AccessibilityCheckClass.REDUNDANT_DESCRIPTION_CHECK),
                    classMapEntry(SpeakableTextPresentCheck.class,
                            AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK),
                    classMapEntry(TextContrastCheck.class,
                            AccessibilityCheckClass.TEXT_CONTRAST_CHECK),
                    classMapEntry(TextSizeCheck.class, AccessibilityCheckClass.TEXT_SIZE_CHECK),
                    classMapEntry(TouchTargetSizeCheck.class,
                            AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK),
                    classMapEntry(TraversalOrderCheck.class,
                            AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK));
    // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto)

    static Set<AccessibilityCheckResultReported> processResults(
            Context context,
            AccessibilityNodeInfo nodeInfo,
            List<AccessibilityHierarchyCheckResult> checkResults,
            @Nullable AccessibilityEvent accessibilityEvent,
            ComponentName a11yServiceComponentName) {
        return processResults(nodeInfo, checkResults, accessibilityEvent,
                context.getPackageManager(), a11yServiceComponentName);
    }

    @VisibleForTesting
    static Set<AccessibilityCheckResultReported> processResults(
            AccessibilityNodeInfo nodeInfo,
            List<AccessibilityHierarchyCheckResult> checkResults,
            @Nullable AccessibilityEvent accessibilityEvent,
            PackageManager packageManager,
            ComponentName a11yServiceComponentName) {
        String appPackageName = nodeInfo.getPackageName().toString();
        AccessibilityCheckResultReported.Builder builder;
        try {
            builder = AccessibilityCheckResultReported.newBuilder()
                    .setPackageName(appPackageName)
                    .setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
                    .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo))
                    .setActivityName(getActivityName(packageManager, accessibilityEvent))
                    .setWindowTitle(getWindowTitle(nodeInfo))
                    .setSourceComponentName(a11yServiceComponentName.flattenToString())
                    .setSourceVersionCode(
                            getAppVersionCode(packageManager,
                                    a11yServiceComponentName.getPackageName()));
        } catch (PackageManager.NameNotFoundException e) {
            Slog.e(LOG_TAG, "Unknown package name", e);
            return Set.of();
        }

        return checkResults.stream()
                .filter(checkResult -> checkResult.getType()
                        == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
                        || checkResult.getType()
                        == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
                .map(checkResult -> builder.setResultCheckClass(
                        getCheckClass(checkResult)).setResultType(
                        getCheckResultType(checkResult)).setResultId(
                        checkResult.getResultId()).build())
                .collect(Collectors.toUnmodifiableSet());
    }

    private static long getAppVersionCode(PackageManager packageManager, String packageName) throws
            PackageManager.NameNotFoundException {
        PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
        return packageInfo.getLongVersionCode();
    }

    /**
     * Returns the simple class name of the Activity providing the cache update, if available,
     * or an empty String if not.
     */
    @VisibleForTesting
    static String getActivityName(
            PackageManager packageManager, @Nullable AccessibilityEvent accessibilityEvent) {
        if (accessibilityEvent == null) {
            return "";
        }
        CharSequence activityName = accessibilityEvent.getClassName();
        if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                && accessibilityEvent.getPackageName() != null
                && activityName != null) {
            try {
                // Check class is for a valid Activity.
                packageManager
                        .getActivityInfo(
                                new ComponentName(accessibilityEvent.getPackageName().toString(),
                                        activityName.toString()), 0);
                int qualifierEnd = activityName.toString().lastIndexOf('.');
                return activityName.toString().substring(qualifierEnd + 1);
            } catch (PackageManager.NameNotFoundException e) {
                // No need to spam the logs. This is very frequent when the class doesn't match
                // an activity.
            }
        }
        return "";
    }

    /**
     * Returns the title of the window containing the a11y node.
     */
    private static String getWindowTitle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo.getWindow() == null) {
            return "";
        }
        CharSequence windowTitle = nodeInfo.getWindow().getTitle();
        return windowTitle == null ? "" : windowTitle.toString();
    }

    /**
     * Maps the {@link AccessibilityHierarchyCheck} class that produced the given result, with the
     * corresponding {@link AccessibilityCheckClass} enum. This enumeration is to avoid relying on
     * String class names in the logging, which can be proguarded. It also reduces the logging size.
     */
    private static AccessibilityCheckClass getCheckClass(
            AccessibilityHierarchyCheckResult checkResult) {
        if (CHECK_CLASS_TO_ENUM_MAP.containsKey(checkResult.getSourceCheckClass())) {
            return CHECK_CLASS_TO_ENUM_MAP.get(checkResult.getSourceCheckClass());
        }
        return AccessibilityCheckClass.UNKNOWN_CHECK;
    }

    private static AccessibilityCheckResultType getCheckResultType(
            AccessibilityHierarchyCheckResult checkResult) {
        return switch (checkResult.getType()) {
            case ERROR -> AccessibilityCheckResultType.ERROR;
            case WARNING -> AccessibilityCheckResultType.WARNING;
            default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
        };
    }

    private static Map.Entry<Class<? extends AccessibilityHierarchyCheck>,
            AccessibilityCheckClass> classMapEntry(
            Class<? extends AccessibilityHierarchyCheck> checkClass,
            AccessibilityCheckClass checkClassEnum) {
        return new AbstractMap.SimpleImmutableEntry<>(checkClass, checkClassEnum);
    }
}
Loading