Making TextClassifier helper objects parcelable

- EntityConfidence is no longer generic because it doesn't mix well with
  being Parcelable.
- Deprecated OnClick listeners in TextClassification as they can't be
  parceled. (Outright removed the secondary listeners that were not part
  of any release)
- Classes that were present in previous releases have their parceling
  factored out into ParcelableWrapper helper classes for backwards
  compatibility.

Bug: 67609167
Test: Added
Change-Id: I820ca4abc6b80f90007ab4424bc5df2a14f797b0
diff --git a/api/current.txt b/api/current.txt
index a75bbfa..7b3a49c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -49240,14 +49240,13 @@
     method public android.graphics.drawable.Drawable getSecondaryIcon(int);
     method public android.content.Intent getSecondaryIntent(int);
     method public java.lang.CharSequence getSecondaryLabel(int);
-    method public android.view.View.OnClickListener getSecondaryOnClickListener(int);
     method public java.lang.String getSignature();
     method public java.lang.String getText();
   }
 
   public static final class TextClassification.Builder {
     ctor public TextClassification.Builder();
-    method public android.view.textclassifier.TextClassification.Builder addSecondaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable, android.view.View.OnClickListener);
+    method public android.view.textclassifier.TextClassification.Builder addSecondaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable);
     method public android.view.textclassifier.TextClassification build();
     method public android.view.textclassifier.TextClassification.Builder clearSecondaryActions();
     method public android.view.textclassifier.TextClassification.Builder setEntityType(java.lang.String, float);
@@ -49255,15 +49254,18 @@
     method public android.view.textclassifier.TextClassification.Builder setIntent(android.content.Intent);
     method public android.view.textclassifier.TextClassification.Builder setLabel(java.lang.String);
     method public android.view.textclassifier.TextClassification.Builder setOnClickListener(android.view.View.OnClickListener);
-    method public android.view.textclassifier.TextClassification.Builder setPrimaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable, android.view.View.OnClickListener);
+    method public android.view.textclassifier.TextClassification.Builder setPrimaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable);
     method public android.view.textclassifier.TextClassification.Builder setSignature(java.lang.String);
     method public android.view.textclassifier.TextClassification.Builder setText(java.lang.String);
   }
 
