diff options
| -rw-r--r-- | api/current.txt | 7 | ||||
| -rw-r--r-- | api/system-current.txt | 7 | ||||
| -rw-r--r-- | api/test-current.txt | 7 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassification.java | 188 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifierImpl.java | 118 | ||||
| -rw-r--r-- | core/res/res/values/strings.xml | 6 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 2 |
7 files changed, 263 insertions, 72 deletions
diff --git a/api/current.txt b/api/current.txt index 2368231508b6..28f95c2c193c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); diff --git a/api/system-current.txt b/api/system-current.txt index cdd28b826179..0ec2bf7c0a97 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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); diff --git a/api/test-current.txt b/api/test-current.txt index 90ecb8c82ceb..baf117ec99fe 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -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); 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; } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 7416113b4e90..ea6d19c7d945 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2685,6 +2685,12 @@ <!-- Label for item in the text selection menu to trigger a Browser app [CHAR LIMIT=20] --> <string name="browse">Browser</string> + <!-- Label for item in the text selection menu to trigger an SMS app [CHAR LIMIT=20] --> + <string name="sms">SMS</string> + + <!-- Label for item in the text selection menu to trigger adding a contact [CHAR LIMIT=20] --> + <string name="add_contact">Contact</string> + <!-- If the device is getting low on internal storage, a notification is shown to the user. This is the title of that notification. --> <string name="low_internal_storage_view_title">Storage space running out</string> <!-- If the device is getting low on internal storage, a notification is shown to the user. This is the message of that notification. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4f03e7b06d97..77cb53abdd49 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -516,6 +516,8 @@ <java-symbol type="string" name="dial" /> <java-symbol type="string" name="map" /> <java-symbol type="string" name="browse" /> + <java-symbol type="string" name="sms" /> + <java-symbol type="string" name="add_contact" /> <java-symbol type="string" name="textSelectionCABTitle" /> <java-symbol type="string" name="BaMmi" /> <java-symbol type="string" name="CLIRDefaultOffNextCallOff" /> |