Deduplicate actions by their look
1. Deduplicate actions by their look. Ideally, we should compare
their labels and icons, but comparing icon is expensive and thus
we are comparing title + component name instead.
2. Put intent to extras in ConversationAction object
3. Updated LabeledIntent.resolve, so we only support activity intent
handler.
4. Fixed a minor issue in the browser title chooser.
If it is resolves to sharesheet / chooser, we will still show the URL.
BUG: 121200744
Test: atest frameworks/base/core/tests/coretests/src/android/view/textclassifier/
Change-Id: Ic7ea31eb0ac5e9386e8e4b428686a0b66726c96b
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
index efdc968..ddbff7b 100644
--- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -18,9 +18,11 @@
import android.annotation.Nullable;
import android.app.Person;
+import android.app.RemoteAction;
import android.content.Context;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
@@ -118,12 +120,60 @@
@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 (labeledIntent, resolveInfo) -> {
+ if (resolveInfo.handleAllWebDataURI) {
+ return labeledIntent.titleWithEntity;
+ }
+ if ("android".equals(resolveInfo.activityInfo.packageName)) {
+ return labeledIntent.titleWithEntity;
+ }
+ return labeledIntent.titleWithoutEntity;
+ };
}
return null;
}
+ /**
+ * Returns a list of {@link ConversationAction}s that have 0 duplicates. Two actions are
+ * duplicates if they may look the same to users. This function assumes every
+ * ConversationActions with a non-null RemoteAction also have a non-null intent in the extras.
+ */
+ public static List<ConversationAction> removeActionsWithDuplicates(
+ List<ConversationAction> conversationActions) {
+ // Ideally, we should compare title and icon here, but comparing icon is expensive and thus
+ // we use the component name of the target handler as the heuristic.
+ Map<Pair<String, String>, Integer> counter = new ArrayMap<>();
+ for (ConversationAction conversationAction : conversationActions) {
+ Pair<String, String> representation = getRepresentation(conversationAction);
+ if (representation == null) {
+ continue;
+ }
+ Integer existingCount = counter.getOrDefault(representation, 0);
+ counter.put(representation, existingCount + 1);
+ }
+ List<ConversationAction> result = new ArrayList<>();
+ for (ConversationAction conversationAction : conversationActions) {
+ Pair<String, String> representation = getRepresentation(conversationAction);
+ if (representation == null || counter.getOrDefault(representation, 0) == 1) {
+ result.add(conversationAction);
+ }
+ }
+ return result;
+ }
+
+ @Nullable
+ private static Pair<String, String> getRepresentation(
+ ConversationAction conversationAction) {
+ RemoteAction remoteAction = conversationAction.getAction();
+ if (remoteAction == null) {
+ return null;
+ }
+ return new Pair<>(
+ conversationAction.getAction().getTitle().toString(),
+ ExtrasUtils.getActionIntent(
+ conversationAction.getExtras()).getComponent().getPackageName());
+ }
+
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/ExtrasUtils.java b/core/java/android/view/textclassifier/ExtrasUtils.java
index 2ad17a8..df548ae 100644
--- a/core/java/android/view/textclassifier/ExtrasUtils.java
+++ b/core/java/android/view/textclassifier/ExtrasUtils.java
@@ -29,6 +29,7 @@
*/
public final class ExtrasUtils {
+ private static final String ACTION_INTENT = "action-intent";
private static final String ACTIONS_INTENTS = "actions-intents";
private static final String FOREIGN_LANGUAGE = "foreign-language";
private static final String ENTITY_TYPE = "entity-type";
@@ -77,6 +78,22 @@
}
/**
+ * Stores {@code actionIntents} information in TextClassifier response object's extras
+ * {@code container}.
+ */
+ public static void putActionIntent(Bundle container, @Nullable Intent actionIntent) {
+ container.putParcelable(ACTION_INTENT, actionIntent);
+ }
+
+ /**
+ * Returns {@code actionIntent} information contained in a TextClassifier response object.
+ */
+ @Nullable
+ public static Intent getActionIntent(Bundle container) {
+ return container.getParcelable(ACTION_INTENT);
+ }
+
+ /**
* Returns {@code actionIntents} information contained in the TextClassification object.
*/
@Nullable
diff --git a/core/java/android/view/textclassifier/LabeledIntent.java b/core/java/android/view/textclassifier/LabeledIntent.java
index 7544dc1..d2897b2 100644
--- a/core/java/android/view/textclassifier/LabeledIntent.java
+++ b/core/java/android/view/textclassifier/LabeledIntent.java
@@ -91,15 +91,22 @@
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;
+
+ if (resolveInfo == null || resolveInfo.activityInfo == null) {
+ Log.w(TAG, "resolveInfo or activityInfo is null");
+ return null;
+ }
+ final String packageName = resolveInfo.activityInfo.packageName;
+ final String className = resolveInfo.activityInfo.name;
+ if (packageName == null || className == null) {
+ Log.w(TAG, "packageName or className is null");
+ return null;
+ }
Intent resolvedIntent = new Intent(intent);
+ resolvedIntent.setComponent(new ComponentName(packageName, className));
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));
+ Icon icon = null;
+ if (!"android".equals(packageName)) {
if (resolveInfo.activityInfo.getIconResource() != 0) {
icon = Icon.createWithResource(
packageName, resolveInfo.activityInfo.getIconResource());
@@ -113,9 +120,6 @@
}
final PendingIntent pendingIntent =
TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
- if (pendingIntent == null) {
- return null;
- }
if (titleChooser == null) {
titleChooser = DEFAULT_TITLE_CHOOSER;
}
@@ -150,6 +154,7 @@
public interface TitleChooser {
/**
* Picks a title from a {@link LabeledIntent} by looking into resolved info.
+ * {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}.
*/
@Nullable
CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java
index 034da01..a275f0f 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -25,8 +25,6 @@
import android.app.RemoteAction;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.drawable.AdaptiveIconDrawable;
@@ -304,53 +302,10 @@
* @throws IllegalArgumentException if context or intent is null
* @hide
*/
- @Nullable
public static PendingIntent createPendingIntent(
@NonNull final Context context, @NonNull final Intent intent, int requestCode) {
- final int flags = PendingIntent.FLAG_UPDATE_CURRENT;
- switch (getIntentType(intent, context)) {
- case IntentType.ACTIVITY:
- return PendingIntent.getActivity(context, requestCode, intent, flags);
- case IntentType.SERVICE:
- return PendingIntent.getService(context, requestCode, intent, flags);
- default:
- return null;
- }
- }
-
- @IntentType
- private static int getIntentType(@NonNull Intent intent, @NonNull Context context) {
- Preconditions.checkArgument(context != null);
- Preconditions.checkArgument(intent != null);
-
- final ResolveInfo activityRI = context.getPackageManager().resolveActivity(intent, 0);
- if (activityRI != null) {
- if (context.getPackageName().equals(activityRI.activityInfo.packageName)) {
- return IntentType.ACTIVITY;
- }
- final boolean exported = activityRI.activityInfo.exported;
- if (exported && hasPermission(context, activityRI.activityInfo.permission)) {
- return IntentType.ACTIVITY;
- }
- }
-
- final ResolveInfo serviceRI = context.getPackageManager().resolveService(intent, 0);
- if (serviceRI != null) {
- if (context.getPackageName().equals(serviceRI.serviceInfo.packageName)) {
- return IntentType.SERVICE;
- }
- final boolean exported = serviceRI.serviceInfo.exported;
- if (exported && hasPermission(context, serviceRI.serviceInfo.permission)) {
- return IntentType.SERVICE;
- }
- }
-
- return IntentType.UNSUPPORTED;
- }
-
- private static boolean hasPermission(@NonNull Context context, @NonNull String permission) {
- return permission == null
- || context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
+ return PendingIntent.getActivity(
+ context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 632328b..293a230 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -403,6 +403,12 @@
return mFallback.suggestConversationActions(request);
}
+ /**
+ * Returns the {@link ConversationAction} result, with a non-null extras.
+ * <p>
+ * Whenever the RemoteAction is non-null, you can expect its corresponding intent
+ * with a non-null component name is in the extras.
+ */
private ConversationActions createConversationActionResult(
ConversationActions.Request request,
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
@@ -419,6 +425,7 @@
}
List<LabeledIntent> labeledIntents =
mTemplateIntentFactory.create(nativeSuggestion.getRemoteActionTemplates());
+ Bundle extras = new Bundle();
RemoteAction remoteAction = null;
// Given that we only support implicit intent here, we should expect there is just one
// intent for each action type.
@@ -428,6 +435,7 @@
LabeledIntent.Result result = labeledIntents.get(0).resolve(mContext, titleChooser);
if (result != null) {
remoteAction = result.remoteAction;
+ ExtrasUtils.putActionIntent(extras, result.resolvedIntent);
}
}
conversationActions.add(
@@ -435,8 +443,11 @@
.setConfidenceScore(nativeSuggestion.getScore())
.setTextReply(nativeSuggestion.getResponseText())
.setAction(remoteAction)
+ .setExtras(extras)
.build());
}
+ conversationActions =
+ ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
String resultId = ActionsSuggestionsHelper.createResultId(
mContext,
request.getConversation(),