-  public static final class TextClassification.Options {
+  public static final class TextClassification.Options implements android.os.Parcelable {
     ctor public TextClassification.Options();
+    method public int describeContents();
     method public android.os.LocaleList getDefaultLocales();
     method public android.view.textclassifier.TextClassification.Options setDefaultLocales(android.os.LocaleList);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassification.Options> CREATOR;
   }
 
   public final class TextClassificationManager {
@@ -49293,16 +49295,22 @@
     field public static final java.lang.String TYPE_URL = "url";
   }
 
-  public static final class TextClassifier.EntityConfig {
+  public static final class TextClassifier.EntityConfig implements android.os.Parcelable {
     ctor public TextClassifier.EntityConfig(int);
+    method public int describeContents();
     method public android.view.textclassifier.TextClassifier.EntityConfig excludeEntities(java.lang.String...);
     method public java.util.List<java.lang.String> getEntities(android.view.textclassifier.TextClassifier);
     method public android.view.textclassifier.TextClassifier.EntityConfig includeEntities(java.lang.String...);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassifier.EntityConfig> CREATOR;
   }
 
-  public final class TextLinks {
+  public final class TextLinks implements android.os.Parcelable {
     method public boolean apply(android.text.SpannableString, java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.text.style.ClickableSpan>);
+    method public int describeContents();
     method public java.util.Collection<android.view.textclassifier.TextLinks.TextLink> getLinks();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks> CREATOR;
   }
 
   public static final class TextLinks.Builder {
@@ -49311,21 +49319,27 @@
     method public android.view.textclassifier.TextLinks build();
   }
 
-  public static final class TextLinks.Options {
+  public static final class TextLinks.Options implements android.os.Parcelable {
     ctor public TextLinks.Options();
+    method public int describeContents();
     method public android.os.LocaleList getDefaultLocales();
     method public android.view.textclassifier.TextClassifier.EntityConfig getEntityConfig();
     method public android.view.textclassifier.TextLinks.Options setDefaultLocales(android.os.LocaleList);
     method public android.view.textclassifier.TextLinks.Options setEntityConfig(android.view.textclassifier.TextClassifier.EntityConfig);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.Options> CREATOR;
   }
 
-  public static final class TextLinks.TextLink {
+  public static final class TextLinks.TextLink implements android.os.Parcelable {
     ctor public TextLinks.TextLink(java.lang.String, int, int, java.util.Map<java.lang.String, java.lang.Float>);
+    method public int describeContents();
     method public float getConfidenceScore(java.lang.String);
     method public int getEnd();
     method public java.lang.String getEntity(int);
     method public int getEntityCount();
     method public int getStart();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.TextLink> CREATOR;
   }
 
   public final class TextSelection {
@@ -49344,10 +49358,13 @@
     method public android.view.textclassifier.TextSelection.Builder setSignature(java.lang.String);
   }
 
-  public static final class TextSelection.Options {
+  public static final class TextSelection.Options implements android.os.Parcelable {
     ctor public TextSelection.Options();
+    method public int describeContents();
     method public android.os.LocaleList getDefaultLocales();
     method public android.view.textclassifier.TextSelection.Options setDefaultLocales(android.os.LocaleList);
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextSelection.Options> CREATOR;
   }
 
 }
diff --git a/core/java/android/view/textclassifier/EntityConfidence.java b/core/java/android/view/textclassifier/EntityConfidence.java
index 19660d9..69a59a5 100644
--- a/core/java/android/view/textclassifier/EntityConfidence.java
+++ b/core/java/android/view/textclassifier/EntityConfidence.java
@@ -18,6 +18,8 @@
 
 import android.annotation.FloatRange;
 import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArrayMap;
 
 import com.android.internal.util.Preconditions;
@@ -30,17 +32,16 @@
 /**
  * Helper object for setting and getting entity scores for classified text.
  *
- * @param <T> the entity type.
  * @hide
  */
-final class EntityConfidence<T> {
+final class EntityConfidence implements Parcelable {
 
-    private final ArrayMap<T, Float> mEntityConfidence = new ArrayMap<>();
-    private final ArrayList<T> mSortedEntities = new ArrayList<>();
+    private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>();
+    private final ArrayList<String> mSortedEntities = new ArrayList<>();
 
     EntityConfidence() {}
 
-    EntityConfidence(@NonNull EntityConfidence<T> source) {
+    EntityConfidence(@NonNull EntityConfidence source) {
         Preconditions.checkNotNull(source);
         mEntityConfidence.putAll(source.mEntityConfidence);
         mSortedEntities.addAll(source.mSortedEntities);
@@ -54,24 +55,16 @@
      * @param source a map from entity to a confidence value in the range 0 (low confidence) to
      *               1 (high confidence).
      */
-    EntityConfidence(@NonNull Map<T, Float> source) {
+    EntityConfidence(@NonNull Map<String, Float> source) {
         Preconditions.checkNotNull(source);
 
         // Prune non-existent entities and clamp to 1.
         mEntityConfidence.ensureCapacity(source.size());
-        for (Map.Entry<T, Float> it : source.entrySet()) {
+        for (Map.Entry<String, Float> it : source.entrySet()) {
             if (it.getValue() <= 0) continue;
             mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue()));
         }
-
-        // Create a list of entities sorted by decreasing confidence for getEntities().
-        mSortedEntities.ensureCapacity(mEntityConfidence.size());
-        mSortedEntities.addAll(mEntityConfidence.keySet());
-        mSortedEntities.sort((e1, e2) -> {
-            float score1 = mEntityConfidence.get(e1);
-            float score2 = mEntityConfidence.get(e2);
-            return Float.compare(score2, score1);
-        });
+        resetSortedEntitiesFromMap();
     }
 
     /**
@@ -79,7 +72,7 @@
      * high confidence to low confidence.
      */
     @NonNull
-    public List<T> getEntities() {
+    public List<String> getEntities() {
         return Collections.unmodifiableList(mSortedEntities);
     }
 
@@ -89,7 +82,7 @@
      * classified text.
      */
     @FloatRange(from = 0.0, to = 1.0)
-    public float getConfidenceScore(T entity) {
+    public float getConfidenceScore(String entity) {
         if (mEntityConfidence.containsKey(entity)) {
             return mEntityConfidence.get(entity);
         }
@@ -100,4 +93,51 @@
     public String toString() {
         return mEntityConfidence.toString();
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mEntityConfidence.size());
+        for (Map.Entry<String, Float> entry : mEntityConfidence.entrySet()) {
+            dest.writeString(entry.getKey());
+            dest.writeFloat(entry.getValue());
+        }
+    }
+
+    public static final Parcelable.Creator<EntityConfidence> CREATOR =
+            new Parcelable.Creator<EntityConfidence>() {
+                @Override
+                public EntityConfidence createFromParcel(Parcel in) {
+                    return new EntityConfidence(in);
+                }
+
+                @Override
+                public EntityConfidence[] newArray(int size) {
+                    return new EntityConfidence[size];
+                }
+            };
+
+    private EntityConfidence(Parcel in) {
+        final int numEntities = in.readInt();
+        mEntityConfidence.ensureCapacity(numEntities);
+        for (int i = 0; i < numEntities; ++i) {
+            mEntityConfidence.put(in.readString(), in.readFloat());
+        }
+        resetSortedEntitiesFromMap();
+    }
+
+    private void resetSortedEntitiesFromMap() {
+        mSortedEntities.clear();
+        mSortedEntities.ensureCapacity(mEntityConfidence.size());
+        mSortedEntities.addAll(mEntityConfidence.keySet());
+        mSortedEntities.sort((e1, e2) -> {
+            float score1 = mEntityConfidence.get(e1);
+            float score2 = mEntityConfidence.get(e2);
+            return Float.compare(score2, score1);
+        });
+    }
 }
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java
index 7ffbf63..7089677 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -22,8 +22,13 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArrayMap;
 import android.view.View.OnClickListener;
 import android.view.textclassifier.TextClassifier.EntityType;
@@ -52,7 +57,7 @@
  *   Button button = new Button(context);
  *   button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
  *   button.setText(classification.getLabel());
- *   button.setOnClickListener(classification.getOnClickListener());
+ *   button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
  * }</pre>
  *
  * <p>e.g. starting an action mode with menu items that can handle the classified text:
@@ -90,7 +95,6 @@
  *       ...
  *   });
  * }</pre>
- *
  */
 public final class TextClassification {
 
@@ -99,6 +103,10 @@
      */
     static final TextClassification EMPTY = new TextClassification.Builder().build();
 
+    // TODO(toki): investigate a way to derive this based on device properties.
+    private static final int MAX_PRIMARY_ICON_SIZE = 192;
+    private static final int MAX_SECONDARY_ICON_SIZE = 144;
+
     @NonNull private final String mText;
     @Nullable private final Drawable mPrimaryIcon;
     @Nullable private final String mPrimaryLabel;
@@ -107,8 +115,7 @@
     @NonNull private final List<Drawable> mSecondaryIcons;
     @NonNull private final List<String> mSecondaryLabels;
     @NonNull private final List<Intent> mSecondaryIntents;
-    @NonNull private final List<OnClickListener> mSecondaryOnClickListeners;
-    @NonNull private final EntityConfidence<String> mEntityConfidence;
+    @NonNull private final EntityConfidence mEntityConfidence;
     @NonNull private final String mSignature;
 
     private TextClassification(
@@ -120,12 +127,10 @@
             @NonNull List<Drawable> secondaryIcons,
             @NonNull List<String> secondaryLabels,
             @NonNull List<Intent> secondaryIntents,
-            @NonNull List<OnClickListener> secondaryOnClickListeners,
             @NonNull Map<String, Float> entityConfidence,
             @NonNull String signature) {
         Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size());
         Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size());
-        Preconditions.checkArgument(secondaryOnClickListeners.size() == secondaryIntents.size());
         mText = text;
         mPrimaryIcon = primaryIcon;
         mPrimaryLabel = primaryLabel;
@@ -134,8 +139,7 @@
         mSecondaryIcons = secondaryIcons;
         mSecondaryLabels = secondaryLabels;
         mSecondaryIntents = secondaryIntents;
-        mSecondaryOnClickListeners = secondaryOnClickListeners;
-        mEntityConfidence = new EntityConfidence<>(entityConfidence);
+        mEntityConfidence = new EntityConfidence(entityConfidence);
         mSignature = signature;
     }
 
@@ -186,7 +190,6 @@
      * @see #getSecondaryIntent(int)
      * @see #getSecondaryLabel(int)
      * @see #getSecondaryIcon(int)
-     * @see #getSecondaryOnClickListener(int)
      */
     @IntRange(from = 0)
     public int getSecondaryActionsCount() {
@@ -198,13 +201,10 @@
      * classified text.
      *
      * @param index Index of the action to get the icon for.
-     *
      * @throws IndexOutOfBoundsException if the specified index is out of range.
-     *
      * @see #getSecondaryActionsCount() for the number of actions available.
      * @see #getSecondaryIntent(int)
      * @see #getSecondaryLabel(int)
-     * @see #getSecondaryOnClickListener(int)
      * @see #getIcon()
      */
     @Nullable
@@ -228,13 +228,10 @@
      * the classified text.
      *
      * @param index Index of the action to get the label for.
-     *
      * @throws IndexOutOfBoundsException if the specified index is out of range.
-     *
      * @see #getSecondaryActionsCount()
      * @see #getSecondaryIntent(int)
      * @see #getSecondaryIcon(int)
-     * @see #getSecondaryOnClickListener(int)
      * @see #getLabel()
      */
     @Nullable
@@ -257,13 +254,10 @@
      * Returns one of the <i>secondary</i> intents that may be fired to act on the classified text.
      *
      * @param index Index of the action to get the intent for.
-     *
      * @throws IndexOutOfBoundsException if the specified index is out of range.
-     *
      * @see #getSecondaryActionsCount()
      * @see #getSecondaryLabel(int)
      * @see #getSecondaryIcon(int)
-     * @see #getSecondaryOnClickListener(int)
      * @see #getIntent()
      */
     @Nullable
@@ -282,29 +276,10 @@
     }
 
     /**
-     * Returns one of the <i>secondary</i> OnClickListeners that may be triggered to act on the
-     * classified text.
-     *
-     * @param index Index of the action to get the click listener for.
-     *
-     * @throws IndexOutOfBoundsException if the specified index is out of range.
-     *
-     * @see #getSecondaryActionsCount()
-     * @see #getSecondaryIntent(int)
-     * @see #getSecondaryLabel(int)
-     * @see #getSecondaryIcon(int)
-     * @see #getOnClickListener()
-     */
-    @Nullable
-    public OnClickListener getSecondaryOnClickListener(int index) {
-        return mSecondaryOnClickListeners.get(index);
-    }
-
-    /**
      * Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified
-     * text.
-     *
-     * @see #getSecondaryOnClickListener(int)
+     * text. This field is not parcelable and will be null for all objects read from a parcel.
+     * Instead, call Context#startActivity(Intent) with the result of #getSecondaryIntent(int).
+     * Note that this may fail if the activity doesn't have permission to send the intent.
      */
     @Nullable
     public OnClickListener getOnClickListener() {
@@ -334,6 +309,42 @@
                 mSignature);
     }
 
+    /** Helper for parceling via #ParcelableWrapper. */
+    private void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mText);
+        final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+        dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+        if (primaryIconBitmap != null) {
+            primaryIconBitmap.writeToParcel(dest, flags);
+        }
+        dest.writeString(mPrimaryLabel);
+        dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+        if (mPrimaryIntent != null) {
+            mPrimaryIntent.writeToParcel(dest, flags);
+        }
+        // mPrimaryOnClickListener is not parcelable.
+        dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+        dest.writeStringList(mSecondaryLabels);
+        dest.writeTypedList(mSecondaryIntents);
+        mEntityConfidence.writeToParcel(dest, flags);
+        dest.writeString(mSignature);
+    }
+
+    /** Helper for unparceling via #ParcelableWrapper. */
+    private TextClassification(Parcel in) {
+        mText = in.readString();
+        mPrimaryIcon = in.readInt() == 0
+                ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+        mPrimaryLabel = in.readString();
+        mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+        mPrimaryOnClickListener = null;  // not parcelable
+        mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+        mSecondaryLabels = in.createStringArrayList();
+        mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+        mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+        mSignature = in.readString();
+    }
+
     /**
      * Creates an OnClickListener that starts an activity with the specified intent.
      *
@@ -349,6 +360,68 @@
     }
 
     /**
+     * Returns a Bitmap representation of the Drawable
+     *
+     * @param drawable The drawable to convert.
+     * @param maxDims The maximum edge length of the resulting bitmap (in pixels).
+     */
+    @Nullable
+    private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
+        if (drawable == null) {
+            return null;
+        }
+        final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
+        final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
+        final double scaleWidth = ((double) maxDims) / actualWidth;
+        final double scaleHeight = ((double) maxDims) / actualHeight;
+        final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
+        final int width = (int) (actualWidth * scale);
+        final int height = (int) (actualHeight * scale);
+        if (drawable instanceof BitmapDrawable) {
+            final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            if (actualWidth != width || actualHeight != height) {
+                return Bitmap.createScaledBitmap(
+                        bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
+            } else {
+                return bitmapDrawable.getBitmap();
+            }
+        } else {
+            final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+            final Canvas canvas = new Canvas(bitmap);
+            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+            drawable.draw(canvas);
+            return bitmap;
+        }
+    }
+
+    /**
+     * Returns a list of drawables converted to Bitmaps
+     *
+     * @param drawables The drawables to convert.
+     * @param maxDims The maximum edge length of the resulting bitmaps (in pixels).
+     */
+    private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) {
+        final List<Bitmap> bitmaps = new ArrayList<>(drawables.size());
+        for (Drawable drawable : drawables) {
+            bitmaps.add(drawableToBitmap(drawable, maxDims));
+        }
+        return bitmaps;
+    }
+
+    /** Returns a list of drawable wrappers for a list of bitmaps. */
+    private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) {
+        final List<Drawable> drawables = new ArrayList<>(bitmaps.size());
+        for (Bitmap bitmap : bitmaps) {
+            if (bitmap != null) {
+                drawables.add(new BitmapDrawable(null, bitmap));
+            } else {
+                drawables.add(null);
+            }
+        }
+        return drawables;
+    }
+
+    /**
      * Builder for building {@link TextClassification} objects.
      *
      * <p>e.g.
@@ -358,9 +431,9 @@
      *          .setText(classifiedText)
      *          .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
      *          .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
-     *          .setPrimaryAction(intent, label, icon, onClickListener)
-     *          .addSecondaryAction(intent1, label1, icon1, onClickListener1)
-     *          .addSecondaryAction(intent2, label2, icon2, onClickListener2)
+     *          .setPrimaryAction(intent, label, icon)
+     *          .addSecondaryAction(intent1, label1, icon1)
+     *          .addSecondaryAction(intent2, label2, icon2)
      *          .build();
      * }</pre>
      */
