Support title_with_entity and title_without_entity

1. Pull out TextClassifierImpl.LabeledIntent to LabeledIntent.
2. LabeledIntent.resolves takes a TitleChooser object, which
   allow custom logic like "if the resolved app is a browser, use
   title_with_entity. Otherwise, use title_without_entity".
   If TitleChooser is not set, the default behavior is to use
   title_with_entity if provided, use title_without_entity otherwise.
3. For classifyText, we use a TitleChooser that always return
   title_without_entity. So no behavior change in classifyText.
4. If custom titleChooser returns null, fallback to use the default
   titleChooser.

BUG: 124428508
BUG: 123946471

Test: atest framework/base/core/tests/coretests/src/android/view/textclassifier/

Change-Id: I7299c40ffc57deb9484d493f8c62b220a5a1e7d8
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
index 4d917a1..efdc968 100644
--- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -16,6 +16,7 @@
 
 package android.view.textclassifier;
 
+import android.annotation.Nullable;
 import android.app.Person;
 import android.content.Context;
 import android.text.TextUtils;
@@ -110,6 +111,19 @@
                 SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
     }
 
+    /**
+     * Returns a {@link android.view.textclassifier.LabeledIntent.TitleChooser} for
+     * conversation actions use case.
+     */
+    @Nullable
+    public static LabeledIntent.TitleChooser createTitleChooser(String actionType) {
+        if (ConversationAction.TYPE_OPEN_URL.equals(actionType)) {
+            return (labeledIntent, resolveInfo) -> resolveInfo.handleAllWebDataURI
+                    ? labeledIntent.titleWithEntity : labeledIntent.titleWithoutEntity;
+        }
+        return null;
+    }
+
     private static final class PersonEncoder {
         private final Map<Person, Integer> mMapping = new ArrayMap<>();
         private int mNextUserId = FIRST_NON_LOCAL_USER;
diff --git a/core/java/android/view/textclassifier/IntentFactory.java b/core/java/android/view/textclassifier/IntentFactory.java
index 9f3b97f..722c812 100644
--- a/core/java/android/view/textclassifier/IntentFactory.java
+++ b/core/java/android/view/textclassifier/IntentFactory.java
@@ -32,7 +32,7 @@
     /**
      * Return a list of LabeledIntent from the classification result.
      */
-    List<TextClassifierImpl.LabeledIntent> create(
+    List<LabeledIntent> create(
             Context context,
             String text,
             boolean foreignText,
@@ -43,9 +43,10 @@
      * Inserts translate action to the list if it is a foreign text.
      */
     static void insertTranslateAction(
-            List<TextClassifierImpl.LabeledIntent> actions, Context context, String text) {
-        actions.add(new TextClassifierImpl.LabeledIntent(
+            List<LabeledIntent> actions, Context context, String text) {
+        actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.translate),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.translate_desc),
                 new Intent(Intent.ACTION_TRANSLATE)
                         // TODO: Probably better to introduce a "translate" scheme instead of
diff --git a/core/java/android/view/textclassifier/LabeledIntent.java b/core/java/android/view/textclassifier/LabeledIntent.java
new file mode 100644
index 0000000..7544dc1
--- /dev/null
+++ b/core/java/android/view/textclassifier/LabeledIntent.java
@@ -0,0 +1,157 @@
+/*
+ * 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.view.textclassifier;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Helper class to store the information from which RemoteActions are built.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class LabeledIntent {
+    private static final String TAG = "LabeledIntent";
+    public static final int DEFAULT_REQUEST_CODE = 0;
+    private static final TitleChooser DEFAULT_TITLE_CHOOSER =
+            (labeledIntent, resolveInfo) -> {
+                if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) {
+                    return labeledIntent.titleWithEntity;
+                }
+                return labeledIntent.titleWithoutEntity;
+            };
+
+    @Nullable
+    public final String titleWithoutEntity;
+    @Nullable
+    public final String titleWithEntity;
+    public final String description;
+    // Do not update this intent.
+    public final Intent intent;
+    public final int requestCode;
+
+    /**
+     * Initializes a LabeledIntent.
+     *
+     * <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
+     * if distinguishing info (e.g. the classified text) is represented in intent extras only.
+     * In such circumstances, the request code should represent the distinguishing info
+     * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
+     * unique. To be correct, the PendingIntent should be definitely unique but we try a
+     * best effort approach that avoids spamming the system with PendingIntents.
+     */
+    // TODO: Fix the issue mentioned above so the behaviour is correct.
+    public LabeledIntent(
+            @Nullable String titleWithoutEntity,
+            @Nullable String titleWithEntity,
+            String description,
+            Intent intent,
+            int requestCode) {
+        if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) {
+            throw new IllegalArgumentException(
+                    "titleWithEntity and titleWithoutEntity should not be both null");
+        }
+        this.titleWithoutEntity = titleWithoutEntity;
+        this.titleWithEntity = titleWithEntity;
+        this.description = Preconditions.checkNotNull(description);
+        this.intent = Preconditions.checkNotNull(intent);
+        this.requestCode = requestCode;
+    }
+
+    /**
+     * Return the resolved result.
+     */
+    @Nullable
+    public Result resolve(
+            Context context, @Nullable TitleChooser titleChooser) {
+        final PackageManager pm = context.getPackageManager();
+        final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
+        final String packageName = resolveInfo != null && resolveInfo.activityInfo != null
+                ? resolveInfo.activityInfo.packageName : null;
+        Icon icon = null;
+        Intent resolvedIntent = new Intent(intent);
+        boolean shouldShowIcon = false;
+        if (packageName != null && !"android".equals(packageName)) {
+            // There is a default activity handling the intent.
+            resolvedIntent.setComponent(
+                    new ComponentName(packageName, resolveInfo.activityInfo.name));
+            if (resolveInfo.activityInfo.getIconResource() != 0) {
+                icon = Icon.createWithResource(
+                        packageName, resolveInfo.activityInfo.getIconResource());
+                shouldShowIcon = true;
+            }
+        }
+        if (icon == null) {
+            // RemoteAction requires that there be an icon.
+            icon = Icon.createWithResource("android",
+                    com.android.internal.R.drawable.ic_more_items);
+        }
+        final PendingIntent pendingIntent =
+                TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
+        if (pendingIntent == null) {
+            return null;
+        }
+        if (titleChooser == null) {
+            titleChooser = DEFAULT_TITLE_CHOOSER;
+        }
+        CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
+        if (TextUtils.isEmpty(title)) {
+            Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
+            title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
+        }
+        final RemoteAction action =
+                new RemoteAction(icon, title, description, pendingIntent);
+        action.setShouldShowIcon(shouldShowIcon);
+        return new Result(resolvedIntent, action);
+    }
+
+    /**
+     * Data class that holds the result.
+     */
+    public static final class Result {
+        public final Intent resolvedIntent;
+        public final RemoteAction remoteAction;
+
+        public Result(Intent resolvedIntent, RemoteAction remoteAction) {
+            this.resolvedIntent = Preconditions.checkNotNull(resolvedIntent);
+            this.remoteAction = Preconditions.checkNotNull(remoteAction);
+        }
+    }
+
+    /**
+     * An object to choose a title from resolved info.  If {@code null} is returned,
+     * {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise.
+     */
+    public interface TitleChooser {
+        /**
+         * Picks a title from a {@link LabeledIntent} by looking into resolved info.
+         */
+        @Nullable
+        CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
+    }
+}
diff --git a/core/java/android/view/textclassifier/LegacyIntentFactory.java b/core/java/android/view/textclassifier/LegacyIntentFactory.java
index 2d0d032..ea9229d 100644
--- a/core/java/android/view/textclassifier/LegacyIntentFactory.java
+++ b/core/java/android/view/textclassifier/LegacyIntentFactory.java
@@ -29,7 +29,6 @@
 import android.provider.Browser;
 import android.provider.CalendarContract;
 import android.provider.ContactsContract;
-import android.view.textclassifier.TextClassifierImpl.LabeledIntent;
 
 import com.google.android.textclassifier.AnnotatorModel;
 
@@ -100,8 +99,7 @@
             IntentFactory.insertTranslateAction(actions, context, text);
         }
         actions.forEach(
-                action -> action.getIntent()
-                        .putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
+                action -> action.intent.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
         return actions;
     }
 
@@ -110,12 +108,14 @@
         final List<LabeledIntent> actions = new ArrayList<>();
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.email),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.email_desc),
                 new Intent(Intent.ACTION_SENDTO)
                         .setData(Uri.parse(String.format("mailto:%s", text))),
                 LabeledIntent.DEFAULT_REQUEST_CODE));
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.add_contact),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.add_contact_desc),
                 new Intent(Intent.ACTION_INSERT_OR_EDIT)
                         .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
