Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c12035eb authored by Tony Mak's avatar Tony Mak
Browse files

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
parent 9a3ebed8
Loading
Loading
Loading
Loading
+52 −2
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package android.view.textclassifier;

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,11 +120,59 @@ public final class ActionsSuggestionsHelper {
    @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<>();
+17 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import java.util.ArrayList;
 */
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";
@@ -76,6 +77,22 @@ public final class ExtrasUtils {
        container.putParcelableArrayList(ACTIONS_INTENTS, actionsIntents);
    }

    /**
     * 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.
     */
+15 −10
Original line number Diff line number Diff line
@@ -91,15 +91,22 @@ public final class LabeledIntent {
            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 @@ public final class LabeledIntent {
        }
        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 final class LabeledIntent {
    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);
+2 −47
Original line number Diff line number Diff line
@@ -25,8 +25,6 @@ import android.app.PendingIntent;
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 @@ public final class TextClassification implements Parcelable {
     * @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);
    }

    /**
+11 −0
Original line number Diff line number Diff line
@@ -403,6 +403,12 @@ public final class TextClassifierImpl implements TextClassifier {
        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 @@ public final class TextClassifierImpl implements TextClassifier {
            }
            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 @@ public final class TextClassifierImpl implements TextClassifier {
                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 @@ public final class TextClassifierImpl implements TextClassifier {
                            .setConfidenceScore(nativeSuggestion.getScore())
                            .setTextReply(nativeSuggestion.getResponseText())
                            .setAction(remoteAction)
                            .setExtras(extras)
                            .build());
        }
        conversationActions =
                ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
        String resultId = ActionsSuggestionsHelper.createResultId(
                mContext,
                request.getConversation(),
Loading