@@ -370,7 +443,6 @@
         @NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>();
         @NonNull private final List<String> mSecondaryLabels = new ArrayList<>();
         @NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>();
-        @NonNull private final List<OnClickListener> mSecondaryOnClickListeners = new ArrayList<>();
         @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
         @Nullable Drawable mPrimaryIcon;
         @Nullable String mPrimaryLabel;
@@ -413,16 +485,14 @@
          * <p><stong>Note: </stong> If all input parameters are set to null, this method will be a
          * no-op.
          *
-         * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+         * @see #setPrimaryAction(Intent, String, Drawable)
          */
         public Builder addSecondaryAction(
-                @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
-                @Nullable OnClickListener onClickListener) {
-            if (intent != null || label != null || icon != null || onClickListener != null) {
+                @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+            if (intent != null || label != null || icon != null) {
                 mSecondaryIntents.add(intent);
                 mSecondaryLabels.add(label);
                 mSecondaryIcons.add(icon);
-                mSecondaryOnClickListeners.add(onClickListener);
             }
             return this;
         }
@@ -432,7 +502,6 @@
          */
         public Builder clearSecondaryActions() {
             mSecondaryIntents.clear();
-            mSecondaryOnClickListeners.clear();
             mSecondaryLabels.clear();
             mSecondaryIcons.clear();
             return this;
@@ -440,26 +509,23 @@
 
         /**
          * Sets the <i>primary</i> action that may be performed on the classified text. This is
-         * equivalent to calling {@code
-         * setIntent(intent).setLabel(label).setIcon(icon).setOnClickListener(onClickListener)}.
+         * equivalent to calling {@code setIntent(intent).setLabel(label).setIcon(icon)}.
          *
          * <p><strong>Note: </strong>If all input parameters are null, there will be no
          * <i>primary</i> action but there may still be <i>secondary</i> actions.
          *
-         * @see #addSecondaryAction(Intent, String, Drawable, OnClickListener)
+         * @see #addSecondaryAction(Intent, String, Drawable)
          */
         public Builder setPrimaryAction(
-                @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
-                @Nullable OnClickListener onClickListener) {
-            return setIntent(intent).setLabel(label).setIcon(icon)
-                    .setOnClickListener(onClickListener);
+                @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+            return setIntent(intent).setLabel(label).setIcon(icon);
         }
 
         /**
          * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
          * on the classified text.
          *
-         * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+         * @see #setPrimaryAction(Intent, String, Drawable)
          */
         public Builder setIcon(@Nullable Drawable icon) {
             mPrimaryIcon = icon;
@@ -470,7 +536,7 @@
          * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
          * act on the classified text.
          *
-         * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+         * @see #setPrimaryAction(Intent, String, Drawable)
          */
         public Builder setLabel(@Nullable String label) {
             mPrimaryLabel = label;
@@ -481,7 +547,7 @@
          * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
          * text.
          *
-         * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+         * @see #setPrimaryAction(Intent, String, Drawable)
          */
         public Builder setIntent(@Nullable Intent intent) {
             mPrimaryIntent = intent;
@@ -490,9 +556,8 @@
 
         /**
          * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
-         * the classified text.
-         *
-         * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
+         * the classified text. This field is not parcelable and will always be null when the
+         * object is read from a parcel.
          */
         public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
             mPrimaryOnClickListener = onClickListener;
@@ -515,10 +580,8 @@
         public TextClassification build() {
             return new TextClassification(
                     mText,
-                    mPrimaryIcon, mPrimaryLabel,
-                    mPrimaryIntent, mPrimaryOnClickListener,
-                    mSecondaryIcons, mSecondaryLabels,
-                    mSecondaryIntents, mSecondaryOnClickListeners,
+                    mPrimaryIcon, mPrimaryLabel, mPrimaryIntent, mPrimaryOnClickListener,
+                    mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
                     mEntityConfidence, mSignature);
         }
     }
@@ -526,9 +589,11 @@
     /**
      * Optional input parameters for generating TextClassification.
      */
-    public static final class Options {
+    public static final class Options implements Parcelable {
 
-        private LocaleList mDefaultLocales;
+        private @Nullable LocaleList mDefaultLocales;
+
+        public Options() {}
 
         /**
          * @param defaultLocales ordered list of locale preferences that may be used to disambiguate
@@ -548,5 +613,80 @@
         public LocaleList getDefaultLocales() {
             return mDefaultLocales;
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mDefaultLocales != null ? 1 : 0);
+            if (mDefaultLocales != null) {
+                mDefaultLocales.writeToParcel(dest, flags);
+            }
+        }
+
+        public static final Parcelable.Creator<Options> CREATOR =
+                new Parcelable.Creator<Options>() {
+                    @Override
+                    public Options createFromParcel(Parcel in) {
+                        return new Options(in);
+                    }
+
+                    @Override
+                    public Options[] newArray(int size) {
+                        return new Options[size];
+                    }
+                };
+
+        private Options(Parcel in) {
+            if (in.readInt() > 0) {
+                mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+            }
+        }
+    }
+
+    /**
+     * Parcelable wrapper for TextClassification objects.
+     * @hide
+     */
+    public static final class ParcelableWrapper implements Parcelable {
+
+        @NonNull private TextClassification mTextClassification;
+
+        public ParcelableWrapper(@NonNull TextClassification textClassification) {
+            Preconditions.checkNotNull(textClassification);
+            mTextClassification = textClassification;
+        }
+
+        @NonNull
+        public TextClassification getTextClassification() {
+            return mTextClassification;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            mTextClassification.writeToParcel(dest, flags);
+        }
+
+        public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+                new Parcelable.Creator<ParcelableWrapper>() {
+                    @Override
+                    public ParcelableWrapper createFromParcel(Parcel in) {
+                        return new ParcelableWrapper(new TextClassification(in));
+                    }
+
+                    @Override
+                    public ParcelableWrapper[] newArray(int size) {
+                        return new ParcelableWrapper[size];
+                    }
+                };
+
     }
 }
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index ed60430..b602095 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -23,6 +23,8 @@
 import android.annotation.StringDef;
 import android.annotation.WorkerThread;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArraySet;
 
 import com.android.internal.util.Preconditions;
@@ -305,7 +307,7 @@
      *
      * Configs are initially based on a predefined preset, and can be modified from there.
      */
-    final class EntityConfig {
+    final class EntityConfig implements Parcelable {
         private final @TextClassifier.EntityPreset int mEntityPreset;
         private final Collection<String> mExcludedEntityTypes;
         private final Collection<String> mIncludedEntityTypes;
@@ -355,6 +357,37 @@
             }
             return Collections.unmodifiableList(entities);
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mEntityPreset);
+            dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
+            dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
+        }
+
+        public static final Parcelable.Creator<EntityConfig> CREATOR =
+                new Parcelable.Creator<EntityConfig>() {
+                    @Override
+                    public EntityConfig createFromParcel(Parcel in) {
+                        return new EntityConfig(in);
+                    }
+
+                    @Override
+                    public EntityConfig[] newArray(int size) {
+                        return new EntityConfig[size];
+                    }
+                };
+
+        private EntityConfig(Parcel in) {
+            mEntityPreset = in.readInt();
+            mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+            mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+        }
     }
 
     /**
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index aea3cb0..9c7be1e 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -32,7 +32,6 @@
 import android.provider.Settings;
 import android.text.util.Linkify;
 import android.util.Patterns;
-import android.view.View.OnClickListener;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
@@ -457,12 +456,10 @@
                     }
                 }
                 final String labelString = (label != null) ? label.toString() : null;
-                final OnClickListener onClickListener =
-                        TextClassification.createStartActivityOnClickListener(mContext, intent);
                 if (i == 0) {
-                    builder.setPrimaryAction(intent, labelString, icon, onClickListener);
+                    builder.setPrimaryAction(intent, labelString, icon);
                 } else {
-                    builder.addSecondaryAction(intent, labelString, icon, onClickListener);
+                    builder.addSecondaryAction(intent, labelString, icon);
                 }
             }
         }
diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java
index 6c587cf..ba854e0 100644
--- a/core/java/android/view/textclassifier/TextLinks.java
+++ b/core/java/android/view/textclassifier/TextLinks.java
@@ -20,6 +20,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.text.SpannableString;
 import android.text.style.ClickableSpan;
 import android.view.View;
@@ -38,7 +40,7 @@
  * A collection of links, representing subsequences of text and the entity types (phone number,
  * address, url, etc) they may be.
  */
-public final class TextLinks {
+public final class TextLinks implements Parcelable {
     private final String mFullText;
     private final List<TextLink> mLinks;
 
@@ -83,11 +85,40 @@
         return true;
     }
 
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mFullText);
+        dest.writeTypedList(mLinks);
+    }
+
+    public static final Parcelable.Creator<TextLinks> CREATOR =
+            new Parcelable.Creator<TextLinks>() {
+                @Override
+                public TextLinks createFromParcel(Parcel in) {
+                    return new TextLinks(in);
+                }
+
+                @Override
+                public TextLinks[] newArray(int size) {
+                    return new TextLinks[size];
+                }
+            };
+
+    private TextLinks(Parcel in) {
+        mFullText = in.readString();
+        mLinks = in.createTypedArrayList(TextLink.CREATOR);
+    }
+
     /**
      * A link, identifying a substring of text and possible entity types for it.
      */
-    public static final class TextLink {
-        private final EntityConfidence<String> mEntityScores;
+    public static final class TextLink implements Parcelable {
+        private final EntityConfidence mEntityScores;
         private final String mOriginalText;
         private final int mStart;
         private final int mEnd;
@@ -105,7 +136,7 @@
             mOriginalText = originalText;
             mStart = start;
             mEnd = end;
-            mEntityScores = new EntityConfidence<>(entityScores);
+            mEntityScores = new EntityConfidence(entityScores);
         }
 
         /**
@@ -153,16 +184,51 @@
                 @TextClassifier.EntityType String entityType) {
             return mEntityScores.getConfidenceScore(entityType);
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            mEntityScores.writeToParcel(dest, flags);
+            dest.writeString(mOriginalText);
+            dest.writeInt(mStart);
+            dest.writeInt(mEnd);
+        }
+
+        public static final Parcelable.Creator<TextLink> CREATOR =
+                new Parcelable.Creator<TextLink>() {
+                    @Override
+                    public TextLink createFromParcel(Parcel in) {
+                        return new TextLink(in);
+                    }
+
+                    @Override
+                    public TextLink[] newArray(int size) {
+                        return new TextLink[size];
+                    }
+                };
+
+        private TextLink(Parcel in) {
+            mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
+            mOriginalText = in.readString();
+            mStart = in.readInt();
+            mEnd = in.readInt();
+        }
     }
 
     /**
      * Optional input parameters for generating TextLinks.
      */
-    public static final class Options {
+    public static final class Options implements Parcelable {
 
         private LocaleList mDefaultLocales;
         private TextClassifier.EntityConfig mEntityConfig;
 
+        public Options() {}
+
         /**
          * @param defaultLocales ordered list of locale preferences that may be used to
          *                       disambiguate the provided text. If no locale preferences exist,
@@ -201,6 +267,45 @@
         public TextClassifier.EntityConfig getEntityConfig() {
             return mEntityConfig;
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mDefaultLocales != null ? 1 : 0);
+            if (mDefaultLocales != null) {
+                mDefaultLocales.writeToParcel(dest, flags);
+            }
+            dest.writeInt(mEntityConfig != null ? 1 : 0);
+            if (mEntityConfig != null) {
+                mEntityConfig.writeToParcel(dest, flags);
+            }
+        }
+
+        public static final Parcelable.Creator<Options> CREATOR =
+                new Parcelable.Creator<Options>() {
+                    @Override
+                    public Options createFromParcel(Parcel in) {
+                        return new Options(in);
+                    }
+
+                    @Override
+                    public Options[] newArray(int size) {
+                        return new Options[size];
+                    }
+                };
+
+        private Options(Parcel in) {
+            if (in.readInt() > 0) {
+                mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+            }
+            if (in.readInt() > 0) {
+                mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in);
+            }
+        }
     }
 
     /**
diff --git a/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java
index 25e9e7e..774d42d 100644
--- a/core/java/android/view/textclassifier/TextSelection.java
+++ b/core/java/android/view/textclassifier/TextSelection.java
@@ -21,6 +21,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArrayMap;
 import android.view.textclassifier.TextClassifier.EntityType;
 
@@ -36,7 +38,7 @@
 
     private final int mStartIndex;
     private final int mEndIndex;
-    @NonNull private final EntityConfidence<String> mEntityConfidence;
+    @NonNull private final EntityConfidence mEntityConfidence;
     @NonNull private final String mSignature;
 
     private TextSelection(
@@ -44,7 +46,7 @@
             @NonNull String signature) {
         mStartIndex = startIndex;
         mEndIndex = endIndex;
-        mEntityConfidence = new EntityConfidence<>(entityConfidence);
+        mEntityConfidence = new EntityConfidence(entityConfidence);
         mSignature = signature;
     }
 
@@ -110,6 +112,22 @@
                 mStartIndex, mEndIndex, mEntityConfidence, mSignature);
     }
 
+    /** Helper for parceling via #ParcelableWrapper. */
+    private void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mStartIndex);
+        dest.writeInt(mEndIndex);
+        mEntityConfidence.writeToParcel(dest, flags);
+        dest.writeString(mSignature);
+    }
+
+    /** Helper for unparceling via #ParcelableWrapper. */
+    private TextSelection(Parcel in) {
+        mStartIndex = in.readInt();
+        mEndIndex = in.readInt();
+        mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+        mSignature = in.readString();
+    }
+
     /**
      * Builder used to build {@link TextSelection} objects.
      */
@@ -170,11 +188,13 @@
     /**
      * Optional input parameters for generating TextSelection.
      */
-    public static final class Options {
+    public static final class Options implements Parcelable {
 
-        private LocaleList mDefaultLocales;
+        private @Nullable LocaleList mDefaultLocales;
         private boolean mDarkLaunchAllowed;
 
+        public Options() {}
+
         /**
          * @param defaultLocales ordered list of locale preferences that may be used to disambiguate
          *      the provided text. If no locale preferences exist, set this to null or an empty
@@ -216,5 +236,82 @@
         public boolean isDarkLaunchAllowed() {
             return mDarkLaunchAllowed;
         }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mDefaultLocales != null ? 1 : 0);
+            if (mDefaultLocales != null) {
+                mDefaultLocales.writeToParcel(dest, flags);
+            }
+            dest.writeInt(mDarkLaunchAllowed ? 1 : 0);
+        }
+
+        public static final Parcelable.Creator<Options> CREATOR =
+                new Parcelable.Creator<Options>() {
+                    @Override
+                    public Options createFromParcel(Parcel in) {
+                        return new Options(in);
+                    }
+
+                    @Override
+                    public Options[] newArray(int size) {
+                        return new Options[size];
+                    }
+                };
+
+        private Options(Parcel in) {
+            if (in.readInt() > 0) {
+                mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+            }
+            mDarkLaunchAllowed = in.readInt() != 0;
+        }
+    }
+
+    /**
+     * Parcelable wrapper for TextSelection objects.
+     * @hide
+     */
+    public static final class ParcelableWrapper implements Parcelable {
+
+        @NonNull private TextSelection mTextSelection;
+
+        public ParcelableWrapper(@NonNull TextSelection textSelection) {
+            Preconditions.checkNotNull(textSelection);
+            mTextSelection = textSelection;
+        }
+
+        @NonNull
+        public TextSelection getTextSelection() {
+            return mTextSelection;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            mTextSelection.writeToParcel(dest, flags);
+        }
+
+        public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+                new Parcelable.Creator<ParcelableWrapper>() {
+                    @Override
+                    public ParcelableWrapper createFromParcel(Parcel in) {
+                        return new ParcelableWrapper(new TextSelection(in));
+                    }
+
+                    @Override
+                    public ParcelableWrapper[] newArray(int size) {
+                        return new ParcelableWrapper[size];
+                    }
+                };
+
     }
 }
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index bddba07..b5ac330 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -4006,7 +4006,6 @@
             if (isValidAssistMenuItem(
                     textClassification.getIcon(),
                     textClassification.getLabel(),
-                    textClassification.getOnClickListener(),
                     textClassification.getIntent())) {
                 final MenuItem item = menu.add(
                         TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
@@ -4014,14 +4013,15 @@
                         .setIcon(textClassification.getIcon())
                         .setIntent(textClassification.getIntent());
                 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-                mAssistClickHandlers.put(item, textClassification.getOnClickListener());
+                mAssistClickHandlers.put(
+                        item, TextClassification.createStartActivityOnClickListener(
+                                mTextView.getContext(), textClassification.getIntent()));
             }
             final int count = textClassification.getSecondaryActionsCount();
             for (int i = 0; i < count; i++) {
                 if (!isValidAssistMenuItem(
                         textClassification.getSecondaryIcon(i),
                         textClassification.getSecondaryLabel(i),
-                        textClassification.getSecondaryOnClickListener(i),
                         textClassification.getSecondaryIntent(i))) {
                     continue;
                 }
@@ -4032,7 +4032,9 @@
                         .setIcon(textClassification.getSecondaryIcon(i))
                         .setIntent(textClassification.getSecondaryIntent(i));
                 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-                mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i));
+                mAssistClickHandlers.put(item,
+                        TextClassification.createStartActivityOnClickListener(
+                                mTextView.getContext(), textClassification.getSecondaryIntent(i)));
             }
         }
 
@@ -4048,10 +4050,9 @@
             }
         }
 