@@ -133,6 +133,7 @@
         if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
             actions.add(new LabeledIntent(
                     context.getString(com.android.internal.R.string.dial),
+                    /* titleWithEntity */ null,
                     context.getString(com.android.internal.R.string.dial_desc),
                     new Intent(Intent.ACTION_DIAL).setData(
                             Uri.parse(String.format("tel:%s", text))),
@@ -140,6 +141,7 @@
         }
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.add_contact),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.add_contact_desc),
                 new Intent(Intent.ACTION_INSERT_OR_EDIT)
                         .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
@@ -148,6 +150,7 @@
         if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
             actions.add(new LabeledIntent(
                     context.getString(com.android.internal.R.string.sms),
+                    /* titleWithEntity */ null,
                     context.getString(com.android.internal.R.string.sms_desc),
                     new Intent(Intent.ACTION_SENDTO)
                             .setData(Uri.parse(String.format("smsto:%s", text))),
@@ -163,6 +166,7 @@
             final String encText = URLEncoder.encode(text, "UTF-8");
             actions.add(new LabeledIntent(
                     context.getString(com.android.internal.R.string.map),
+                    /* titleWithEntity */ null,
                     context.getString(com.android.internal.R.string.map_desc),
                     new Intent(Intent.ACTION_VIEW)
                             .setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
@@ -181,6 +185,7 @@
         final List<LabeledIntent> actions = new ArrayList<>();
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.browse),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.browse_desc),
                 new Intent(Intent.ACTION_VIEW)
                         .setDataAndNormalize(Uri.parse(text))
@@ -211,6 +216,7 @@
         final List<LabeledIntent> actions = new ArrayList<>();
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.view_flight),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.view_flight_desc),
                 new Intent(Intent.ACTION_WEB_SEARCH)
                         .putExtra(SearchManager.QUERY, text),
