Handle long text for share/cut/copy operations.

When TextView or EditText contains text that is larger than the
total parcelable limit, some of the FloatingToolbar operations would
crash.

This CL changes the behavior as follows:
- Show a toast message if cut or copy operation fails because it cannot
  set the primary clip.
- Trim the text for share and process_text actions
- A simple app with an EditText and a long text in it, would not open
since Autofill value was being sent over IPC. Trimmed the text that is
sent for Autofill feature.
- Trim the value send to accessibility services

Test: bit CtsWidgetTestCases:.TextViewTest
Test: bit FrameworksCoreTests:android.widget.TextViewTest
Test: bit FrameworksCoreTests:android.text.TextUtilsTest
Test: Manual sample app test

Bug: 8013261
Change-Id: Ia0df6b4eb4c13071a1bf75cedac7241c7239663c
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 440c88e..d946e96 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -17,6 +17,7 @@
 package android.text;
 
 import android.annotation.FloatRange;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.PluralsRes;
@@ -2024,6 +2025,45 @@
         builder.append(end);
     }
 
+    /**
+     * Intent size limitations prevent sending over a megabyte of data. Limit
+     * text length to 100K characters - 200KB.
+     */
+    private static final int PARCEL_SAFE_TEXT_LENGTH = 100000;
+
+    /**
+     * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if
+     * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled
+     * into a {@link Parcelable}.
+     *
+     * @hide
+     */
+    @Nullable
+    public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) {
+        return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH);
+    }
+
+    /**
+     * Trims the text to {@code size} length. Returns the string as it is if the length() is
+     * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate
+     * pair, returns a CharSequence of length {@code size-1}.
+     *
+     * @param size length of the result, should be greater than 0
+     *
+     * @hide
+     */
+    @Nullable
+    public static <T extends CharSequence> T trimToSize(@Nullable T text,
+            @IntRange(from = 1) int size) {
+        Preconditions.checkArgument(size > 0);
+        if (TextUtils.isEmpty(text) || text.length() <= size) return text;
+        if (Character.isHighSurrogate(text.charAt(size - 1))
+                && Character.isLowSurrogate(text.charAt(size))) {
+            size = size - 1;
+        }
+        return (T) text.subSequence(0, size);
+    }
+
     private static Object sLock = new Object();
 
     private static char[] sTemp = null;
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 04a8265..0d02444 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -6449,7 +6449,9 @@
 
         private boolean fireIntent(Intent intent) {
             if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
-                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
+                String selectedText = mTextView.getSelectedText();
+                selectedText = TextUtils.trimToParcelableSize(selectedText);
+                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
                 mEditor.mPreserveSelection = true;
                 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
                 return true;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 69edbbb..1ac0b36 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -22,6 +22,7 @@
 import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
 
 import android.R;
+import android.annotation.CheckResult;
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.FloatRange;
@@ -10328,10 +10329,22 @@
         return isTextEditable() ? AUTOFILL_TYPE_TEXT : AUTOFILL_TYPE_NONE;
     }
 
+    /**
+     * Gets the {@link TextView}'s current text for AutoFill. The value is trimmed to 100K
+     * {@code char}s if longer.
+     *
+     * @return current text, {@code null} if the text is not editable
+     *
+     * @see View#getAutofillValue()
+     */
     @Override
     @Nullable
     public AutofillValue getAutofillValue() {
-        return isTextEditable() ? AutofillValue.forText(getText()) : null;
+        if (isTextEditable()) {
+            final CharSequence text = TextUtils.trimToParcelableSize(getText());
+            return AutofillValue.forText(text);
+        }
+        return null;
     }
 
     /** @hide */
@@ -10745,7 +10758,7 @@
         }
 
         // Otherwise, return whatever text is being displayed.
-        return mTransformed;
+        return TextUtils.trimToParcelableSize(mTransformed);
     }
 
     void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
@@ -10830,13 +10843,25 @@
                 return true;
 
             case ID_CUT:
-                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
-                deleteText_internal(min, max);
+                final ClipData cutData = ClipData.newPlainText(null, getTransformedText(min, max));
+                if (setPrimaryClip(cutData)) {
+                    deleteText_internal(min, max);
+                } else {
+                    Toast.makeText(getContext(),
+                            com.android.internal.R.string.failed_to_copy_to_clipboard,
+                            Toast.LENGTH_SHORT).show();
+                }
                 return true;
 
             case ID_COPY:
-                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
-                stopTextActionMode();
+                final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
+                if (setPrimaryClip(copyData)) {
+                    stopTextActionMode();
+                } else {
+                    Toast.makeText(getContext(),
+                            com.android.internal.R.string.failed_to_copy_to_clipboard,
+                            Toast.LENGTH_SHORT).show();
+                }
                 return true;
 
             case ID_REPLACE:
@@ -11192,17 +11217,24 @@
             Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
             sharingIntent.setType("text/plain");
             sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
