Merge "Create new API for ImageSpan"
diff --git a/api/current.txt b/api/current.txt
index 47cfe3c..d70fbdb 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -47136,7 +47136,9 @@
   public abstract class ReplacementSpan extends android.text.style.MetricAffectingSpan {
     ctor public ReplacementSpan();
     method public abstract void draw(@NonNull android.graphics.Canvas, CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, float, int, int, int, @NonNull android.graphics.Paint);
+    method @Nullable public CharSequence getContentDescription();
     method public abstract int getSize(@NonNull android.graphics.Paint, CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @Nullable android.graphics.Paint.FontMetricsInt);
+    method public void setContentDescription(@Nullable CharSequence);
     method public void updateDrawState(android.text.TextPaint);
     method public void updateMeasureState(android.text.TextPaint);
   }
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 81643e9..5bda867 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -36,6 +36,7 @@
 import android.sysprop.DisplayProperties;
 import android.text.style.AbsoluteSizeSpan;
 import android.text.style.AccessibilityClickableSpan;
+import android.text.style.AccessibilityReplacementSpan;
 import android.text.style.AccessibilityURLSpan;
 import android.text.style.AlignmentSpan;
 import android.text.style.BackgroundColorSpan;
@@ -735,6 +736,8 @@
     /** @hide */
     public static final int LINE_HEIGHT_SPAN = 28;
     /** @hide */