-        private boolean isValidAssistMenuItem(
-                Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) {
+        private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) {
             final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
-            final boolean hasAction = onClick != null || isSupportedIntent(intent);
+            final boolean hasAction = isSupportedIntent(intent);
             return hasUi && hasAction;
         }
 
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
new file mode 100644
index 0000000..9ee7fac
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 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.view.textclassifier;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextClassificationTest {
+
+    public BitmapDrawable generateTestDrawable(int width, int height, int colorValue) {
+        final int numPixels = width * height;
+        final int[] colors = new int[numPixels];
+        for (int i = 0; i < numPixels; ++i) {
+            colors[i] = colorValue;
+        }
+        final Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
+        final BitmapDrawable drawable = new BitmapDrawable(null, bitmap);
+        drawable.setTargetDensity(bitmap.getDensity());
+        return drawable;
+    }
+
+    @Test
+    public void testParcel() {
+        final String text = "text";
+        final BitmapDrawable primaryIcon = generateTestDrawable(16, 16, Color.RED);
+        final String primaryLabel = "primarylabel";
+        final Intent primaryIntent = new Intent("primaryintentaction");
+        final View.OnClickListener primaryOnClick = v -> { };
+        final BitmapDrawable secondaryIcon0 = generateTestDrawable(32, 288, Color.GREEN);
+        final String secondaryLabel0 = "secondarylabel0";
+        final Intent secondaryIntent0 = new Intent("secondaryintentaction0");
+        final BitmapDrawable secondaryIcon1 = generateTestDrawable(576, 288, Color.BLUE);
+        final String secondaryLabel1 = "secondaryLabel1";
+        final Intent secondaryIntent1 = null;
+        final BitmapDrawable secondaryIcon2 = null;
+        final String secondaryLabel2 = null;
+        final Intent secondaryIntent2 = new Intent("secondaryintentaction2");
+        final ColorDrawable secondaryIcon3 = new ColorDrawable(Color.CYAN);
+        final String secondaryLabel3 = null;
+        final Intent secondaryIntent3 = null;
+        final String signature = "signature";
+        final TextClassification reference = new TextClassification.Builder()
+                .setText(text)
+                .setPrimaryAction(primaryIntent, primaryLabel, primaryIcon)
+                .setOnClickListener(primaryOnClick)
+                .addSecondaryAction(null, null, null)  // ignored
+                .addSecondaryAction(secondaryIntent0, secondaryLabel0, secondaryIcon0)
+                .addSecondaryAction(secondaryIntent1, secondaryLabel1, secondaryIcon1)
+                .addSecondaryAction(secondaryIntent2, secondaryLabel2, secondaryIcon2)
+                .addSecondaryAction(secondaryIntent3, secondaryLabel3, secondaryIcon3)
+                .setEntityType(TextClassifier.TYPE_ADDRESS, 0.3f)
+                .setEntityType(TextClassifier.TYPE_PHONE, 0.7f)
+                .setSignature(signature)
+                .build();
+
+        // Parcel and unparcel using ParcelableWrapper.
+        final TextClassification.ParcelableWrapper parcelableReference = new TextClassification
+                .ParcelableWrapper(reference);
+        final Parcel parcel = Parcel.obtain();
+        parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+        parcel.setDataPosition(0);
+        final TextClassification result =
+                TextClassification.ParcelableWrapper.CREATOR.createFromParcel(
+                        parcel).getTextClassification();
+
+        assertEquals(text, result.getText());
+        assertEquals(signature, result.getSignature());
+        assertEquals(4, result.getSecondaryActionsCount());
+
+        // Primary action (re-use existing icon).
+        final Bitmap resPrimaryIcon = ((BitmapDrawable) result.getIcon()).getBitmap();
+        assertEquals(primaryIcon.getBitmap().getPixel(0, 0), resPrimaryIcon.getPixel(0, 0));
+        assertEquals(16, resPrimaryIcon.getWidth());
+        assertEquals(16, resPrimaryIcon.getHeight());
+        assertEquals(primaryLabel, result.getLabel());
+        assertEquals(primaryIntent.getAction(), result.getIntent().getAction());
+        assertEquals(null, result.getOnClickListener());  // Non-parcelable.
+
+        // Secondary action 0 (scale with  height limit).
+        final Bitmap resSecondaryIcon0 = ((BitmapDrawable) result.getSecondaryIcon(0)).getBitmap();
+        assertEquals(secondaryIcon0.getBitmap().getPixel(0, 0), resSecondaryIcon0.getPixel(0, 0));
+        assertEquals(16, resSecondaryIcon0.getWidth());
+        assertEquals(144, resSecondaryIcon0.getHeight());
+        assertEquals(secondaryLabel0, result.getSecondaryLabel(0));
+        assertEquals(secondaryIntent0.getAction(), result.getSecondaryIntent(0).getAction());
+
+        // Secondary action 1 (scale with width limit).
+        final Bitmap resSecondaryIcon1 = ((BitmapDrawable) result.getSecondaryIcon(1)).getBitmap();
+        assertEquals(secondaryIcon1.getBitmap().getPixel(0, 0), resSecondaryIcon1.getPixel(0, 0));
+        assertEquals(144, resSecondaryIcon1.getWidth());
+        assertEquals(72, resSecondaryIcon1.getHeight());
+        assertEquals(secondaryLabel1, result.getSecondaryLabel(1));
+        assertEquals(null, result.getSecondaryIntent(1));
+
+        // Secondary action 2 (no icon).
+        assertEquals(null, result.getSecondaryIcon(2));
+        assertEquals(null, result.getSecondaryLabel(2));
+        assertEquals(secondaryIntent2.getAction(), result.getSecondaryIntent(2).getAction());
+
+        // Secondary action 3 (convert non-bitmap drawable with negative size).
+        final Bitmap resSecondaryIcon3 = ((BitmapDrawable) result.getSecondaryIcon(3)).getBitmap();
+        assertEquals(secondaryIcon3.getColor(), resSecondaryIcon3.getPixel(0, 0));
+        assertEquals(1, resSecondaryIcon3.getWidth());
+        assertEquals(1, resSecondaryIcon3.getHeight());
+        assertEquals(null, result.getSecondaryLabel(3));
+        assertEquals(null, result.getSecondaryIntent(3));
+
+        // Entities.
+        assertEquals(2, result.getEntityCount());
+        assertEquals(TextClassifier.TYPE_PHONE, result.getEntity(0));
+        assertEquals(TextClassifier.TYPE_ADDRESS, result.getEntity(1));
+        assertEquals(0.7f, result.getConfidenceScore(TextClassifier.TYPE_PHONE), 1e-7f);
+        assertEquals(0.3f, result.getConfidenceScore(TextClassifier.TYPE_ADDRESS), 1e-7f);
+    }
+
+    @Test
+    public void testParcelOptions() {
+        TextClassification.Options reference = new TextClassification.Options();
+        reference.setDefaultLocales(new LocaleList(Locale.US, Locale.GERMANY));
+
+        // Parcel and unparcel.
+        final Parcel parcel = Parcel.obtain();
+        reference.writeToParcel(parcel, reference.describeContents());
+        parcel.setDataPosition(0);
+        TextClassification.Options result = TextClassification.Options.CREATOR.createFromParcel(
+                parcel);
+
+        assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags());
+    }
+}
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java
new file mode 100644
index 0000000..a82542c
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 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.view.textclassifier;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArrayMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextLinksTest {
+
+    private TextClassificationManager mTcm;
+    private TextClassifier mClassifier;
+
+    @Before
+    public void setup() {
+        mTcm = InstrumentationRegistry.getTargetContext()
+                .getSystemService(TextClassificationManager.class);
+        mTcm.setTextClassifier(null);
+        mClassifier = mTcm.getTextClassifier();
+    }
+
+    private Map<String, Float> getEntityScores(float address, float phone, float other) {
+        final Map<String, Float> result = new ArrayMap<>();
+        if (address > 0.f) {
+            result.put(TextClassifier.TYPE_ADDRESS, address);
+        }
+        if (phone > 0.f) {
+            result.put(TextClassifier.TYPE_PHONE, phone);
+        }
+        if (other > 0.f) {
+            result.put(TextClassifier.TYPE_OTHER, other);
+        }
+        return result;
+    }
+
+    @Test
+    public void testParcel() {
+        final String fullText = "this is just a test";
+        final TextLinks reference = new TextLinks.Builder(fullText)
+                .addLink(new TextLinks.TextLink(fullText, 0, 4, getEntityScores(0.f, 0.f, 1.f)))
+                .addLink(new TextLinks.TextLink(fullText, 5, 12, getEntityScores(.8f, .1f, .5f)))
+                .build();
+
+        // Parcel and unparcel.
+        final Parcel parcel = Parcel.obtain();
+        reference.writeToParcel(parcel, reference.describeContents());
+        parcel.setDataPosition(0);
+        final TextLinks result = TextLinks.CREATOR.createFromParcel(parcel);
+        final List<TextLinks.TextLink> resultList = new ArrayList<>(result.getLinks());
+
+        assertEquals(2, resultList.size());
+        assertEquals(0, resultList.get(0).getStart());
+        assertEquals(4, resultList.get(0).getEnd());
+        assertEquals(1, resultList.get(0).getEntityCount());
+        assertEquals(TextClassifier.TYPE_OTHER, resultList.get(0).getEntity(0));
+        assertEquals(1.f, resultList.get(0).getConfidenceScore(TextClassifier.TYPE_OTHER), 1e-7f);
+        assertEquals(5, resultList.get(1).getStart());
+        assertEquals(12, resultList.get(1).getEnd());
+        assertEquals(3, resultList.get(1).getEntityCount());
+        assertEquals(TextClassifier.TYPE_ADDRESS, resultList.get(1).getEntity(0));
+        assertEquals(TextClassifier.TYPE_OTHER, resultList.get(1).getEntity(1));
+        assertEquals(TextClassifier.TYPE_PHONE, resultList.get(1).getEntity(2));
+        assertEquals(.8f, resultList.get(1).getConfidenceScore(TextClassifier.TYPE_ADDRESS), 1e-7f);
+        assertEquals(.5f, resultList.get(1).getConfidenceScore(TextClassifier.TYPE_OTHER), 1e-7f);
+        assertEquals(.1f, resultList.get(1).getConfidenceScore(TextClassifier.TYPE_PHONE), 1e-7f);
+    }
+
+    @Test
+    public void testParcelOptions() {
+        TextClassifier.EntityConfig entityConfig = new TextClassifier.EntityConfig(
+                TextClassifier.ENTITY_PRESET_NONE);
+        entityConfig.includeEntities("a", "b", "c");
+        entityConfig.excludeEntities("b");
+        TextLinks.Options reference = new TextLinks.Options();
+        reference.setDefaultLocales(new LocaleList(Locale.US, Locale.GERMANY));
+        reference.setEntityConfig(entityConfig);
+
+        // Parcel and unparcel.
+        final Parcel parcel = Parcel.obtain();
+        reference.writeToParcel(parcel, reference.describeContents());
+        parcel.setDataPosition(0);
+        TextLinks.Options result = TextLinks.Options.CREATOR.createFromParcel(parcel);
+
+        assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags());
+        assertEquals(Arrays.asList("a", "c"), result.getEntityConfig().getEntities(mClassifier));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
new file mode 100644
index 0000000..e920236
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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.view.textclassifier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextSelectionTest {
+
+    @Test
+    public void testParcel() {
+        final int startIndex = 13;
+        final int endIndex = 37;
+        final String signature = "signature";
+        final TextSelection reference = new TextSelection.Builder(startIndex, endIndex)
+                .setEntityType(TextClassifier.TYPE_ADDRESS, 0.3f)
+                .setEntityType(TextClassifier.TYPE_PHONE, 0.7f)
+                .setEntityType(TextClassifier.TYPE_URL, 0.1f)
+                .setSignature(signature)
+                .build();
+
+        // Parcel and unparcel using ParcelableWrapper.
+        final TextSelection.ParcelableWrapper parcelableReference = new TextSelection
+                .ParcelableWrapper(reference);
+        final Parcel parcel = Parcel.obtain();
+        parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+        parcel.setDataPosition(0);
+        final TextSelection result =
+                TextSelection.ParcelableWrapper.CREATOR.createFromParcel(
+                        parcel).getTextSelection();
+
+        assertEquals(startIndex, result.getSelectionStartIndex());
+        assertEquals(endIndex, result.getSelectionEndIndex());
+        assertEquals(signature, result.getSignature());
+
+        assertEquals(3, result.getEntityCount());
+        assertEquals(TextClassifier.TYPE_PHONE, result.getEntity(0));
+        assertEquals(TextClassifier.TYPE_ADDRESS, result.getEntity(1));
+        assertEquals(TextClassifier.TYPE_URL, result.getEntity(2));
+        assertEquals(0.7f, result.getConfidenceScore(TextClassifier.TYPE_PHONE), 1e-7f);
+        assertEquals(0.3f, result.getConfidenceScore(TextClassifier.TYPE_ADDRESS), 1e-7f);
+        assertEquals(0.1f, result.getConfidenceScore(TextClassifier.TYPE_URL), 1e-7f);
+    }
+
+    @Test
+    public void testParcelOptions() {
+        TextSelection.Options reference = new TextSelection.Options();
+        reference.setDefaultLocales(new LocaleList(Locale.US, Locale.GERMANY));
+        reference.setDarkLaunchAllowed(true);
+
+        // Parcel and unparcel.
+        final Parcel parcel = Parcel.obtain();
+        reference.writeToParcel(parcel, reference.describeContents());
+        parcel.setDataPosition(0);
+        TextSelection.Options result = TextSelection.Options.CREATOR.createFromParcel(parcel);
+
+        assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags());
+        assertTrue(result.isDarkLaunchAllowed());
+    }
+}