@@ -225,6 +231,7 @@
         ContentUris.appendId(builder, parsedTime.toEpochMilli());
         return new LabeledIntent(
                 context.getString(com.android.internal.R.string.view_calendar),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.view_calendar_desc),
                 new Intent(Intent.ACTION_VIEW).setData(builder.build()),
                 LabeledIntent.DEFAULT_REQUEST_CODE);
@@ -236,6 +243,7 @@
         final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
         return new LabeledIntent(
                 context.getString(com.android.internal.R.string.add_calendar_event),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.add_calendar_event_desc),
                 new Intent(Intent.ACTION_INSERT)
                         .setData(CalendarContract.Events.CONTENT_URI)
@@ -252,6 +260,7 @@
         final List<LabeledIntent> actions = new ArrayList<>();
         actions.add(new LabeledIntent(
                 context.getString(com.android.internal.R.string.define),
+                /* titleWithEntity */ null,
                 context.getString(com.android.internal.R.string.define_desc),
                 new Intent(Intent.ACTION_DEFINE)
                         .putExtra(Intent.EXTRA_TEXT, text),
diff --git a/core/java/android/view/textclassifier/TemplateClassificationIntentFactory.java b/core/java/android/view/textclassifier/TemplateClassificationIntentFactory.java
index 2467802..ed0259f 100644
--- a/core/java/android/view/textclassifier/TemplateClassificationIntentFactory.java
+++ b/core/java/android/view/textclassifier/TemplateClassificationIntentFactory.java
@@ -48,12 +48,12 @@
     }
 
     /**
-     * Returns a list of {@link android.view.textclassifier.TextClassifierImpl.LabeledIntent}
+     * Returns a list of {@link android.view.textclassifier.LabeledIntent}
      * that are constructed from the classification result.
      */
     @NonNull
     @Override
-    public List<TextClassifierImpl.LabeledIntent> create(
+    public List<LabeledIntent> create(
             Context context,
             String text,
             boolean foreignText,
@@ -68,7 +68,7 @@
             Log.w(TAG, "RemoteActionTemplate is missing, fallback to LegacyIntentFactory.");
             return mFallback.create(context, text, foreignText, referenceTime, classification);
         }
-        final List<TextClassifierImpl.LabeledIntent> labeledIntents =
+        final List<LabeledIntent> labeledIntents =
                 mTemplateIntentFactory.create(remoteActionTemplates);
         if (foreignText) {
             IntentFactory.insertTranslateAction(labeledIntents, context, text.trim());
diff --git a/core/java/android/view/textclassifier/TemplateIntentFactory.java b/core/java/android/view/textclassifier/TemplateIntentFactory.java
index 95f88c7..0696d98 100644
--- a/core/java/android/view/textclassifier/TemplateIntentFactory.java
+++ b/core/java/android/view/textclassifier/TemplateIntentFactory.java
@@ -42,29 +42,29 @@
     private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
 
     @NonNull
-    public List<TextClassifierImpl.LabeledIntent> create(
+    public List<LabeledIntent> create(
             @Nullable RemoteActionTemplate[] remoteActionTemplates) {
         if (ArrayUtils.isEmpty(remoteActionTemplates)) {
             return Collections.emptyList();
         }
-        final List<TextClassifierImpl.LabeledIntent> labeledIntents = new ArrayList<>();
+        final List<LabeledIntent> labeledIntents = new ArrayList<>();
         for (RemoteActionTemplate remoteActionTemplate : remoteActionTemplates) {
             if (!isValidTemplate(remoteActionTemplate)) {
                 Log.w(TAG, "Invalid RemoteActionTemplate skipped.");
                 continue;
             }
             labeledIntents.add(
-                    new TextClassifierImpl.LabeledIntent(
-                            remoteActionTemplate.title,
+                    new LabeledIntent(
+                            remoteActionTemplate.titleWithoutEntity,
+                            remoteActionTemplate.titleWithEntity,
                             remoteActionTemplate.description,
                             createIntent(remoteActionTemplate),
                             remoteActionTemplate.requestCode == null
-                                    ? TextClassifierImpl.LabeledIntent.DEFAULT_REQUEST_CODE
+                                    ? LabeledIntent.DEFAULT_REQUEST_CODE
                                     : remoteActionTemplate.requestCode));
         }
         labeledIntents.forEach(
-                action -> action.getIntent()
-                        .putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
+                action -> action.intent.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
         return labeledIntents;
     }
 
@@ -73,7 +73,8 @@
             Log.w(TAG, "Invalid RemoteActionTemplate: is null");
             return false;
         }
-        if (TextUtils.isEmpty(remoteActionTemplate.title)) {
+        if (TextUtils.isEmpty(remoteActionTemplate.titleWithEntity)
+                && TextUtils.isEmpty(remoteActionTemplate.titleWithoutEntity)) {
             Log.w(TAG, "Invalid RemoteActionTemplate: title is null");
             return false;
         }
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index e628f19..632328b 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -19,21 +19,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
-import android.app.PendingIntent;
 import android.app.RemoteAction;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Icon;
 import android.icu.util.ULocale;
 import android.os.Bundle;
 import android.os.LocaleList;
 import android.os.ParcelFileDescriptor;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
@@ -430,7 +423,12 @@
             // Given that we only support implicit intent here, we should expect there is just one
             // intent for each action type.
             if (!labeledIntents.isEmpty()) {
-                remoteAction = labeledIntents.get(0).asRemoteAction(mContext);
+                LabeledIntent.TitleChooser titleChooser =
+                        ActionsSuggestionsHelper.createTitleChooser(actionType);
+                LabeledIntent.Result result = labeledIntents.get(0).resolve(mContext, titleChooser);
+                if (result != null) {
+                    remoteAction = result.remoteAction;
+                }
             }
             conversationActions.add(
                     new ConversationAction.Builder(actionType)
@@ -593,23 +591,26 @@
                 foreignLanguageBundle != null,
                 referenceTime,
                 highestScoringResult);
+        LabeledIntent.TitleChooser titleChooser =
+                (labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
         for (LabeledIntent labeledIntent : labeledIntents) {
-            final RemoteAction action = labeledIntent.asRemoteAction(mContext);
-            if (action == null) {
+            LabeledIntent.Result result = labeledIntent.resolve(mContext, titleChooser);
+            if (result == null) {
                 continue;
             }
+            final RemoteAction action = result.remoteAction;
             if (isPrimaryAction) {
                 // For O backwards compatibility, the first RemoteAction is also written to the
                 // legacy API fields.
                 builder.setIcon(action.getIcon().loadDrawable(mContext));
                 builder.setLabel(action.getTitle().toString());
-                builder.setIntent(labeledIntent.getIntent());
+                builder.setIntent(result.resolvedIntent);
                 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
                         TextClassification.createPendingIntent(mContext,
-                                labeledIntent.getIntent(), labeledIntent.getRequestCode())));
+                                result.resolvedIntent, labeledIntent.requestCode)));
                 isPrimaryAction = false;
             }
-            builder.addAction(action, labeledIntent.getIntent());
+            builder.addAction(action, result.resolvedIntent);
         }
 
         return builder.setId(createId(text, start, end)).build();
@@ -737,89 +738,5 @@
             return LocaleList.getDefault().get(0).toLanguageTag();
         }
     }
-
-    /**
-     * Helper class to store the information from which RemoteActions are built.
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    public static final class LabeledIntent {
-
-        static final int DEFAULT_REQUEST_CODE = 0;
-
-        private final String mTitle;
-        private final String mDescription;
-        private final Intent mIntent;
-        private final int mRequestCode;
-
-        /**
-         * Initializes a LabeledIntent.
-         *
-         * <p>NOTE: {@code reqestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
-         * if distinguishing info (e.g. the classified text) is represented in intent extras only.
-         * In such circumstances, the request code should represent the distinguishing info
-         * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
-         * unique. To be correct, the PendingIntent should be definitely unique but we try a
-         * best effort approach that avoids spamming the system with PendingIntents.
-         */
-        // TODO: Fix the issue mentioned above so the behaviour is correct.
-        LabeledIntent(String title, String description, Intent intent, int requestCode) {
-            mTitle = title;
-            mDescription = description;
-            mIntent = intent;
-            mRequestCode = requestCode;
-        }
-
-        @VisibleForTesting
-        public String getTitle() {
-            return mTitle;
-        }
-
-        @VisibleForTesting
-        public String getDescription() {
-            return mDescription;
-        }
-
-        @VisibleForTesting
-        public Intent getIntent() {
-            return mIntent;
-        }
-
-        @VisibleForTesting
-        public int getRequestCode() {
-            return mRequestCode;
-        }
-
-        @Nullable
-        RemoteAction asRemoteAction(Context context) {
-            final PackageManager pm = context.getPackageManager();
-            final ResolveInfo resolveInfo = pm.resolveActivity(mIntent, 0);
-            final String packageName = resolveInfo != null && resolveInfo.activityInfo != null
-                    ? resolveInfo.activityInfo.packageName : null;
-            Icon icon = null;
-            boolean shouldShowIcon = false;
-            if (packageName != null && !"android".equals(packageName)) {
-                // There is a default activity handling the intent.
-                mIntent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
-                if (resolveInfo.activityInfo.getIconResource() != 0) {
-                    icon = Icon.createWithResource(
-                            packageName, resolveInfo.activityInfo.getIconResource());
-                    shouldShowIcon = true;
-                }
-            }
-            if (icon == null) {
-                // RemoteAction requires that there be an icon.
-                icon = Icon.createWithResource("android",
-                        com.android.internal.R.drawable.ic_more_items);
-            }
-            final PendingIntent pendingIntent =
-                    TextClassification.createPendingIntent(context, mIntent, mRequestCode);
-            if (pendingIntent == null) {
-                return null;
-            }
-            final RemoteAction action = new RemoteAction(icon, mTitle, mDescription, pendingIntent);
-            action.setShouldShowIcon(shouldShowIcon);
-            return action;
-        }
-    }
 }
 
