diff options
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassification.java | 188 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifierImpl.java | 118 |
2 files changed, 234 insertions, 72 deletions
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index 1849368f6ae9..8c3b8a2e6b20 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -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 an intent that may be fired to act on the classified text. + * 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 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 an OnClickListener that may be triggered to act on the classified text. + * 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 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; } @@ -266,11 +374,21 @@ public final class TextClassification { } /** + * 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); } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 7e93b78c4809..2aa81a2ce16c 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -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,25 +671,47 @@ 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: - return context.getString(com.android.internal.R.string.map); - case TextClassifier.TYPE_URL: - return context.getString(com.android.internal.R.string.browse); + 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 "http": // fall through + case "https": + return context.getString(com.android.internal.R.string.browse); + default: + return null; + } default: return null; } |