+    public static final int ACCESSIBILITY_REPLACEMENT_SPAN = 29;
+    /** @hide */
     public static final int LAST_SPAN = LINE_HEIGHT_SPAN;
 
     /**
@@ -860,7 +863,7 @@
 
                 case LEADING_MARGIN_SPAN:
                     readSpan(p, sp, new LeadingMarginSpan.Standard(p));
-                break;
+                    break;
 
                 case URL_SPAN:
                     readSpan(p, sp, new URLSpan(p));
@@ -933,7 +936,11 @@
                 case LINE_HEIGHT_SPAN:
                     readSpan(p, sp, new LineHeightSpan.Standard(p));
                     break;
-                    
+
+                case ACCESSIBILITY_REPLACEMENT_SPAN:
+                    readSpan(p, sp, new AccessibilityReplacementSpan(p));
+                    break;
+
                 default:
                     throw new RuntimeException("bogus span encoding " + kind);
                 }
diff --git a/core/java/android/text/style/AccessibilityReplacementSpan.java b/core/java/android/text/style/AccessibilityReplacementSpan.java
new file mode 100644
index 0000000..07b0975
--- /dev/null
+++ b/core/java/android/text/style/AccessibilityReplacementSpan.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 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 android.text.style;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.ParcelableSpan;
+import android.text.TextUtils;
+
+/**
+ * This class serves as a parcelable placeholder for the ReplacementSpans.
+ *
+ * This span contains content description of original span to let Accessibility service to do the
+ * substitution for it.
+ *
+ * @hide
+ */
+public class AccessibilityReplacementSpan extends ReplacementSpan
+        implements ParcelableSpan {
+    // The content description of the span this one replaces
+    private CharSequence mContentDescription;
+
+    /**
+     * @param contentDescription The content description of the span this one replaces
+     */
+    public AccessibilityReplacementSpan(CharSequence contentDescription) {
+        this.setContentDescription(contentDescription);
+        mContentDescription = contentDescription;
+    }
+
+    public AccessibilityReplacementSpan(Parcel p) {
+        mContentDescription = p.readCharSequence();
+    }
+
+    @Override
+    public int getSpanTypeId() {
+        return getSpanTypeIdInternal();
+    }
+
+    @Override
+    public int getSpanTypeIdInternal() {
+        return TextUtils.ACCESSIBILITY_REPLACEMENT_SPAN;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        writeToParcelInternal(dest, flags);
+    }
+
+    @Override
+    public void writeToParcelInternal(Parcel dest, int flags) {
+        dest.writeCharSequence(mContentDescription);
+    }
+
+    @Override
+    public int getSize(Paint paint, CharSequence text, int start, int end,
+            Paint.FontMetricsInt fm) {
+        return 0;
+    }
+
+    @Override
+    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
+            int bottom, Paint paint) {
+    }
+
+    public static final @android.annotation.NonNull
+    Parcelable.Creator<AccessibilityReplacementSpan> CREATOR =
+            new Parcelable.Creator<AccessibilityReplacementSpan>() {
+        @Override
+        public AccessibilityReplacementSpan createFromParcel(Parcel parcel) {
+            return new AccessibilityReplacementSpan(parcel);
+        }
+
+        @Override
+        public AccessibilityReplacementSpan[] newArray(int size) {
+            return new AccessibilityReplacementSpan[size];
+        }
+    };
+}
diff --git a/core/java/android/text/style/ReplacementSpan.java b/core/java/android/text/style/ReplacementSpan.java
index 5f94ad0..0553232 100644
--- a/core/java/android/text/style/ReplacementSpan.java
+++ b/core/java/android/text/style/ReplacementSpan.java
@@ -25,6 +25,8 @@
 
 public abstract class ReplacementSpan extends MetricAffectingSpan {
 
+    private CharSequence mContentDescription = null;
+
     /**
      * Returns the width of the span. Extending classes can set the height of the span by updating
      * attributes of {@link android.graphics.Paint.FontMetricsInt}. If the span covers the whole
@@ -61,6 +63,27 @@
                               int top, int y, int bottom, @NonNull Paint paint);
 
     /**
+     * Gets a brief description of this ImageSpan for use in accessibility support.
+     *
+     * @return The content description.
+     */
+    @Nullable
+    public CharSequence getContentDescription() {
+        return mContentDescription;
+    }
+
+    /**
+     * Sets the specific content description into ImageSpan.
+     * ReplacementSpans are shared with accessibility services,
+     * but only the content description is available from them.
+     *
+     * @param contentDescription content description. The default value is null.
+     */
+    public void setContentDescription(@Nullable CharSequence contentDescription) {
+        mContentDescription = contentDescription;
+    }
+
+    /**
      * This method does nothing, since ReplacementSpans are measured
      * explicitly instead of affecting Paint properties.
      */
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index cf29ed7..06e9d0d 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -39,8 +39,10 @@
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.AccessibilityClickableSpan;
+import android.text.style.AccessibilityReplacementSpan;
 import android.text.style.AccessibilityURLSpan;
 import android.text.style.ClickableSpan;
+import android.text.style.ReplacementSpan;
 import android.text.style.URLSpan;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -2641,37 +2643,86 @@
     public void setText(CharSequence text) {
         enforceNotSealed();
         mOriginalText = text;
-        // Replace any ClickableSpans in mText with placeholders
         if (text instanceof Spanned) {
-            ClickableSpan[] spans =
-                    ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
-            if (spans.length > 0) {
-                Spannable spannable = new SpannableStringBuilder(text);
-                for (int i = 0; i < spans.length; i++) {
-                    ClickableSpan span = spans[i];
-                    if ((span instanceof AccessibilityClickableSpan)
-                            || (span instanceof AccessibilityURLSpan)) {
-                        // We've already done enough
-                        break;
-                    }
-                    int spanToReplaceStart = spannable.getSpanStart(span);
-                    int spanToReplaceEnd = spannable.getSpanEnd(span);
-                    int spanToReplaceFlags = spannable.getSpanFlags(span);
-                    spannable.removeSpan(span);
-                    ClickableSpan replacementSpan = (span instanceof URLSpan)
-                            ? new AccessibilityURLSpan((URLSpan) span)
-                            : new AccessibilityClickableSpan(span.getId());
-                    spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd,
-                            spanToReplaceFlags);
-                }
-                mText = spannable;
-                return;
-            }
+            CharSequence tmpText = text;
+            tmpText = replaceClickableSpan(tmpText);
+            tmpText = replaceReplacementSpan(tmpText);
+            mText = tmpText;
+            return;
         }
         mText = (text == null) ? null : text.subSequence(0, text.length());
     }
 
     /**
+     * Replaces any ClickableSpans in mText with placeholders.
+     *
+     * @param text The text.
+     *
+     * @return The spannable with ClickableSpan replacement.
+     */
+    private CharSequence replaceClickableSpan(CharSequence text) {
+        ClickableSpan[] clickableSpans =
+                ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
+        Spannable spannable = new SpannableStringBuilder(text);
+        if (clickableSpans.length == 0) {
+            return text;
+        }
+        for (int i = 0; i < clickableSpans.length; i++) {
+            ClickableSpan span = clickableSpans[i];
+            if ((span instanceof AccessibilityClickableSpan)
+                    || (span instanceof AccessibilityURLSpan)) {
+                // We've already done enough
+                break;
+            }
+            int spanToReplaceStart = spannable.getSpanStart(span);
+            int spanToReplaceEnd = spannable.getSpanEnd(span);
+            int spanToReplaceFlags = spannable.getSpanFlags(span);
+            spannable.removeSpan(span);
+            ClickableSpan replacementSpan = (span instanceof URLSpan)
+                    ? new AccessibilityURLSpan((URLSpan) span)
+                    : new AccessibilityClickableSpan(span.getId());
+            spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd,
+                    spanToReplaceFlags);
+        }
+        return spannable;
+    }
+
+    /**
+     * Replace any ImageSpans in mText with its content description.
+     *
+     * @param text The text.
+     *
+     * @return The spannable with ReplacementSpan replacement.
+     */
+    private CharSequence replaceReplacementSpan(CharSequence text) {
+        ReplacementSpan[] replacementSpans =
+                ((Spanned) text).getSpans(0, text.length(), ReplacementSpan.class);
+        SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        if (replacementSpans.length == 0) {
+            return text;
+        }
+        for (int i = 0; i < replacementSpans.length; i++) {
+            ReplacementSpan span = replacementSpans[i];
+            CharSequence replacementText = span.getContentDescription();
+            if (span instanceof AccessibilityReplacementSpan) {
+                // We've already done enough
+                break;
+            }
+            if (replacementText == null) {
+                continue;
+            }
+            int spanToReplaceStart = spannable.getSpanStart(span);
+            int spanToReplaceEnd = spannable.getSpanEnd(span);
+            int spanToReplaceFlags = spannable.getSpanFlags(span);
+            spannable.removeSpan(span);
+            ReplacementSpan replacementSpan = new AccessibilityReplacementSpan(replacementText);
+            spannable.setSpan(replacementSpan, spanToReplaceStart, spanToReplaceEnd,
+                    spanToReplaceFlags);
+        }
+        return spannable;
+    }
+
+    /**
      * Gets the hint text of this node. Only applies to nodes where text can be entered.
      *
      * @return The hint text.