diff --git a/core/tests/coretests/src/android/view/textclassifier/LabeledIntentTest.java b/core/tests/coretests/src/android/view/textclassifier/LabeledIntentTest.java
new file mode 100644
index 0000000..e4e9cde
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/LabeledIntentTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.view.textclassifier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class LabeledIntentTest {
+    private static final String TITLE_WITHOUT_ENTITY = "Map";
+    private static final String TITLE_WITH_ENTITY = "Map NW14D1";
+    private static final String DESCRIPTION = "Check the map";
+    private static final Intent INTENT =
+            new Intent(Intent.ACTION_VIEW).setDataAndNormalize(Uri.parse("http://www.android.com"));
+    private static final int REQUEST_CODE = 42;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void resolve_preferTitleWithEntity() {
+        LabeledIntent labeledIntent = new LabeledIntent(
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
+                DESCRIPTION,
+                INTENT,
+                REQUEST_CODE
+        );
+
+        LabeledIntent.Result result =
+                labeledIntent.resolve(mContext, /*titleChooser*/ null);
+
+        assertThat(result).isNotNull();
+        assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITH_ENTITY);
+        assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
+        Intent intent = result.resolvedIntent;
+        assertThat(intent.getAction()).isEqualTo(intent.getAction());
+        assertThat(intent.getComponent()).isNotNull();
+    }
+
+    @Test
+    public void resolve_useAvailableTitle() {
+        LabeledIntent labeledIntent = new LabeledIntent(
+                TITLE_WITHOUT_ENTITY,
+                null,
+                DESCRIPTION,
+                INTENT,
+                REQUEST_CODE
+        );
+
+        LabeledIntent.Result result =
+                labeledIntent.resolve(mContext, /*titleChooser*/ null);
+
+        assertThat(result).isNotNull();
+        assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
+        assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
+        Intent intent = result.resolvedIntent;
+        assertThat(intent.getAction()).isEqualTo(intent.getAction());
+        assertThat(intent.getComponent()).isNotNull();
+    }
+
+    @Test
+    public void resolve_titleChooser() {
+        LabeledIntent labeledIntent = new LabeledIntent(
+                TITLE_WITHOUT_ENTITY,
+                null,
+                DESCRIPTION,
+                INTENT,
+                REQUEST_CODE
+        );
+
+        LabeledIntent.Result result =
+                labeledIntent.resolve(mContext, (labeledIntent1, resolveInfo) -> "chooser");
+
+        assertThat(result).isNotNull();
+        assertThat(result.remoteAction.getTitle()).isEqualTo("chooser");
+        assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
+        Intent intent = result.resolvedIntent;
+        assertThat(intent.getAction()).isEqualTo(intent.getAction());
+        assertThat(intent.getComponent()).isNotNull();
+    }
+
+    @Test
+    public void resolve_titleChooserReturnsNull() {
+        LabeledIntent labeledIntent = new LabeledIntent(
+                TITLE_WITHOUT_ENTITY,
+                null,
+                DESCRIPTION,
+                INTENT,
+                REQUEST_CODE
+        );
+
+        LabeledIntent.Result result =
+                labeledIntent.resolve(mContext, (labeledIntent1, resolveInfo) -> null);
+
+        assertThat(result).isNotNull();
+        assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
+        assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
+        Intent intent = result.resolvedIntent;
+        assertThat(intent.getAction()).isEqualTo(intent.getAction());
+        assertThat(intent.getComponent()).isNotNull();
+    }
+
+
+    @Test
+    public void resolve_missingTitle() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        new LabeledIntent(
+                                null,
+                                null,
+                                DESCRIPTION,
+                                INTENT,
+                                REQUEST_CODE
+                        ));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/textclassifier/LegacyIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/LegacyIntentFactoryTest.java
index 73d3eec..743818c 100644
--- a/core/tests/coretests/src/android/view/textclassifier/LegacyIntentFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/LegacyIntentFactoryTest.java
@@ -58,9 +58,12 @@
                         null,
                         null,
                         null,
