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

Commit 5f759864 authored by lucychang's avatar lucychang
Browse files

Don't send AccessibilityEent if text is unchanged

It always sends TYPE_WINDOW_CONTENT_CHANGED event with
CONTENT_CHANGE_TYPE_TEXT when invoking the API TextView#setText.
However, performs action ACTION_SET_SELECTION will call the API and
send the event even though the text unchanged. So the fix tried to
send event only when text is changed.

Bug: 176707630
Test: atest com.android.internal.accessibility.AccessibilityUtilsTest
and manually test by performing action ACTION_SET_SELECTION

Change-Id: I16973f8648270cb0c652c2663615aed1435b0e2e
parent 83fff6cd
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -197,6 +197,7 @@ import android.view.textservice.TextServicesManager;
import android.view.translation.TranslationRequest;
import android.widget.RemoteViews.RemoteView;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -6301,6 +6302,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            text = TextUtils.stringOrSpannedString(text);
        }
        @AccessibilityUtils.A11yTextChangeType int a11yTextChangeType = AccessibilityUtils.NONE;
        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            a11yTextChangeType = AccessibilityUtils.textOrSpanChanged(text, mText);
        }
        if (mAutoLinkMask != 0) {
            Spannable s2;
@@ -6320,6 +6326,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                 * setText() again to try to upgrade the buffer type.
                 */
                setTextInternal(text);
                if (a11yTextChangeType == AccessibilityUtils.NONE) {
                    a11yTextChangeType = AccessibilityUtils.PARCELABLE_SPAN;
                }
                // Do not change the movement method for text that support text selection as it
                // would prevent an arbitrary cursor displacement.
@@ -6384,7 +6393,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);
        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
        if (a11yTextChangeType == AccessibilityUtils.TEXT) {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
        } else if (a11yTextChangeType == AccessibilityUtils.PARCELABLE_SPAN) {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
        }
        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
+77 −1
Original line number Diff line number Diff line
@@ -20,16 +20,23 @@ import static com.android.internal.accessibility.common.ShortcutConstants.Access
import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.ParcelableSpan;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.ArraySet;
import android.view.accessibility.AccessibilityManager;

import libcore.util.EmptyArray;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -39,7 +46,25 @@ import java.util.Set;
 * Collection of utilities for accessibility service.
 */
public final class AccessibilityUtils {
    private AccessibilityUtils() {}
    private AccessibilityUtils() {
    }

    /** @hide */
    @IntDef(value = {
            NONE,
            TEXT,
            PARCELABLE_SPAN
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface A11yTextChangeType {
    }

    /** Specifies no content has been changed for accessibility. */
    public static final int NONE = 0;
    /** Specifies some readable sequence has been changed. */
    public static final int TEXT = 1;
    /** Specifies some parcelable spans has been changed. */
    public static final int PARCELABLE_SPAN = 2;

    /**
     * Returns the set of enabled accessibility services for userId. If there are no
@@ -168,4 +193,55 @@ public final class AccessibilityUtils {
                Settings.Secure.USER_SETUP_COMPLETE, /* def= */ 0, UserHandle.USER_CURRENT)
                != /* false */ 0;
    }

    /**
     * Returns the text change type for accessibility. It only cares about readable sequence changes
     * or {@link ParcelableSpan} changes which are able to pass via IPC.
     *
     * @param before The CharSequence before changing
     * @param after  The CharSequence after changing
     * @return Returns {@code TEXT} for readable sequence changes or {@code PARCELABLE_SPAN} for
     * ParcelableSpan changes. Otherwise, returns {@code NONE}.
     */
    @A11yTextChangeType
    public static int textOrSpanChanged(CharSequence before, CharSequence after) {
        if (!TextUtils.equals(before, after)) {
            return TEXT;
        }
        if (before instanceof Spanned || after instanceof Spanned) {
            if (!parcelableSpansEquals(before, after)) {
                return PARCELABLE_SPAN;
            }
        }
        return NONE;
    }

