Loading core/java/android/widget/TextView.java +16 −1 Original line number Diff line number Diff line Loading @@ -202,6 +202,7 @@ import android.view.translation.ViewTranslationRequest; import android.view.translation.ViewTranslationResponse; 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; Loading Loading @@ -6316,6 +6317,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; Loading @@ -6335,6 +6341,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. Loading Loading @@ -6399,7 +6408,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); Loading core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +77 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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 Loading Loading @@ -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; } } core/tests/coretests/src/com/android/internal/accessibility/AccessibilityUtilsTest.java 0 → 100644 +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); } } Loading
core/java/android/widget/TextView.java +16 −1 Original line number Diff line number Diff line Loading @@ -202,6 +202,7 @@ import android.view.translation.ViewTranslationRequest; import android.view.translation.ViewTranslationResponse; 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; Loading Loading @@ -6316,6 +6317,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; Loading @@ -6335,6 +6341,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. Loading Loading @@ -6399,7 +6408,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); Loading
core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +77 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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 Loading Loading @@ -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; } }
core/tests/coretests/src/com/android/internal/accessibility/AccessibilityUtilsTest.java 0 → 100644 +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); } }