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

Commit 92d76838 authored by Jan Althaus's avatar Jan Althaus
Browse files

Adding multiple action support to TextClassification

This introduces no user visible changes. All users of the TextClassifier
still just query the default action.

Bug: 320611
Test: Manually tested with a locally modified action mode that supports multiple actions.
Change-Id: I8e8714e04d70f4787ecf605bb7e27ef7d1af4d79
parent 6833a07a
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -48630,19 +48630,26 @@ package android.view.inputmethod {
package android.view.textclassifier {
  public final class TextClassification {
    method public int getActionCount();
    method public float getConfidenceScore(java.lang.String);
    method public java.lang.String getEntity(int);
    method public int getEntityCount();
    method public android.graphics.drawable.Drawable getIcon(int);
    method public android.graphics.drawable.Drawable getIcon();
    method public android.content.Intent getIntent(int);
    method public android.content.Intent getIntent();
    method public java.lang.CharSequence getLabel(int);
    method public java.lang.CharSequence getLabel();
    method public android.view.View.OnClickListener getOnClickListener(int);
    method public android.view.View.OnClickListener getOnClickListener();
    method public java.lang.String getText();
  }
  public static final class TextClassification.Builder {
    ctor public TextClassification.Builder();
    method public android.view.textclassifier.TextClassification.Builder addAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable, android.view.View.OnClickListener);
    method public android.view.textclassifier.TextClassification build();
    method public android.view.textclassifier.TextClassification.Builder clearActions();
    method public android.view.textclassifier.TextClassification.Builder setEntityType(java.lang.String, float);
    method public android.view.textclassifier.TextClassification.Builder setIcon(android.graphics.drawable.Drawable);
    method public android.view.textclassifier.TextClassification.Builder setIntent(android.content.Intent);
+7 −0
Original line number Diff line number Diff line
@@ -52324,19 +52324,26 @@ package android.view.inputmethod {
package android.view.textclassifier {
  public final class TextClassification {
    method public int getActionCount();
    method public float getConfidenceScore(java.lang.String);
    method public java.lang.String getEntity(int);
    method public int getEntityCount();
    method public android.graphics.drawable.Drawable getIcon(int);
    method public android.graphics.drawable.Drawable getIcon();
    method public android.content.Intent getIntent(int);
    method public android.content.Intent getIntent();
    method public java.lang.CharSequence getLabel(int);
    method public java.lang.CharSequence getLabel();
    method public android.view.View.OnClickListener getOnClickListener(int);
    method public android.view.View.OnClickListener getOnClickListener();
    method public java.lang.String getText();
  }
  public static final class TextClassification.Builder {
    ctor public TextClassification.Builder();
    method public android.view.textclassifier.TextClassification.Builder addAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable, android.view.View.OnClickListener);
    method public android.view.textclassifier.TextClassification build();
    method public android.view.textclassifier.TextClassification.Builder clearActions();
    method public android.view.textclassifier.TextClassification.Builder setEntityType(java.lang.String, float);
    method public android.view.textclassifier.TextClassification.Builder setIcon(android.graphics.drawable.Drawable);
    method public android.view.textclassifier.TextClassification.Builder setIntent(android.content.Intent);
+7 −0
Original line number Diff line number Diff line
@@ -49187,19 +49187,26 @@ package android.view.inputmethod {
package android.view.textclassifier {
  public final class TextClassification {
    method public int getActionCount();
    method public float getConfidenceScore(java.lang.String);
    method public java.lang.String getEntity(int);
    method public int getEntityCount();
    method public android.graphics.drawable.Drawable getIcon(int);
    method public android.graphics.drawable.Drawable getIcon();
    method public android.content.Intent getIntent(int);
    method public android.content.Intent getIntent();
    method public java.lang.CharSequence getLabel(int);
    method public java.lang.CharSequence getLabel();
    method public android.view.View.OnClickListener getOnClickListener(int);
    method public android.view.View.OnClickListener getOnClickListener();
    method public java.lang.String getText();
  }
  public static final class TextClassification.Builder {
    ctor public TextClassification.Builder();
    method public android.view.textclassifier.TextClassification.Builder addAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable, android.view.View.OnClickListener);
    method public android.view.textclassifier.TextClassification build();
    method public android.view.textclassifier.TextClassification.Builder clearActions();
    method public android.view.textclassifier.TextClassification.Builder setEntityType(java.lang.String, float);
    method public android.view.textclassifier.TextClassification.Builder setIcon(android.graphics.drawable.Drawable);
    method public android.view.textclassifier.TextClassification.Builder setIntent(android.content.Intent);
+153 −35
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.view.textclassifier.TextClassifier.EntityType;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.List;

/**
@@ -41,10 +42,10 @@ public final class TextClassification {
    static final TextClassification EMPTY = new TextClassification.Builder().build();

    @NonNull private final String mText;
    @Nullable private final Drawable mIcon;
    @Nullable private final String mLabel;
    @Nullable private final Intent mIntent;
    @Nullable private final OnClickListener mOnClickListener;
    @NonNull private final List<Drawable> mIcons;
    @NonNull private final List<String> mLabels;
    @NonNull private final List<Intent> mIntents;
    @NonNull private final List<OnClickListener> mOnClickListeners;
    @NonNull private final EntityConfidence<String> mEntityConfidence;
    @NonNull private final List<String> mEntities;
    private int mLogType;
@@ -52,18 +53,21 @@ public final class TextClassification {

    private TextClassification(
            @Nullable String text,
            @Nullable Drawable icon,
            @Nullable String label,
            @Nullable Intent intent,
            @Nullable OnClickListener onClickListener,
            @NonNull List<Drawable> icons,
            @NonNull List<String> labels,
            @NonNull List<Intent> intents,
            @NonNull List<OnClickListener> onClickListeners,
            @NonNull EntityConfidence<String> entityConfidence,
            int logType,
            @NonNull String versionInfo) {
        Preconditions.checkArgument(labels.size() == intents.size());
        Preconditions.checkArgument(icons.size() == intents.size());
        Preconditions.checkArgument(onClickListeners.size() == intents.size());
        mText = text;
        mIcon = icon;
        mLabel = label;
        mIntent = intent;
        mOnClickListener = onClickListener;
        mIcons = icons;
        mLabels = labels;
        mIntents = intents;
        mOnClickListeners = onClickListeners;
        mEntityConfidence = new EntityConfidence<>(entityConfidence);
        mEntities = mEntityConfidence.getEntities();
        mLogType = logType;
@@ -109,35 +113,106 @@ public final class TextClassification {
    }

    /**
     * Returns an icon that may be rendered on a widget used to act on the classified text.
     * Returns the number of actions that are available to act on the classified text.
     * @see #getIntent(int)
     * @see #getLabel(int)
     * @see #getIcon(int)
     * @see #getOnClickListener(int)
     */
    @IntRange(from = 0)
    public int getActionCount() {
        return mIntents.size();
    }

    /**
     * Returns one of the icons that maybe rendered on a widget used to act on the classified text.
     * @param index Index of the action to get the icon for.
     * @throws IndexOutOfBoundsException if the specified index is out of range.
     * @see #getActionCount() for the number of entities available.
     * @see #getIntent(int)
     * @see #getLabel(int)
     * @see #getOnClickListener(int)
     */
    @Nullable
    public Drawable getIcon(int index) {
        return mIcons.get(index);
    }

    /**
     * Returns an icon for the default intent that may be rendered on a widget used to act on the
     * classified text.
     */
    @Nullable
    public Drawable getIcon() {
        return mIcon;
        return mIcons.isEmpty() ? null : mIcons.get(0);
    }

    /**
     * Returns a label that may be rendered on a widget used to act on the classified text.
     * Returns one of the labels that may be rendered on a widget used to act on 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 #getActionCount()
     * @see #getIntent(int)
     * @see #getIcon(int)
     * @see #getOnClickListener(int)
     */
    @Nullable
    public CharSequence getLabel(int index) {
        return mLabels.get(index);
    }

    /**
     * Returns a label for the default intent that may be rendered on a widget used to act on the
     * classified text.
     */
    @Nullable
    public CharSequence getLabel() {
        return mLabel;
        return mLabels.isEmpty() ? null : mLabels.get(0);
    }

    /**
     * Returns one of the 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 #getActionCount()
     * @see #getLabel(int)
     * @see #getIcon(int)
     * @see #getOnClickListener(int)
     */
    @Nullable
    public Intent getIntent(int index) {
        return mIntents.get(index);
    }

    /**
     * Returns an intent that may be fired to act on the classified text.
     * Returns the default intent that may be fired to act on the classified text.
     */
    @Nullable
    public Intent getIntent() {
        return mIntent;
        return mIntents.isEmpty() ? null : mIntents.get(0);
    }

    /**
     * Returns one of the 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 #getActionCount()
     * @see #getIntent(int)
     * @see #getLabel(int)
     * @see #getIcon(int)
     */
    @Nullable
    public OnClickListener getOnClickListener(int index) {
        return mOnClickListeners.get(index);
    }

    /**
     * Returns an OnClickListener that may be triggered to act on the classified text.
     * Returns the default OnClickListener that may be triggered to act on the classified text.
     */
    @Nullable
    public OnClickListener getOnClickListener() {
        return mOnClickListener;
        return mOnClickListeners.isEmpty() ? null : mOnClickListeners.get(0);
    }

    /**
@@ -160,8 +235,8 @@ public final class TextClassification {
    @Override
    public String toString() {
        return String.format("TextClassification {"
                        + "text=%s, entities=%s, label=%s, intent=%s}",
                mText, mEntityConfidence, mLabel, mIntent);
                        + "text=%s, entities=%s, labels=%s, intents=%s}",
                mText, mEntityConfidence, mLabels, mIntents);
    }

    /**
@@ -184,10 +259,10 @@ public final class TextClassification {
    public static final class Builder {

        @NonNull private String mText;
        @Nullable private Drawable mIcon;
        @Nullable private String mLabel;
        @Nullable private Intent mIntent;
        @Nullable private OnClickListener mOnClickListener;
        @NonNull private final List<Drawable> mIcons = new ArrayList<>();
        @NonNull private final List<String> mLabels = new ArrayList<>();
        @NonNull private final List<Intent> mIntents = new ArrayList<>();
        @NonNull private final List<OnClickListener> mOnClickListeners = new ArrayList<>();
        @NonNull private final EntityConfidence<String> mEntityConfidence =
                new EntityConfidence<>();
        private int mLogType;
@@ -216,26 +291,57 @@ public final class TextClassification {
        }

        /**
         * Sets an icon that may be rendered on a widget used to act on the classified text.
         * Adds an action that may be performed on the classified text. The label and icon are used
         * for rendering of widgets that offer the intent. Actions should be added in order of
         * priority and the first one will be treated as the default.
         */
        public Builder addAction(
                Intent intent, @Nullable String label, @Nullable Drawable icon,
                @Nullable OnClickListener onClickListener) {
            mIntents.add(intent);
            mLabels.add(label);
            mIcons.add(icon);
            mOnClickListeners.add(onClickListener);
            return this;
        }

        /**
         * Removes all actions.
         */
        public Builder clearActions() {
            mIntents.clear();
            mOnClickListeners.clear();
            mLabels.clear();
            mIcons.clear();
            return this;
        }

        /**
         * Sets the icon for the default action that may be rendered on a widget used to act on the
         * classified text.
         */
        public Builder setIcon(@Nullable Drawable icon) {
            mIcon = icon;
            ensureDefaultActionAvailable();
            mIcons.set(0, icon);
            return this;
        }

        /**
         * Sets a label that may be rendered on a widget used to act on the classified text.
         * Sets the label for the default action that may be rendered on a widget used to act on the
         * classified text.
         */
        public Builder setLabel(@Nullable String label) {
            mLabel = label;
            ensureDefaultActionAvailable();
            mLabels.set(0, label);
            return this;
        }

        /**
         * Sets an intent that may be fired to act on the classified text.
         * Sets the intent for the default action that may be fired to act on the classified text.
         */
        public Builder setIntent(@Nullable Intent intent) {
            mIntent = intent;
            ensureDefaultActionAvailable();
            mIntents.set(0, intent);
            return this;
        }

@@ -249,10 +355,12 @@ public final class TextClassification {
        }

        /**
         * Sets an OnClickListener that may be triggered to act on the classified text.
         * Sets the OnClickListener for the default action that may be triggered to act on the
         * classified text.
         */
        public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
            mOnClickListener = onClickListener;
            ensureDefaultActionAvailable();
            mOnClickListeners.set(0, onClickListener);
            return this;
        }

@@ -265,12 +373,22 @@ public final class TextClassification {
            return this;
        }

        /**
         * Ensures that we have at we have storage for the default action.
         */
        private void ensureDefaultActionAvailable() {
            if (mIntents.isEmpty()) mIntents.add(null);
            if (mLabels.isEmpty()) mLabels.add(null);
            if (mIcons.isEmpty()) mIcons.add(null);
            if (mOnClickListeners.isEmpty()) mOnClickListeners.add(null);
        }

        /**
         * Builds and returns a {@link TextClassification} object.
         */
        public TextClassification build() {
            return new TextClassification(
                    mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence,
                    mText, mIcons, mLabels, mIntents, mOnClickListeners, mEntityConfidence,
                    mLogType, mVersionInfo);
        }
    }
+81 −37
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.net.Uri;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.provider.Browser;
import android.provider.ContactsContract;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.method.WordIterator;
@@ -356,7 +357,16 @@ final class TextClassifierImpl implements TextClassifier {
        final String type = getHighestScoringType(classifications);
        builder.setLogType(IntentFactory.getLogType(type));

        final Intent intent = IntentFactory.create(mContext, type, text.toString());
        final List<Intent> intents = IntentFactory.create(mContext, type, text.toString());
        for (Intent intent : intents) {
            extendClassificationWithIntent(intent, builder);
        }

        return builder.setVersionInfo(getVersionInfo()).build();
    }

    /** Extends the classification with the intent if it can be resolved. */
    private void extendClassificationWithIntent(Intent intent, TextClassification.Builder builder) {
        final PackageManager pm;
        final ResolveInfo resolveInfo;
        if (intent != null) {
@@ -367,30 +377,29 @@ final class TextClassifierImpl implements TextClassifier {
            resolveInfo = null;
        }
        if (resolveInfo != null && resolveInfo.activityInfo != null) {
            builder.setIntent(intent)
                    .setOnClickListener(TextClassification.createStartActivityOnClickListener(
                            mContext, intent));

            final String packageName = resolveInfo.activityInfo.packageName;
            CharSequence label;
            Drawable icon;
            if ("android".equals(packageName)) {
                // Requires the chooser to find an activity to handle the intent.
                builder.setLabel(IntentFactory.getLabel(mContext, type));
                label = IntentFactory.getLabel(mContext, intent);
                icon = null;
            } else {
                // A default activity will handle the intent.
                intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
                Drawable icon = resolveInfo.activityInfo.loadIcon(pm);
                icon = resolveInfo.activityInfo.loadIcon(pm);
                if (icon == null) {
                    icon = resolveInfo.loadIcon(pm);
                }
                builder.setIcon(icon);
                CharSequence label = resolveInfo.activityInfo.loadLabel(pm);
                label = resolveInfo.activityInfo.loadLabel(pm);
                if (label == null) {
                    label = resolveInfo.loadLabel(pm);
                }
                builder.setLabel(label != null ? label.toString() : null);
            }
            builder.addAction(
                    intent, label != null ? label.toString() : null, icon,
                    TextClassification.createStartActivityOnClickListener(mContext, intent));
        }
        return builder.setVersionInfo(getVersionInfo()).build();
    }

    private static int getHintFlags(CharSequence text, int start, int end) {
@@ -477,10 +486,11 @@ final class TextClassifierImpl implements TextClassifier {
                    if (results.length > 0) {
                        final String type = getHighestScoringType(results);
                        if (matches(type, linkMask)) {
                            final Intent intent = IntentFactory.create(
                            // For links without disambiguation, we simply use the default intent.
                            final List<Intent> intents = IntentFactory.create(
                                    context, type, text.substring(selectionStart, selectionEnd));
                            if (hasActivityHandler(context, intent)) {
                                final ClickableSpan span = createSpan(context, intent);
                            if (!intents.isEmpty() && hasActivityHandler(context, intents.get(0))) {
                                final ClickableSpan span = createSpan(context, intents.get(0));
                                spans.add(new SpanSpec(selectionStart, selectionEnd, span));
                            }
                        }
@@ -564,7 +574,7 @@ final class TextClassifierImpl implements TextClassifier {
            };
        }

        private static boolean hasActivityHandler(Context context, @Nullable Intent intent) {
        private static boolean hasActivityHandler(Context context, Intent intent) {
            if (intent == null) {
                return false;
            }
@@ -625,20 +635,32 @@ final class TextClassifierImpl implements TextClassifier {

        private IntentFactory() {}

        @Nullable
        public static Intent create(Context context, String type, String text) {
        @NonNull
        public static List<Intent> create(Context context, String type, String text) {
            final List<Intent> intents = new ArrayList<>();
            type = type.trim().toLowerCase(Locale.ENGLISH);
            text = text.trim();
            switch (type) {
                case TextClassifier.TYPE_EMAIL:
                    return new Intent(Intent.ACTION_SENDTO)
                            .setData(Uri.parse(String.format("mailto:%s", text)));
                    intents.add(new Intent(Intent.ACTION_SENDTO)
                            .setData(Uri.parse(String.format("mailto:%s", text))));
                    intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
                                    .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
                                    .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
                    break;
                case TextClassifier.TYPE_PHONE:
                    return new Intent(Intent.ACTION_DIAL)
                            .setData(Uri.parse(String.format("tel:%s", text)));
                    intents.add(new Intent(Intent.ACTION_DIAL)
                            .setData(Uri.parse(String.format("tel:%s", text))));
                    intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
                            .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
                            .putExtra(ContactsContract.Intents.Insert.PHONE, text));
                    intents.add(new Intent(Intent.ACTION_SENDTO)
                            .setData(Uri.parse(String.format("smsto:%s", text))));
                    break;
                case TextClassifier.TYPE_ADDRESS:
                    return new Intent(Intent.ACTION_VIEW)
                            .setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
                    intents.add(new Intent(Intent.ACTION_VIEW)
                            .setData(Uri.parse(String.format("geo:0,0?q=%s", text))));
                    break;
                case TextClassifier.TYPE_URL:
                    final String httpPrefix = "http://";
                    final String httpsPrefix = "https://";
@@ -649,28 +671,50 @@ final class TextClassifierImpl implements TextClassifier {
                    } else {
                        text = httpPrefix + text;
                    }
                    return new Intent(Intent.ACTION_VIEW, Uri.parse(text))
                            .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
                default:
                    return null;
                    intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text))
                            .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()));
                    break;
            }
            return intents;
        }

        @Nullable
        public static String getLabel(Context context, String type) {
            type = type.trim().toLowerCase(Locale.ENGLISH);
            switch (type) {
                case TextClassifier.TYPE_EMAIL:
                    return context.getString(com.android.internal.R.string.email);
                case TextClassifier.TYPE_PHONE:
        public static String getLabel(Context context, @Nullable Intent intent) {
            if (intent == null || intent.getAction() == null) {
                return null;
            }
            switch (intent.getAction()) {
                case Intent.ACTION_DIAL:
                    return context.getString(com.android.internal.R.string.dial);
                case TextClassifier.TYPE_ADDRESS:
                case Intent.ACTION_SENDTO:
                    switch (intent.getScheme()) {
                        case "mailto":
                            return context.getString(com.android.internal.R.string.email);
                        case "smsto":
                            return context.getString(com.android.internal.R.string.sms);
                        default:
                            return null;
                    }
                case Intent.ACTION_INSERT_OR_EDIT:
                    switch (intent.getDataString()) {
                        case ContactsContract.Contacts.CONTENT_ITEM_TYPE:
                            return context.getString(com.android.internal.R.string.add_contact);
                        default:
                            return null;
                    }
                case Intent.ACTION_VIEW:
                    switch (intent.getScheme()) {
                        case "geo":
                            return context.getString(com.android.internal.R.string.map);
                case TextClassifier.TYPE_URL:
                        case "http": // fall through
                        case "https":
                            return context.getString(com.android.internal.R.string.browse);
                        default:
                            return null;
                    }
                default:
                    return null;
            }
        }

        @Nullable
Loading