+            selectedText = TextUtils.trimToParcelableSize(selectedText);
             sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
             getContext().startActivity(Intent.createChooser(sharingIntent, null));
             Selection.setSelection((Spannable) mText, getSelectionEnd());
         }
     }
 
-    private void setPrimaryClip(ClipData clip) {
+    @CheckResult
+    private boolean setPrimaryClip(ClipData clip) {
         ClipboardManager clipboard =
                 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
-        clipboard.setPrimaryClip(clip);
+        try {
+            clipboard.setPrimaryClip(clip);
+        } catch (Throwable t) {
+            return false;
+        }
         sLastCutCopyOrTextChangedTime = SystemClock.uptimeMillis();
+        return true;
     }
 
     /**
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 8c26db4..8f05307 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2599,6 +2599,9 @@
     <!-- Item on EditText context menu. This action is used to cut selected the text into the clipboard. -->
     <string name="copy">Copy</string>
 
+    <!-- Error shown by TextView/EditText when cut/copy operation fails because text is too long to copy into the clipboard. -->
+    <string name="failed_to_copy_to_clipboard">Failed to copy to clipboard</string>
+
     <!-- Item on EditText context menu. This action is used to paste from the clipboard into the eidt field -->
     <string name="paste">Paste</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index beab29a..311f957 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2538,6 +2538,8 @@
   <java-symbol type="id" name="suggestionContainer" />
   <java-symbol type="id" name="addToDictionaryButton" />
   <java-symbol type="id" name="deleteButton" />
+  <!-- TextView -->
+  <java-symbol type="string" name="failed_to_copy_to_clipboard" />
 
   <java-symbol type="id" name="notification_material_reply_container" />
   <java-symbol type="id" name="notification_material_reply_text_1" />
diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java
index 472b3e2..46d361f 100644
--- a/core/tests/coretests/src/android/text/TextUtilsTest.java
+++ b/core/tests/coretests/src/android/text/TextUtilsTest.java
@@ -683,4 +683,36 @@
             assertEquals(source.getSpanFlags(span), result.getSpanFlags(span));
         }
     }
+
+    @Test
+    public void testTrimToSize() {
+        final String testString = "a\uD800\uDC00a";
+        assertEquals("Should return text as it is if size is longer than length",
+                testString, TextUtils.trimToSize(testString, 5));
+        assertEquals("Should return text as it is if size is equal to length",
+                testString, TextUtils.trimToSize(testString, 4));
+        assertEquals("Should trim text",
+                "a\uD800\uDC00", TextUtils.trimToSize(testString, 3));
+        assertEquals("Should trim surrogate pairs if size is in the middle of a pair",
+                "a", TextUtils.trimToSize(testString, 2));
+        assertEquals("Should trim text",
+                "a", TextUtils.trimToSize(testString, 1));
+        assertEquals("Should handle null",
+                null, TextUtils.trimToSize(null, 1));
+
+        assertEquals("Should trim high surrogate if invalid surrogate",
+                "a\uD800", TextUtils.trimToSize("a\uD800\uD800", 2));
+        assertEquals("Should trim low surrogate if invalid surrogate",
+                "a\uDC00", TextUtils.trimToSize("a\uDC00\uDC00", 2));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTrimToSizeThrowsExceptionForNegativeSize() {
+        TextUtils.trimToSize("", -1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testTrimToSizeThrowsExceptionForZeroSize() {
+        TextUtils.trimToSize("abc", 0);
+    }
 }
diff --git a/core/tests/coretests/src/android/widget/TextViewTest.java b/core/tests/coretests/src/android/widget/TextViewTest.java
index 1a1244f..5806bf1 100644
--- a/core/tests/coretests/src/android/widget/TextViewTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewTest.java
@@ -220,4 +220,33 @@
         assertTrue("Hyphenation must happen on TextView narrower than the word width",
                 hyphenationHappend);
     }
+
+    @Test
+    @UiThreadTest
+    public void testCopyShouldNotThrowException() throws Throwable {
+        mTextView = new TextView(mActivity);
+        mTextView.setTextIsSelectable(true);
+        mTextView.setText(createLongText());
+        mTextView.onTextContextMenuItem(TextView.ID_SELECT_ALL);
+        mTextView.onTextContextMenuItem(TextView.ID_COPY);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testCutShouldNotThrowException() throws Throwable {
+        mTextView = new TextView(mActivity);
+        mTextView.setTextIsSelectable(true);
+        mTextView.setText(createLongText());
+        mTextView.onTextContextMenuItem(TextView.ID_SELECT_ALL);
+        mTextView.onTextContextMenuItem(TextView.ID_CUT);
+    }
+
+    private String createLongText() {
+        int size = 600 * 1000;
+        final StringBuilder builder = new StringBuilder(size);
+        for (int i = 0; i < size; i++) {
+            builder.append('a');
+        }
+        return builder.toString();
+    }
 }