+                        null,
+                        null,
+                        null,
                         null);
 
-        List<TextClassifierImpl.LabeledIntent> intents = mLegacyIntentFactory.create(
+        List<LabeledIntent> intents = mLegacyIntentFactory.create(
                 InstrumentationRegistry.getContext(),
                 TEXT,
                 /* foreignText */ false,
@@ -68,8 +71,8 @@
                 classificationResult);
 
         assertThat(intents).hasSize(1);
-        TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
-        Intent intent = labeledIntent.getIntent();
+        LabeledIntent labeledIntent = intents.get(0);
+        Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(Intent.ACTION_DEFINE);
         assertThat(intent.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(TEXT);
         assertThat(
@@ -89,9 +92,12 @@
                         null,
                         null,
                         null,
+                        null,
+                        null,
+                        null,
                         null);
 
-        List<TextClassifierImpl.LabeledIntent> intents = mLegacyIntentFactory.create(
+        List<LabeledIntent> intents = mLegacyIntentFactory.create(
                 InstrumentationRegistry.getContext(),
                 TEXT,
                 /* foreignText */ true,
@@ -99,7 +105,7 @@
                 classificationResult);
 
         assertThat(intents).hasSize(2);
-        assertThat(intents.get(0).getIntent().getAction()).isEqualTo(Intent.ACTION_DEFINE);
-        assertThat(intents.get(1).getIntent().getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
+        assertThat(intents.get(0).intent.getAction()).isEqualTo(Intent.ACTION_DEFINE);
+        assertThat(intents.get(1).intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
     }
 }
diff --git a/core/tests/coretests/src/android/view/textclassifier/TemplateClassificationIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/TemplateClassificationIntentFactoryTest.java
index d9dac31..9fd9e8e 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TemplateClassificationIntentFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TemplateClassificationIntentFactoryTest.java
@@ -40,7 +40,7 @@
 public class TemplateClassificationIntentFactoryTest {
 
     private static final String TEXT = "text";
-    private static final String TITLE = "Map";
+    private static final String TITLE_WITHOUT_ENTITY = "Map";
     private static final String DESCRIPTION = "Opens in Maps";
     private static final String ACTION = Intent.ACTION_VIEW;
 
@@ -69,9 +69,12 @@
                         null,
                         null,
                         null,
+                        null,
+                        null,
+                        null,
                         createRemoteActionTemplates());
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateClassificationIntentFactory.create(
                         InstrumentationRegistry.getContext(),
                         TEXT,
@@ -80,14 +83,14 @@
                         classificationResult);
 
         assertThat(intents).hasSize(2);
-        TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
-        assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
-        Intent intent = labeledIntent.getIntent();
+        LabeledIntent labeledIntent = intents.get(0);
+        assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
+        Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
         assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
 
         labeledIntent = intents.get(1);
-        intent = labeledIntent.getIntent();
+        intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
         assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
@@ -105,9 +108,12 @@
                         null,
                         null,
                         null,
+                        null,
+                        null,
+                        null,
                         createRemoteActionTemplates());
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateClassificationIntentFactory.create(
                         InstrumentationRegistry.getContext(),
                         TEXT,
@@ -116,9 +122,9 @@
                         classificationResult);
 
         assertThat(intents).hasSize(1);
-        TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
-        assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
-        Intent intent = labeledIntent.getIntent();
+        LabeledIntent labeledIntent = intents.get(0);
+        assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
+        Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
         assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
@@ -126,7 +132,8 @@
     private static RemoteActionTemplate[] createRemoteActionTemplates() {
         return new RemoteActionTemplate[]{
                 new RemoteActionTemplate(
-                        TITLE,
+                        TITLE_WITHOUT_ENTITY,
+                        null,
                         DESCRIPTION,
                         ACTION,
                         null,
diff --git a/core/tests/coretests/src/android/view/textclassifier/TemplateIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/TemplateIntentFactoryTest.java
index a1158a7..1860734 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TemplateIntentFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TemplateIntentFactoryTest.java
@@ -38,7 +38,8 @@
 public class TemplateIntentFactoryTest {
 
     private static final String TEXT = "text";
-    private static final String TITLE = "Map";
+    private static final String TITLE_WITHOUT_ENTITY = "Map";
+    private static final String TITLE_WITH_ENTITY = "Map NW14D1";
     private static final String DESCRIPTION = "Check the map";
     private static final String ACTION = Intent.ACTION_VIEW;
     private static final String DATA = Uri.parse("http://www.android.com").toString();
@@ -69,7 +70,8 @@
     @Test
     public void create_full() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
                 DESCRIPTION,
                 ACTION,
                 DATA,
@@ -81,15 +83,16 @@
                 REQUEST_CODE
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
 
         assertThat(intents).hasSize(1);
-        TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
-        assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
-        assertThat(labeledIntent.getDescription()).isEqualTo(DESCRIPTION);
-        assertThat(labeledIntent.getRequestCode()).isEqualTo(REQUEST_CODE);
-        Intent intent = labeledIntent.getIntent();
+        LabeledIntent labeledIntent = intents.get(0);
+        assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
+        assertThat(labeledIntent.titleWithEntity).isEqualTo(TITLE_WITH_ENTITY);
+        assertThat(labeledIntent.description).isEqualTo(DESCRIPTION);
+        assertThat(labeledIntent.requestCode).isEqualTo(REQUEST_CODE);
+        Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
         assertThat(intent.getData().toString()).isEqualTo(DATA);
         assertThat(intent.getType()).isEqualTo(TYPE);
@@ -104,7 +107,8 @@
     @Test
     public void normalizesScheme() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
                 DESCRIPTION,
                 ACTION,
                 "HTTp://www.android.com",
@@ -116,17 +120,18 @@
                 REQUEST_CODE
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
-        String data = intents.get(0).getIntent().getData().toString();
+        String data = intents.get(0).intent.getData().toString();
         assertThat(data).isEqualTo("http://www.android.com");
     }
 
     @Test
     public void create_minimal() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                null,
                 DESCRIPTION,
                 ACTION,
                 null,
@@ -138,16 +143,17 @@
                 null
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
 
         assertThat(intents).hasSize(1);
-        TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
-        assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
-        assertThat(labeledIntent.getDescription()).isEqualTo(DESCRIPTION);
-        assertThat(labeledIntent.getRequestCode()).isEqualTo(
-                TextClassifierImpl.LabeledIntent.DEFAULT_REQUEST_CODE);
-        Intent intent = labeledIntent.getIntent();
+        LabeledIntent labeledIntent = intents.get(0);
+        assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
+        assertThat(labeledIntent.titleWithEntity).isNull();
+        assertThat(labeledIntent.description).isEqualTo(DESCRIPTION);
+        assertThat(labeledIntent.requestCode).isEqualTo(
+                LabeledIntent.DEFAULT_REQUEST_CODE);
+        Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
         assertThat(intent.getData()).isNull();
         assertThat(intent.getType()).isNull();
@@ -161,7 +167,7 @@
     public void invalidTemplate_nullTemplate() {
         RemoteActionTemplate remoteActionTemplate = null;
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
         assertThat(intents).isEmpty();
@@ -170,7 +176,8 @@
     @Test
     public void invalidTemplate_nonEmptyPackageName() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
                 DESCRIPTION,
                 ACTION,
                 DATA,
@@ -182,7 +189,7 @@
                 REQUEST_CODE
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
         assertThat(intents).isEmpty();
@@ -192,6 +199,7 @@
     public void invalidTemplate_emptyTitle() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
                 null,
+                null,
                 DESCRIPTION,
                 ACTION,
                 null,
@@ -203,7 +211,7 @@
                 null
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
         assertThat(intents).isEmpty();
@@ -212,7 +220,8 @@
     @Test
     public void invalidTemplate_emptyDescription() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
                 null,
                 ACTION,
                 null,
@@ -224,7 +233,7 @@
                 null
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
         assertThat(intents).isEmpty();
@@ -233,7 +242,8 @@
     @Test
     public void invalidTemplate_emptyIntentAction() {
         RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
-                TITLE,
+                TITLE_WITHOUT_ENTITY,
+                TITLE_WITH_ENTITY,
                 DESCRIPTION,
                 null,
                 null,
@@ -245,7 +255,7 @@
                 null
         );
 
-        List<TextClassifierImpl.LabeledIntent> intents =
+        List<LabeledIntent> intents =
                 mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
 
         assertThat(intents).isEmpty();