    private static boolean parcelableSpansEquals(CharSequence before, CharSequence after) {
        Object[] spansA = EmptyArray.OBJECT;
        Object[] spansB = EmptyArray.OBJECT;
        Spanned a = null;
        Spanned b = null;
        if (before instanceof Spanned) {
            a = (Spanned) before;
            spansA = a.getSpans(0, a.length(), ParcelableSpan.class);
        }
        if (after instanceof Spanned) {
            b = (Spanned) after;
            spansB = b.getSpans(0, b.length(), ParcelableSpan.class);
        }
        if (spansA.length != spansB.length) {
            return false;
        }
        for (int i = 0; i < spansA.length; ++i) {
            final Object thisSpan = spansA[i];
            final Object otherSpan = spansB[i];
            if ((thisSpan.getClass() != otherSpan.getClass())
                    || (a.getSpanStart(thisSpan) != b.getSpanStart(otherSpan))
                    || (a.getSpanEnd(thisSpan) != b.getSpanEnd(otherSpan))
                    || (a.getSpanFlags(thisSpan) != b.getSpanFlags(otherSpan))) {
                return false;
            }
        }
        return true;
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.accessibility;

import static junit.framework.Assert.assertEquals;

import android.text.ParcelableSpan;
import android.text.SpannableString;
import android.text.style.LocaleSpan;

import androidx.test.runner.AndroidJUnit4;

import com.android.internal.accessibility.util.AccessibilityUtils;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Locale;

/**
 * Unit tests for AccessibilityUtils.
 */
@RunWith(AndroidJUnit4.class)
public class AccessibilityUtilsTest {
    @Test
    public void textOrSpanChanged_stringChange_returnTextChange() {
        final CharSequence beforeText = "a";

        final CharSequence afterText = "b";

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.TEXT, type);
    }

    @Test
    public void textOrSpanChanged_stringNotChange_returnNoneChange() {
        final CharSequence beforeText = "a";

        final CharSequence afterText = "a";

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.NONE, type);
    }

    @Test
    public void textOrSpanChanged_nonSpanToNonParcelableSpan_returnNoneChange() {
        final Object nonParcelableSpan = new Object();
        final CharSequence beforeText = new SpannableString("a");

        final SpannableString afterText = new SpannableString("a");
        afterText.setSpan(nonParcelableSpan, 0, 1, 0);

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.NONE, type);
    }

    @Test
    public void textOrSpanChanged_nonSpanToParcelableSpan_returnParcelableSpanChange() {
        final ParcelableSpan parcelableSpan = new LocaleSpan(Locale.ENGLISH);
        final CharSequence beforeText = new SpannableString("a");

        final SpannableString afterText = new SpannableString("a");
        afterText.setSpan(parcelableSpan, 0, 1, 0);

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.PARCELABLE_SPAN, type);
    }

    @Test
    public void textOrSpanChanged_nonParcelableSpanToParcelableSpan_returnParcelableSpanChange() {
        final Object nonParcelableSpan = new Object();
        final ParcelableSpan parcelableSpan = new LocaleSpan(Locale.ENGLISH);
        final SpannableString beforeText = new SpannableString("a");
        beforeText.setSpan(nonParcelableSpan, 0, 1, 0);

        SpannableString afterText = new SpannableString("a");
        afterText.setSpan(parcelableSpan, 0, 1, 0);

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.PARCELABLE_SPAN, type);
    }

    @Test
    public void textOrSpanChanged_nonParcelableSpanChange_returnNoneChange() {
        final Object nonParcelableSpan = new Object();
        final SpannableString beforeText = new SpannableString("a");
        beforeText.setSpan(nonParcelableSpan, 0, 1, 0);

        final SpannableString afterText = new SpannableString("a");
        afterText.setSpan(nonParcelableSpan, 1, 1, 0);

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.NONE, type);
    }

    @Test
    public void textOrSpanChanged_parcelableSpanChange_returnParcelableSpanChange() {
        final ParcelableSpan parcelableSpan = new LocaleSpan(Locale.ENGLISH);
        final SpannableString beforeText = new SpannableString("a");
        beforeText.setSpan(parcelableSpan, 0, 1, 0);

        final SpannableString afterText = new SpannableString("a");
        afterText.setSpan(parcelableSpan, 1, 1, 0);

        @AccessibilityUtils.A11yTextChangeType int type = AccessibilityUtils.textOrSpanChanged(
                beforeText, afterText);
        assertEquals(AccessibilityUtils.PARCELABLE_SPAN, type);
    }
}