Merge "Change TextClassification to use RemoteActions" into pi-dev
diff --git a/api/current.txt b/api/current.txt
index dda00ba..9e18b73 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5877,6 +5877,8 @@
method public java.lang.CharSequence getTitle();
method public boolean isEnabled();
method public void setEnabled(boolean);
+ method public void setShouldShowIcon(boolean);
+ method public boolean shouldShowIcon();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.RemoteAction> CREATOR;
}
@@ -50354,17 +50356,14 @@
public final class TextClassification implements android.os.Parcelable {
method public int describeContents();
+ method public java.util.List<android.app.RemoteAction> getActions();
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();
- method public android.content.Intent getIntent();
- method public java.lang.CharSequence getLabel();
- method public android.view.View.OnClickListener getOnClickListener();
- method public int getSecondaryActionsCount();
- method public android.graphics.drawable.Drawable getSecondaryIcon(int);
- method public android.content.Intent getSecondaryIntent(int);
- method public java.lang.CharSequence getSecondaryLabel(int);
+ method public deprecated android.graphics.drawable.Drawable getIcon();
+ method public deprecated android.content.Intent getIntent();
+ method public deprecated java.lang.CharSequence getLabel();
+ method public deprecated android.view.View.OnClickListener getOnClickListener();
method public java.lang.String getSignature();
method public java.lang.String getText();
method public void writeToParcel(android.os.Parcel, int);
@@ -50373,15 +50372,13 @@
public static final class TextClassification.Builder {
ctor public TextClassification.Builder();
- method public android.view.textclassifier.TextClassification.Builder addSecondaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable);
+ method public android.view.textclassifier.TextClassification.Builder addAction(android.app.RemoteAction);
method public android.view.textclassifier.TextClassification build();
- method public android.view.textclassifier.TextClassification.Builder clearSecondaryActions();
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);
- method public android.view.textclassifier.TextClassification.Builder setLabel(java.lang.String);
- method public android.view.textclassifier.TextClassification.Builder setOnClickListener(android.view.View.OnClickListener);
- method public android.view.textclassifier.TextClassification.Builder setPrimaryAction(android.content.Intent, java.lang.String, android.graphics.drawable.Drawable);
+ method public deprecated android.view.textclassifier.TextClassification.Builder setIcon(android.graphics.drawable.Drawable);
+ method public deprecated android.view.textclassifier.TextClassification.Builder setIntent(android.content.Intent);
+ method public deprecated android.view.textclassifier.TextClassification.Builder setLabel(java.lang.String);
+ method public deprecated android.view.textclassifier.TextClassification.Builder setOnClickListener(android.view.View.OnClickListener);
method public android.view.textclassifier.TextClassification.Builder setSignature(java.lang.String);
method public android.view.textclassifier.TextClassification.Builder setText(java.lang.String);
}
diff --git a/core/java/android/app/RemoteAction.java b/core/java/android/app/RemoteAction.java
index e7fe407..47741c0 100644
--- a/core/java/android/app/RemoteAction.java
+++ b/core/java/android/app/RemoteAction.java
@@ -18,14 +18,9 @@
import android.annotation.NonNull;
import android.graphics.drawable.Icon;
-import android.os.Handler;
-import android.os.Message;
-import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.RemoteException;
import android.text.TextUtils;
-import android.util.Log;
import java.io.PrintWriter;
@@ -42,6 +37,7 @@
private final CharSequence mContentDescription;
private final PendingIntent mActionIntent;
private boolean mEnabled;
+ private boolean mShouldShowIcon;
RemoteAction(Parcel in) {
mIcon = Icon.CREATOR.createFromParcel(in);
@@ -49,6 +45,7 @@
mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mActionIntent = PendingIntent.CREATOR.createFromParcel(in);
mEnabled = in.readBoolean();
+ mShouldShowIcon = in.readBoolean();
}
public RemoteAction(@NonNull Icon icon, @NonNull CharSequence title,
@@ -62,6 +59,7 @@
mContentDescription = contentDescription;
mActionIntent = intent;
mEnabled = true;
+ mShouldShowIcon = true;
}
/**
@@ -79,6 +77,20 @@
}
/**
+ * Sets whether the icon should be shown.
+ */
+ public void setShouldShowIcon(boolean shouldShowIcon) {
+ mShouldShowIcon = shouldShowIcon;
+ }
+
+ /**
+ * Return whether the icon should be shown.
+ */
+ public boolean shouldShowIcon() {
+ return mShouldShowIcon;
+ }
+
+ /**
* Return an icon representing the action.
*/
public @NonNull Icon getIcon() {
@@ -125,6 +137,7 @@
TextUtils.writeToParcel(mContentDescription, out, flags);
mActionIntent.writeToParcel(out, flags);
out.writeBoolean(mEnabled);
+ out.writeBoolean(mShouldShowIcon);
}
public void dump(String prefix, PrintWriter pw) {
@@ -134,6 +147,7 @@
pw.print(" contentDescription=" + mContentDescription);
pw.print(" icon=" + mIcon);
pw.print(" action=" + mActionIntent.getIntent());
+ pw.print(" shouldShowIcon=" + mShouldShowIcon);
pw.println();
}
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java
index b5c9de9..630007b 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -21,6 +21,8 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -43,6 +45,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -77,25 +80,16 @@
* view.startActionMode(new ActionMode.Callback() {
*
* public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- * // Add the "primary" action.
- * if (thisAppHasPermissionToInvokeIntent(classification.getIntent())) {
- * menu.add(Menu.NONE, 0, 20, classification.getLabel())
- * .setIcon(classification.getIcon())
- * .setIntent(classification.getIntent());
- * }
- * // Add the "secondary" actions.
- * for (int i = 0; i < classification.getSecondaryActionsCount(); i++) {
- * if (thisAppHasPermissionToInvokeIntent(classification.getSecondaryIntent(i))) {
- * menu.add(Menu.NONE, i + 1, 20, classification.getSecondaryLabel(i))
- * .setIcon(classification.getSecondaryIcon(i))
- * .setIntent(classification.getSecondaryIntent(i));
- * }
+ * for (int i = 0; i < classification.getActions().size(); ++i) {
+ * RemoteAction action = classification.getActions().get(i);
+ * menu.add(Menu.NONE, i, 20, action.getTitle())
+ * .setIcon(action.getIcon());
* }
* return true;
* }
*
* public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- * context.startActivity(item.getIntent());
+ * classification.getActions().get(item.getItemId()).getActionIntent().send();
* return true;
* }
*
@@ -110,9 +104,9 @@
*/
static final TextClassification EMPTY = new TextClassification.Builder().build();
+ private static final String LOG_TAG = "TextClassification";
// TODO(toki): investigate a way to derive this based on device properties.
- private static final int MAX_PRIMARY_ICON_SIZE = 192;
- private static final int MAX_SECONDARY_ICON_SIZE = 144;
+ private static final int MAX_LEGACY_ICON_SIZE = 192;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {IntentType.UNSUPPORTED, IntentType.ACTIVITY, IntentType.SERVICE})
@@ -123,37 +117,29 @@
}
@NonNull private final String mText;
- @Nullable private final Drawable mPrimaryIcon;
- @Nullable private final String mPrimaryLabel;
- @Nullable private final Intent mPrimaryIntent;
- @Nullable private final OnClickListener mPrimaryOnClickListener;
- @NonNull private final List<Drawable> mSecondaryIcons;
- @NonNull private final List<String> mSecondaryLabels;
- @NonNull private final List<Intent> mSecondaryIntents;
+ @Nullable private final Drawable mLegacyIcon;
+ @Nullable private final String mLegacyLabel;
+ @Nullable private final Intent mLegacyIntent;
+ @Nullable private final OnClickListener mLegacyOnClickListener;
+ @NonNull private final List<RemoteAction> mActions;
@NonNull private final EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextClassification(
@Nullable String text,
- @Nullable Drawable primaryIcon,
- @Nullable String primaryLabel,
- @Nullable Intent primaryIntent,
- @Nullable OnClickListener primaryOnClickListener,
- @NonNull List<Drawable> secondaryIcons,
- @NonNull List<String> secondaryLabels,
- @NonNull List<Intent> secondaryIntents,
+ @Nullable Drawable legacyIcon,
+ @Nullable String legacyLabel,
+ @Nullable Intent legacyIntent,
+ @Nullable OnClickListener legacyOnClickListener,
+ @NonNull List<RemoteAction> actions,
@NonNull Map<String, Float> entityConfidence,
@NonNull String signature) {
- Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size());
- Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size());
mText = text;
- mPrimaryIcon = primaryIcon;
- mPrimaryLabel = primaryLabel;
- mPrimaryIntent = primaryIntent;
- mPrimaryOnClickListener = primaryOnClickListener;
- mSecondaryIcons = secondaryIcons;
- mSecondaryLabels = secondaryLabels;
- mSecondaryIntents = secondaryIntents;
+ mLegacyIcon = legacyIcon;
+ mLegacyLabel = legacyLabel;
+ mLegacyIntent = legacyIntent;
+ mLegacyOnClickListener = legacyOnClickListener;
+ mActions = Collections.unmodifiableList(actions);
mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
@@ -197,108 +183,57 @@
}
/**
- * Returns the number of <i>secondary</i> actions that are available to act on the classified
- * text.
- *
- * <p><strong>Note: </strong> that there may or may not be a <i>primary</i> action.
- *
- * @see #getSecondaryIntent(int)
- * @see #getSecondaryLabel(int)
- * @see #getSecondaryIcon(int)
+ * Returns a list of actions that may be performed on the text. The list is ordered based on
+ * the likelihood that a user will use the action, with the most likely action appearing first.
*/
- @IntRange(from = 0)
- public int getSecondaryActionsCount() {
- return mSecondaryIntents.size();
+ public List<RemoteAction> getActions() {
+ return mActions;
}
/**
- * Returns one of the <i>secondary</i> icons that maybe rendered on a widget used to act on the
- * classified text.
+ * Returns an icon that may be 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 #getSecondaryActionsCount() for the number of actions available.
- * @see #getSecondaryIntent(int)
- * @see #getSecondaryLabel(int)
- * @see #getIcon()
+ * @deprecated Use {@link #getActions()} instead.
*/
- @Nullable
- public Drawable getSecondaryIcon(int index) {
- return mSecondaryIcons.get(index);
- }
-
- /**
- * Returns an icon for the <i>primary</i> intent that may be rendered on a widget used to act
- * on the classified text.
- *
- * @see #getSecondaryIcon(int)
- */
+ @Deprecated
@Nullable
public Drawable getIcon() {
- return mPrimaryIcon;
+ return mLegacyIcon;
}
/**
- * Returns one of the <i>secondary</i> labels that may be rendered on a widget used to act on
- * the classified text.
+ * Returns a label 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 #getSecondaryActionsCount()
- * @see #getSecondaryIntent(int)
- * @see #getSecondaryIcon(int)
- * @see #getLabel()
+ * @deprecated Use {@link #getActions()} instead.
*/
- @Nullable
- public CharSequence getSecondaryLabel(int index) {
- return mSecondaryLabels.get(index);
- }
-
- /**
- * Returns a label for the <i>primary</i> intent that may be rendered on a widget used to act
- * on the classified text.
- *
- * @see #getSecondaryLabel(int)
- */
+ @Deprecated
@Nullable
public CharSequence getLabel() {
- return mPrimaryLabel;
+ return mLegacyLabel;
}
/**
- * Returns one of the <i>secondary</i> intents that may be fired to act on the classified text.
+ * Returns an intent 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 #getSecondaryActionsCount()
- * @see #getSecondaryLabel(int)
- * @see #getSecondaryIcon(int)
- * @see #getIntent()
+ * @deprecated Use {@link #getActions()} instead.
*/
- @Nullable
- public Intent getSecondaryIntent(int index) {
- return mSecondaryIntents.get(index);
- }
-
- /**
- * Returns the <i>primary</i> intent that may be fired to act on the classified text.
- *
- * @see #getSecondaryIntent(int)
- */
+ @Deprecated
@Nullable
public Intent getIntent() {
- return mPrimaryIntent;
+ return mLegacyIntent;
}
/**
- * Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified
- * text. This field is not parcelable and will be null for all objects read from a parcel.
- * Instead, call Context#startActivity(Intent) with the result of #getSecondaryIntent(int).
- * Note that this may fail if the activity doesn't have permission to send the intent.
+ * Returns the OnClickListener that may be triggered to act on the classified text. This field
+ * is not parcelable and will be null for all objects read from a parcel. Instead, call
+ * Context#startActivity(Intent) with the result of #getSecondaryIntent(int). Note that this may
+ * fail if the activity doesn't have permission to send the intent.
+ *
+ * @deprecated Use {@link #getActions()} instead.
*/
@Nullable
public OnClickListener getOnClickListener() {
- return mPrimaryOnClickListener;
+ return mLegacyOnClickListener;
}
/**
@@ -313,32 +248,42 @@
@Override
public String toString() {
- return String.format(Locale.US, "TextClassification {"
- + "text=%s, entities=%s, "
- + "primaryLabel=%s, secondaryLabels=%s, "
- + "primaryIntent=%s, secondaryIntents=%s, "
- + "signature=%s}",
- mText, mEntityConfidence,
- mPrimaryLabel, mSecondaryLabels,
- mPrimaryIntent, mSecondaryIntents,
- mSignature);
+ return String.format(Locale.US,
+ "TextClassification {text=%s, entities=%s, actions=%s, signature=%s}",
+ mText, mEntityConfidence, mActions, mSignature);
}
/**
- * Creates an OnClickListener that triggers the specified intent.
+ * Creates an OnClickListener that triggers the specified PendingIntent.
+ *
+ * @hide
+ */
+ public static OnClickListener createIntentOnClickListener(@NonNull final PendingIntent intent) {
+ Preconditions.checkNotNull(intent);
+ return v -> {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(LOG_TAG, "Error creating OnClickListener from PendingIntent", e);
+ }
+ };
+ }
+
+ /**
+ * Creates a PendingIntent for the specified intent.
* Returns null if the intent is not supported for the specified context.
*
* @throws IllegalArgumentException if context or intent is null
* @hide
*/
@Nullable
- public static OnClickListener createIntentOnClickListener(
+ public static PendingIntent createPendingIntent(
@NonNull final Context context, @NonNull final Intent intent) {
switch (getIntentType(intent, context)) {
case IntentType.ACTIVITY:
- return v -> context.startActivity(intent);
+ return PendingIntent.getActivity(context, 0, intent, 0);
case IntentType.SERVICE:
- return v -> context.startService(intent);
+ return PendingIntent.getService(context, 0, intent, 0);
default:
return null;
}
@@ -434,33 +379,6 @@
}
/**
- * Returns a list of drawables converted to Bitmaps
- *
- * @param drawables The drawables to convert.
- * @param maxDims The maximum edge length of the resulting bitmaps (in pixels).
- */
- private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) {
- final List<Bitmap> bitmaps = new ArrayList<>(drawables.size());
- for (Drawable drawable : drawables) {
- bitmaps.add(drawableToBitmap(drawable, maxDims));
- }
- return bitmaps;
- }
-
- /** Returns a list of drawable wrappers for a list of bitmaps. */
- private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) {
- final List<Drawable> drawables = new ArrayList<>(bitmaps.size());
- for (Bitmap bitmap : bitmaps) {
- if (bitmap != null) {
- drawables.add(new BitmapDrawable(Resources.getSystem(), bitmap));
- } else {
- drawables.add(null);
- }
- }
- return drawables;
- }
-
- /**
* Builder for building {@link TextClassification} objects.
*
* <p>e.g.
@@ -470,23 +388,20 @@
* .setText(classifiedText)
* .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
* .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
- * .setPrimaryAction(intent, label, icon)
- * .addSecondaryAction(intent1, label1, icon1)
- * .addSecondaryAction(intent2, label2, icon2)
+ * .addAction(remoteAction1)
+ * .addAction(remoteAction2)
* .build();
* }</pre>
*/
public static final class Builder {
@NonNull private String mText;
- @NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>();
- @NonNull private final List<String> mSecondaryLabels = new ArrayList<>();
- @NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>();
+ @NonNull private List<RemoteAction> mActions = new ArrayList<>();
@NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
- @Nullable Drawable mPrimaryIcon;
- @Nullable String mPrimaryLabel;
- @Nullable Intent mPrimaryIntent;
- @Nullable OnClickListener mPrimaryOnClickListener;
+ @Nullable Drawable mLegacyIcon;
+ @Nullable String mLegacyLabel;
+ @Nullable Intent mLegacyIntent;
+ @Nullable OnClickListener mLegacyOnClickListener;
@NonNull private String mSignature = "";
/**
@@ -514,60 +429,25 @@
}
/**
- * Adds an <i>secondary</i> action that may be performed on the classified text.
- * Secondary actions are in addition to the <i>primary</i> action which may or may not
- * exist.
- *
- * <p>The label and icon are used for rendering of widgets that offer the intent.
- * Actions should be added in order of priority.
- *
- * <p><stong>Note: </stong> If all input parameters are set to null, this method will be a
- * no-op.
- *
- * @see #setPrimaryAction(Intent, String, Drawable)
+ * Adds an action that may be performed on the classified text. Actions should be added in
+ * order of likelihood that the user will use them, with the most likely action being added
+ * first.
*/
- public Builder addSecondaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
- if (intent != null || label != null || icon != null) {
- mSecondaryIntents.add(intent);
- mSecondaryLabels.add(label);
- mSecondaryIcons.add(icon);
- }
+ public Builder addAction(@NonNull RemoteAction action) {
+ Preconditions.checkArgument(action != null);
+ mActions.add(action);
return this;
}
/**
- * Removes all the <i>secondary</i> actions.
- */
- public Builder clearSecondaryActions() {
- mSecondaryIntents.clear();
- mSecondaryLabels.clear();
- mSecondaryIcons.clear();
- return this;
- }
-
- /**
- * Sets the <i>primary</i> action that may be performed on the classified text. This is
- * equivalent to calling {@code setIntent(intent).setLabel(label).setIcon(icon)}.
- *
- * <p><strong>Note: </strong>If all input parameters are null, there will be no
- * <i>primary</i> action but there may still be <i>secondary</i> actions.
- *
- * @see #addSecondaryAction(Intent, String, Drawable)
- */
- public Builder setPrimaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
- return setIntent(intent).setLabel(label).setIcon(icon);
- }
-
- /**
* Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
* on the classified text.
*
- * @see #setPrimaryAction(Intent, String, Drawable)
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
*/
+ @Deprecated
public Builder setIcon(@Nullable Drawable icon) {
- mPrimaryIcon = icon;
+ mLegacyIcon = icon;
return this;
}
@@ -575,10 +455,11 @@
* Sets the label for the <i>primary</i> action that may be rendered on a widget used to
* act on the classified text.
*
- * @see #setPrimaryAction(Intent, String, Drawable)
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
*/
+ @Deprecated
public Builder setLabel(@Nullable String label) {
- mPrimaryLabel = label;
+ mLegacyLabel = label;
return this;
}
@@ -586,10 +467,11 @@
* Sets the intent for the <i>primary</i> action that may be fired to act on the classified
* text.
*
- * @see #setPrimaryAction(Intent, String, Drawable)
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
*/
+ @Deprecated
public Builder setIntent(@Nullable Intent intent) {
- mPrimaryIntent = intent;
+ mLegacyIntent = intent;
return this;
}
@@ -597,9 +479,11 @@
* Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
* the classified text. This field is not parcelable and will always be null when the
* object is read from a parcel.
+ *
+ * @deprecated Use {@link #addAction(RemoteAction)} instead.
*/
public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
- mPrimaryOnClickListener = onClickListener;
+ mLegacyOnClickListener = onClickListener;
return this;
}
@@ -617,11 +501,8 @@
* Builds and returns a {@link TextClassification} object.
*/
public TextClassification build() {
- return new TextClassification(
- mText,
- mPrimaryIcon, mPrimaryLabel, mPrimaryIntent, mPrimaryOnClickListener,
- mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
- mEntityConfidence, mSignature);
+ return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent,
+ mLegacyOnClickListener, mActions, mEntityConfidence, mSignature);
}
}
@@ -721,20 +602,18 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mText);
- final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
- dest.writeInt(primaryIconBitmap != null ? 1 : 0);
- if (primaryIconBitmap != null) {
- primaryIconBitmap.writeToParcel(dest, flags);
+ final Bitmap legacyIconBitmap = drawableToBitmap(mLegacyIcon, MAX_LEGACY_ICON_SIZE);
+ dest.writeInt(legacyIconBitmap != null ? 1 : 0);
+ if (legacyIconBitmap != null) {
+ legacyIconBitmap.writeToParcel(dest, flags);
}
- dest.writeString(mPrimaryLabel);
- dest.writeInt(mPrimaryIntent != null ? 1 : 0);
- if (mPrimaryIntent != null) {
- mPrimaryIntent.writeToParcel(dest, flags);
+ dest.writeString(mLegacyLabel);
+ dest.writeInt(mLegacyIntent != null ? 1 : 0);
+ if (mLegacyIntent != null) {
+ mLegacyIntent.writeToParcel(dest, flags);
}
- // mPrimaryOnClickListener is not parcelable.
- dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
- dest.writeStringList(mSecondaryLabels);
- dest.writeTypedList(mSecondaryIntents);
+ // mOnClickListener is not parcelable.
+ dest.writeTypedList(mActions);
mEntityConfidence.writeToParcel(dest, flags);
dest.writeString(mSignature);
}
@@ -754,15 +633,19 @@
private TextClassification(Parcel in) {
mText = in.readString();
- mPrimaryIcon = in.readInt() == 0
+ mLegacyIcon = in.readInt() == 0
? null
: new BitmapDrawable(Resources.getSystem(), Bitmap.CREATOR.createFromParcel(in));
- mPrimaryLabel = in.readString();
- mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
- mPrimaryOnClickListener = null; // not parcelable
- mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
- mSecondaryLabels = in.createStringArrayList();
- mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mLegacyLabel = in.readString();
+ if (in.readInt() == 0) {
+ mLegacyIntent = null;
+ } else {
+ mLegacyIntent = Intent.CREATOR.createFromParcel(in);
+ mLegacyIntent.removeFlags(
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+ mLegacyOnClickListener = null; // not parcelable
+ mActions = in.createTypedArrayList(RemoteAction.CREATOR);
mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
mSignature = in.readString();
}
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index c2fb032..a0f4d5c 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
+import android.app.RemoteAction;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.ContentUris;
@@ -26,7 +27,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
@@ -418,50 +419,27 @@
}
}
- addActions(builder, IntentFactory.create(
- mContext, referenceTime, highestScoringResult, classifiedText));
+ boolean isPrimaryAction = true;
+ for (LabeledIntent labeledIntent : IntentFactory.create(
+ mContext, referenceTime, highestScoringResult, classifiedText)) {
+ RemoteAction action = labeledIntent.asRemoteAction(mContext);
+ if (isPrimaryAction) {
+ // For O backwards compatibility, the first RemoteAction is also written to the
+ // legacy API fields.
+ builder.setIcon(action.getIcon().loadDrawable(mContext));
+ builder.setLabel(action.getTitle().toString());
+ builder.setIntent(labeledIntent.getIntent());
+ builder.setOnClickListener(TextClassification.createIntentOnClickListener(
+ TextClassification.createPendingIntent(mContext,
+ labeledIntent.getIntent())));
+ isPrimaryAction = false;
+ }
+ builder.addAction(action);
+ }
return builder.setSignature(getSignature(text, start, end)).build();
}
- /** Extends the classification with the intents that can be resolved. */
- private void addActions(
- TextClassification.Builder builder, List<Intent> intents) {
- final PackageManager pm = mContext.getPackageManager();
- final int size = intents.size();
- for (int i = 0; i < size; i++) {
- final Intent intent = intents.get(i);
- final ResolveInfo resolveInfo;
- if (intent != null) {
- resolveInfo = pm.resolveActivity(intent, 0);
- } else {
- resolveInfo = null;
- }
- if (resolveInfo != null && resolveInfo.activityInfo != null) {
- final String packageName = resolveInfo.activityInfo.packageName;
- final String label = IntentFactory.getLabel(mContext, intent);
- Drawable icon;
- if ("android".equals(packageName)) {
- // Requires the chooser to find an activity to handle the intent.
- icon = null;
- } else {
- // A default activity will handle the intent.
- intent.setComponent(
- new ComponentName(packageName, resolveInfo.activityInfo.name));
- icon = resolveInfo.activityInfo.loadIcon(pm);
- if (icon == null) {
- icon = resolveInfo.loadIcon(pm);
- }
- }
- if (i == 0) {
- builder.setPrimaryAction(intent, label, icon);
- } else {
- builder.addSecondaryAction(intent, label, icon);
- }
- }
- }
- }
-
/**
* Closes the ParcelFileDescriptor and logs any errors that occur.
*/
@@ -588,6 +566,60 @@
}
/**
+ * Helper class to store the information from which RemoteActions are built.
+ */
+ private static final class LabeledIntent {
+ private String mTitle;
+ private String mDescription;
+ private Intent mIntent;
+
+ LabeledIntent(String title, String description, Intent intent) {
+ mTitle = title;
+ mDescription = description;
+ mIntent = intent;
+ }
+
+ String getTitle() {
+ return mTitle;
+ }
+
+ String getDescription() {
+ return mDescription;
+ }
+
+ Intent getIntent() {
+ return mIntent;
+ }
+
+ RemoteAction asRemoteAction(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ final ResolveInfo resolveInfo = pm.resolveActivity(mIntent, 0);
+ final String packageName = resolveInfo != null && resolveInfo.activityInfo != null
+ ? resolveInfo.activityInfo.packageName : null;
+ Icon icon = null;
+ boolean shouldShowIcon = false;
+ if (packageName != null && !"android".equals(packageName)) {
+ // There is a default activity handling the intent.
+ mIntent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
+ if (resolveInfo.activityInfo.getIconResource() != 0) {
+ icon = Icon.createWithResource(
+ packageName, resolveInfo.activityInfo.getIconResource());
+ shouldShowIcon = true;
+ }
+ }
+ if (icon == null) {
+ // RemoteAction requires that there be an icon.
+ icon = Icon.createWithResource("android",
+ com.android.internal.R.drawable.ic_more_items);
+ }
+ RemoteAction action = new RemoteAction(icon, mTitle, mDescription,
+ TextClassification.createPendingIntent(context, mIntent));
+ action.setShouldShowIcon(shouldShowIcon);
+ return action;
+ }
+ }
+
+ /**
* Creates intents based on the classification type.
*/
static final class IntentFactory {
@@ -598,7 +630,7 @@
private IntentFactory() {}
@NonNull
- public static List<Intent> create(
+ public static List<LabeledIntent> create(
Context context,
@Nullable Calendar referenceTime,
TextClassifierImplNative.ClassificationResult classification,
@@ -607,11 +639,11 @@
text = text.trim();
switch (type) {
case TextClassifier.TYPE_EMAIL:
- return createForEmail(text);
+ return createForEmail(context, text);
case TextClassifier.TYPE_PHONE:
return createForPhone(context, text);
case TextClassifier.TYPE_ADDRESS:
- return createForAddress(text);
+ return createForAddress(context, text);
case TextClassifier.TYPE_URL:
return createForUrl(context, text);
case TextClassifier.TYPE_DATE:
@@ -620,62 +652,80 @@
Calendar eventTime = Calendar.getInstance();
eventTime.setTimeInMillis(
classification.getDatetimeResult().getTimeMsUtc());
- return createForDatetime(type, referenceTime, eventTime);
+ return createForDatetime(context, type, referenceTime, eventTime);
} else {
return new ArrayList<>();
}
case TextClassifier.TYPE_FLIGHT_NUMBER:
- return createForFlight(text);
+ return createForFlight(context, text);
default:
return new ArrayList<>();
}
}
@NonNull
- private static List<Intent> createForEmail(String text) {
+ private static List<LabeledIntent> createForEmail(Context context, String text) {
return Arrays.asList(
- new Intent(Intent.ACTION_SENDTO)
- .setData(Uri.parse(String.format("mailto:%s", text))),
- new Intent(Intent.ACTION_INSERT_OR_EDIT)
- .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
- .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
+ new LabeledIntent(
+ context.getString(com.android.internal.R.string.email),
+ context.getString(com.android.internal.R.string.email_desc),
+ new Intent(Intent.ACTION_SENDTO)
+ .setData(Uri.parse(String.format("mailto:%s", text)))),
+ new LabeledIntent(
+ context.getString(com.android.internal.R.string.add_contact),
+ context.getString(com.android.internal.R.string.add_contact_desc),
+ new Intent(Intent.ACTION_INSERT_OR_EDIT)
+ .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
+ .putExtra(ContactsContract.Intents.Insert.EMAIL, text)));
}
@NonNull
- private static List<Intent> createForPhone(Context context, String text) {
- final List<Intent> intents = new ArrayList<>();
+ private static List<LabeledIntent> createForPhone(Context context, String text) {
+ final List<LabeledIntent> actions = new ArrayList<>();
final UserManager userManager = context.getSystemService(UserManager.class);
final Bundle userRestrictions = userManager != null
? userManager.getUserRestrictions() : new Bundle();
if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
- intents.add(new Intent(Intent.ACTION_DIAL)
- .setData(Uri.parse(String.format("tel:%s", text))));
+ actions.add(new LabeledIntent(
+ context.getString(com.android.internal.R.string.dial),
+ context.getString(com.android.internal.R.string.dial_desc),
+ 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));
+ actions.add(new LabeledIntent(
+ context.getString(com.android.internal.R.string.add_contact),
+ context.getString(com.android.internal.R.string.add_contact_desc),
+ new Intent(Intent.ACTION_INSERT_OR_EDIT)
+ .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
+ .putExtra(ContactsContract.Intents.Insert.PHONE, text)));
if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
- intents.add(new Intent(Intent.ACTION_SENDTO)
- .setData(Uri.parse(String.format("smsto:%s", text))));
+ actions.add(new LabeledIntent(
+ context.getString(com.android.internal.R.string.sms),
+ context.getString(com.android.internal.R.string.sms_desc),
+ new Intent(Intent.ACTION_SENDTO)
+ .setData(Uri.parse(String.format("smsto:%s", text)))));
}
- return intents;
+ return actions;
}
@NonNull
- private static List<Intent> createForAddress(String text) {
- final List<Intent> intents = new ArrayList<>();
+ private static List<LabeledIntent> createForAddress(Context context, String text) {
+ final List<LabeledIntent> actions = new ArrayList<>();
try {
final String encText = URLEncoder.encode(text, "UTF-8");
- intents.add(new Intent(Intent.ACTION_VIEW)
- .setData(Uri.parse(String.format("geo:0,0?q=%s", encText))));
+ actions.add(new LabeledIntent(
+ context.getString(com.android.internal.R.string.map),
+ context.getString(com.android.internal.R.string.map_desc),
+ new Intent(Intent.ACTION_VIEW)
+ .setData(Uri.parse(String.format("geo:0,0?q=%s", encText)))));
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Could not encode address", e);
}
- return intents;
+ return actions;
}
@NonNull
- private static List<Intent> createForUrl(Context context, String text) {
+ private static List<LabeledIntent> createForUrl(Context context, String text) {
final String httpPrefix = "http://";
final String httpsPrefix = "https://";
if (text.toLowerCase().startsWith(httpPrefix)) {
@@ -685,99 +735,65 @@
} else {
text = httpPrefix + text;
}
- return Arrays.asList(new Intent(Intent.ACTION_VIEW, Uri.parse(text))
- .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()));
+ return Arrays.asList(new LabeledIntent(
+ context.getString(com.android.internal.R.string.browse),
+ context.getString(com.android.internal.R.string.browse_desc),
+ new Intent(Intent.ACTION_VIEW, Uri.parse(text))
+ .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())));
}
@NonNull
- private static List<Intent> createForDatetime(
- String type, @Nullable Calendar referenceTime, Calendar eventTime) {
+ private static List<LabeledIntent> createForDatetime(
+ Context context, String type, @Nullable Calendar referenceTime,
+ Calendar eventTime) {
if (referenceTime == null) {
// If no reference time was given, use now.
referenceTime = Calendar.getInstance();
}
- List<Intent> intents = new ArrayList<>();
- intents.add(createCalendarViewIntent(eventTime));
+ List<LabeledIntent> actions = new ArrayList<>();
+ actions.add(createCalendarViewIntent(context, eventTime));
final long millisSinceReference =
eventTime.getTimeInMillis() - referenceTime.getTimeInMillis();
if (millisSinceReference > MIN_EVENT_FUTURE_MILLIS) {
- intents.add(createCalendarCreateEventIntent(eventTime, type));
+ actions.add(createCalendarCreateEventIntent(context, eventTime, type));
}
- return intents;
+ return actions;
}
@NonNull
- private static List<Intent> createForFlight(String text) {
- return Arrays.asList(new Intent(Intent.ACTION_WEB_SEARCH)
- .putExtra(SearchManager.QUERY, text));
+ private static List<LabeledIntent> createForFlight(Context context, String text) {
+ return Arrays.asList(new LabeledIntent(
+ context.getString(com.android.internal.R.string.view_flight),
+ context.getString(com.android.internal.R.string.view_flight_desc),
+ new Intent(Intent.ACTION_WEB_SEARCH)
+ .putExtra(SearchManager.QUERY, text)));
}
@NonNull
- private static Intent createCalendarViewIntent(Calendar eventTime) {
+ private static LabeledIntent createCalendarViewIntent(Context context, Calendar eventTime) {
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, eventTime.getTimeInMillis());
- return new Intent(Intent.ACTION_VIEW).setData(builder.build());
+ return new LabeledIntent(
+ context.getString(com.android.internal.R.string.view_calendar),
+ context.getString(com.android.internal.R.string.view_calendar_desc),
+ new Intent(Intent.ACTION_VIEW).setData(builder.build()));
}
@NonNull
- private static Intent createCalendarCreateEventIntent(
- Calendar eventTime, @EntityType String type) {
+ private static LabeledIntent createCalendarCreateEventIntent(
+ Context context, Calendar eventTime, @EntityType String type) {
final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
- return new Intent(Intent.ACTION_INSERT)
- .setData(CalendarContract.Events.CONTENT_URI)
- .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
- .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, eventTime.getTimeInMillis())
- .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
- eventTime.getTimeInMillis() + DEFAULT_EVENT_DURATION);
- }
-
- @Nullable
- public static String getLabel(Context context, @Nullable Intent intent) {
- if (intent == null || intent.getAction() == null) {
- return null;
- }
- final String authority =
- intent.getData() == null ? null : intent.getData().getAuthority();
- switch (intent.getAction()) {
- case Intent.ACTION_DIAL:
- return context.getString(com.android.internal.R.string.dial);
- case Intent.ACTION_SENDTO:
- if ("mailto".equals(intent.getScheme())) {
- return context.getString(com.android.internal.R.string.email);
- } else if ("smsto".equals(intent.getScheme())) {
- return context.getString(com.android.internal.R.string.sms);
- } else {
- return null;
- }
- case Intent.ACTION_INSERT:
- if (CalendarContract.AUTHORITY.equals(authority)) {
- return context.getString(com.android.internal.R.string.add_calendar_event);
- }
- return null;
- case Intent.ACTION_INSERT_OR_EDIT:
- if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(
- intent.getType())) {
- return context.getString(com.android.internal.R.string.add_contact);
- } else {
- return null;
- }
- case Intent.ACTION_VIEW:
- if (CalendarContract.AUTHORITY.equals(authority)) {
- return context.getString(com.android.internal.R.string.view_calendar);
- } else if ("geo".equals(intent.getScheme())) {
- return context.getString(com.android.internal.R.string.map);
- } else if ("http".equals(intent.getScheme())
- || "https".equals(intent.getScheme())) {
- return context.getString(com.android.internal.R.string.browse);
- } else {
- return null;
- }
- case Intent.ACTION_WEB_SEARCH:
- return context.getString(com.android.internal.R.string.view_flight);
- default:
- return null;
- }
+ return new LabeledIntent(
+ context.getString(com.android.internal.R.string.add_calendar_event),
+ context.getString(com.android.internal.R.string.add_calendar_event_desc),
+ new Intent(Intent.ACTION_INSERT)
+ .setData(CalendarContract.Events.CONTENT_URI)
+ .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
+ .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
+ eventTime.getTimeInMillis())
+ .putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
+ eventTime.getTimeInMillis() + DEFAULT_EVENT_DURATION));
}
}
}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 57d64b9..92f496a8 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
+import android.app.RemoteAction;
import android.content.ClipData;
import android.content.ClipData.Item;
import android.content.Context;
@@ -4045,43 +4046,46 @@
if (textClassification == null) {
return;
}
- final OnClickListener onClick = getSupportedOnClickListener(
- textClassification.getIcon(),
- textClassification.getLabel(),
- textClassification.getIntent());
- if (onClick != null) {
+ if (!textClassification.getActions().isEmpty()) {
+ // Primary assist action (Always shown).
+ final MenuItem item = addAssistMenuItem(menu,
+ textClassification.getActions().get(0), TextView.ID_ASSIST,
+ MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
+ item.setIntent(textClassification.getIntent());
+ } else if (hasLegacyAssistItem(textClassification)) {
+ // Legacy primary assist action (Always shown).
final MenuItem item = menu.add(
TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
textClassification.getLabel())
.setIcon(textClassification.getIcon())
.setIntent(textClassification.getIntent());
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- mAssistClickHandlers.put(
- item, TextClassification.createIntentOnClickListener(
- mTextView.getContext(), textClassification.getIntent()));
+ mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
+ TextClassification.createPendingIntent(mTextView.getContext(),
+ textClassification.getIntent())));
}
- final int count = textClassification.getSecondaryActionsCount();
- for (int i = 0; i < count; i++) {
- final OnClickListener onClick1 = getSupportedOnClickListener(
- textClassification.getSecondaryIcon(i),
- textClassification.getSecondaryLabel(i),
- textClassification.getSecondaryIntent(i));
- if (onClick1 == null) {
- continue;
- }
- final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i;
- final MenuItem item = menu.add(
- TextView.ID_ASSIST, Menu.NONE, order,
- textClassification.getSecondaryLabel(i))
- .setIcon(textClassification.getSecondaryIcon(i))
- .setIntent(textClassification.getSecondaryIntent(i));
- item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- mAssistClickHandlers.put(item,
- TextClassification.createIntentOnClickListener(
- mTextView.getContext(), textClassification.getSecondaryIntent(i)));
+ final int count = textClassification.getActions().size();
+ for (int i = 1; i < count; i++) {
+ // Secondary assist action (Never shown).
+ addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
+ MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
+ MenuItem.SHOW_AS_ACTION_NEVER);
}
}
+ private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int intemId, int order,
+ int showAsAction) {
+ final MenuItem item = menu.add(TextView.ID_ASSIST, intemId, order, action.getTitle())
+ .setContentDescription(action.getContentDescription());
+ if (action.shouldShowIcon()) {
+ item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
+ }
+ item.setShowAsAction(showAsAction);
+ mAssistClickHandlers.put(item,
+ TextClassification.createIntentOnClickListener(action.getActionIntent()));
+ return item;
+ }
+
private void clearAssistMenuItems(Menu menu) {
int i = 0;
while (i < menu.size()) {
@@ -4094,15 +4098,11 @@
}
}
- @Nullable
- private OnClickListener getSupportedOnClickListener(
- Drawable icon, CharSequence label, Intent intent) {
- final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
- if (hasUi) {
- return TextClassification.createIntentOnClickListener(
- mTextView.getContext(), intent);
- }
- return null;
+ private boolean hasLegacyAssistItem(TextClassification classification) {
+ // Check whether we have the UI data and and action.
+ return (classification.getIcon() != null || !TextUtils.isEmpty(
+ classification.getLabel())) && (classification.getIntent() != null
+ || classification.getOnClickListener() != null);
}
private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
@@ -4120,7 +4120,7 @@
final Intent intent = assistMenuItem.getIntent();
if (intent != null) {
onClickListener = TextClassification.createIntentOnClickListener(
- mTextView.getContext(), intent);
+ TextClassification.createPendingIntent(mTextView.getContext(), intent));
}
}
if (onClickListener != null) {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 2e8f663..149c88a 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2752,30 +2752,57 @@
<!-- Label for item in the text selection menu to trigger an Email app [CHAR LIMIT=20] -->
<string name="email">Email</string>
+ <!-- Accessibility description for an item in the text selection menu to trigger an Email app [CHAR LIMIT=NONE] -->
+ <string name="email_desc">Email selected address</string>
+
<!-- Label for item in the text selection menu to trigger a Dialer app [CHAR LIMIT=20] -->
<string name="dial">Call</string>
+ <!-- Accessibility description for an item in the text selection menu to call a phone number [CHAR LIMIT=NONE] -->
+ <string name="dial_desc">Call selected phone number</string>
+
<!-- Label for item in the text selection menu to trigger a Map app [CHAR LIMIT=20] -->
<string name="map">Locate</string>
+ <!-- Accessibility description for an item in the text selection menu to open maps for an address [CHAR LIMIT=NONE] -->
+ <string name="map_desc">Locale selected address</string>
+
<!-- Label for item in the text selection menu to trigger a Browser app [CHAR LIMIT=20] -->
<string name="browse">Open</string>
+ <!-- Accessibility description for an item in the text selection menu to open a URL in a browser [CHAR LIMIT=NONE] -->
+ <string name="browse_desc">Open selected URL</string>
+
<!-- Label for item in the text selection menu to trigger an SMS app [CHAR LIMIT=20] -->
<string name="sms">Message</string>
+ <!-- Accessibility description for an item in the text selection menu to send an SMS to a phone number [CHAR LIMIT=NONE] -->
+ <string name="sms_desc">Message selected phone number</string>
+
<!-- Label for item in the text selection menu to trigger adding a contact [CHAR LIMIT=20] -->
<string name="add_contact">Add</string>
+ <!-- Accessibility description for an item in the text selection menu to add the selected detail to contacts [CHAR LIMIT=NONE] -->
+ <string name="add_contact_desc">Add to contacts</string>
+
<!-- Label for item in the text selection menu to view the calendar for the selected time/date [CHAR LIMIT=20] -->
<string name="view_calendar">View</string>
+ <!-- Accessibility description for an item in the text selection menu to view the calendar for a date [CHAR LIMIT=NONE]-->
+ <string name="view_calendar_desc">View selected time in calendar</string>
+
<!-- Label for item in the text selection menu to create a calendar event at the selected time/date [CHAR LIMIT=20] -->
<string name="add_calendar_event">Schedule</string>
+ <!-- Accessibility description for an item in the text selection menu to schedule an event for a date [CHAR LIMIT=NONE] -->
+ <string name="add_calendar_event_desc">Schedule event for selected time</string>
+
<!-- Label for item in the text selection menu to track a selected flight number [CHAR LIMIT=20] -->
<string name="view_flight">Track</string>
+ <!-- Accessibility description for an item in the text selection menu to track a flight [CHAR LIMIT=NONE] -->
+ <string name="view_flight_desc">Track selected flight</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 2f7ae27..1182ca7 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -545,14 +545,23 @@
<java-symbol type="string" name="undo" />
<java-symbol type="string" name="redo" />
<java-symbol type="string" name="email" />
+ <java-symbol type="string" name="email_desc" />
<java-symbol type="string" name="dial" />
+ <java-symbol type="string" name="dial_desc" />
<java-symbol type="string" name="map" />
+ <java-symbol type="string" name="map_desc" />
<java-symbol type="string" name="browse" />
+ <java-symbol type="string" name="browse_desc" />
<java-symbol type="string" name="sms" />
+ <java-symbol type="string" name="sms_desc" />
<java-symbol type="string" name="add_contact" />
+ <java-symbol type="string" name="add_contact_desc" />
<java-symbol type="string" name="view_calendar" />
+ <java-symbol type="string" name="view_calendar_desc" />
<java-symbol type="string" name="add_calendar_event" />
+ <java-symbol type="string" name="add_calendar_event_desc" />
<java-symbol type="string" name="view_flight" />
+ <java-symbol type="string" name="view_flight_desc" />
<java-symbol type="string" name="textSelectionCABTitle" />
<java-symbol type="string" name="BaMmi" />
<java-symbol type="string" name="CLIRDefaultOffNextCallOff" />
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
index 57db153..f96027d 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
@@ -126,11 +126,7 @@
TextClassification classification = mClassifier.classifyText(
text, startIndex, endIndex, mClassificationOptions);
- assertThat(classification,
- isTextClassification(
- classifiedText,
- TextClassifier.TYPE_EMAIL,
- "mailto:" + classifiedText));
+ assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_EMAIL));
}
@Test
@@ -144,11 +140,7 @@
TextClassification classification = mClassifier.classifyText(
text, startIndex, endIndex, mClassificationOptions);
- assertThat(classification,
- isTextClassification(
- classifiedText,
- TextClassifier.TYPE_URL,
- "http://" + classifiedText));
+ assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL));
}
@Test
@@ -158,11 +150,7 @@
String text = "Brandschenkestrasse 110, Zürich, Switzerland";
TextClassification classification = mClassifier.classifyText(
text, 0, text.length(), mClassificationOptions);
- assertThat(classification,
- isTextClassification(
- text,
- TextClassifier.TYPE_ADDRESS,
- "geo:0,0?q=Brandschenkestrasse+110%2C+Z%C3%BCrich%2C+Switzerland"));
+ assertThat(classification, isTextClassification(text, TextClassifier.TYPE_ADDRESS));
}
@Test
@@ -176,11 +164,7 @@
TextClassification classification = mClassifier.classifyText(
text, startIndex, endIndex, mClassificationOptions);
- assertThat(classification,
- isTextClassification(
- classifiedText,
- TextClassifier.TYPE_URL,
- "http://ANDROID.COM"));
+ assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL));
}
@Test
@@ -194,11 +178,7 @@
TextClassification classification = mClassifier.classifyText(
text, startIndex, endIndex, mClassificationOptions);
- assertThat(classification,
- isTextClassification(
- classifiedText,
- TextClassifier.TYPE_DATE,
- null));
+ assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_DATE));
}
@Test
@@ -213,10 +193,7 @@
TextClassification classification = mClassifier.classifyText(
text, startIndex, endIndex, mClassificationOptions);
assertThat(classification,
- isTextClassification(
- classifiedText,
- TextClassifier.TYPE_DATE_TIME,
- null));
+ isTextClassification(classifiedText, TextClassifier.TYPE_DATE_TIME));
}
@Test
@@ -355,39 +332,15 @@
}
private static Matcher<TextClassification> isTextClassification(
- final String text, final String type, final String intentUri) {
+ final String text, final String type) {
return new BaseMatcher<TextClassification>() {
@Override
public boolean matches(Object o) {
if (o instanceof TextClassification) {
TextClassification result = (TextClassification) o;
- final boolean typeRequirementSatisfied;
- String scheme;
- switch (type) {
- case TextClassifier.TYPE_EMAIL:
- scheme = result.getIntent().getData().getScheme();
- typeRequirementSatisfied = "mailto".equals(scheme);
- break;
- case TextClassifier.TYPE_URL:
- scheme = result.getIntent().getData().getScheme();
- typeRequirementSatisfied = "http".equals(scheme)
- || "https".equals(scheme);
- break;
- case TextClassifier.TYPE_ADDRESS:
- scheme = result.getIntent().getData().getScheme();
- typeRequirementSatisfied = "geo".equals(scheme);
- break;
- default:
- typeRequirementSatisfied = true;
- }
-
- return typeRequirementSatisfied
- && text.equals(result.getText())
+ return text.equals(result.getText())
&& result.getEntityCount() > 0
- && type.equals(result.getEntity(0))
- && (intentUri == null
- || intentUri.equals(result.getIntent().getDataString()));
- // TODO: Include other properties.
+ && type.equals(result.getEntity(0));
}
return false;
}
@@ -395,8 +348,7 @@
@Override
public void describeTo(Description description) {
description.appendText("text=").appendValue(text)
- .appendText(", type=").appendValue(type)
- .appendText(", intent.data=").appendValue(intentUri);
+ .appendText(", type=").appendValue(type);
}
};
}
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
index ada19fc..afc4bd5 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
@@ -17,15 +17,19 @@
package android.view.textclassifier;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Icon;
import android.os.LocaleList;
import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
@@ -41,47 +45,44 @@
@RunWith(AndroidJUnit4.class)
public class TextClassificationTest {
- public BitmapDrawable generateTestDrawable(int width, int height, int colorValue) {
+ public Icon generateTestIcon(int width, int height, int colorValue) {
final int numPixels = width * height;
final int[] colors = new int[numPixels];
for (int i = 0; i < numPixels; ++i) {
colors[i] = colorValue;
}
final Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
- final BitmapDrawable drawable = new BitmapDrawable(Resources.getSystem(), bitmap);
- drawable.setTargetDensity(bitmap.getDensity());
- return drawable;
+ return Icon.createWithBitmap(bitmap);
}
@Test
public void testParcel() {
+ final Context context = InstrumentationRegistry.getTargetContext();
final String text = "text";
- final BitmapDrawable primaryIcon = generateTestDrawable(16, 16, Color.RED);
- final String primaryLabel = "primarylabel";
- final Intent primaryIntent = new Intent("primaryintentaction");
- final View.OnClickListener primaryOnClick = v -> { };
- final BitmapDrawable secondaryIcon0 = generateTestDrawable(32, 288, Color.GREEN);
- final String secondaryLabel0 = "secondarylabel0";
- final Intent secondaryIntent0 = new Intent("secondaryintentaction0");
- final BitmapDrawable secondaryIcon1 = generateTestDrawable(576, 288, Color.BLUE);
- final String secondaryLabel1 = "secondaryLabel1";
- final Intent secondaryIntent1 = null;
- final BitmapDrawable secondaryIcon2 = null;
- final String secondaryLabel2 = null;
- final Intent secondaryIntent2 = new Intent("secondaryintentaction2");
- final ColorDrawable secondaryIcon3 = new ColorDrawable(Color.CYAN);
- final String secondaryLabel3 = null;
- final Intent secondaryIntent3 = null;
+
+ final Icon primaryIcon = generateTestIcon(576, 288, Color.BLUE);
+ final String primaryLabel = "primaryLabel";
+ final String primaryDescription = "primaryDescription";
+ final Intent primaryIntent = new Intent("primaryIntentAction");
+ final PendingIntent primaryPendingIntent = PendingIntent.getActivity(context, 0,
+ primaryIntent, 0);
+ final RemoteAction remoteAction0 = new RemoteAction(primaryIcon, primaryLabel,
+ primaryDescription, primaryPendingIntent);
+
+ final Icon secondaryIcon = generateTestIcon(32, 288, Color.GREEN);
+ final String secondaryLabel = "secondaryLabel";
+ final String secondaryDescription = "secondaryDescription";
+ final Intent secondaryIntent = new Intent("secondaryIntentAction");
+ final PendingIntent secondaryPendingIntent = PendingIntent.getActivity(context, 0,
+ secondaryIntent, 0);
+ final RemoteAction remoteAction1 = new RemoteAction(secondaryIcon, secondaryLabel,
+ secondaryDescription, secondaryPendingIntent);
+
final String signature = "signature";
final TextClassification reference = new TextClassification.Builder()
.setText(text)
- .setPrimaryAction(primaryIntent, primaryLabel, primaryIcon)
- .setOnClickListener(primaryOnClick)
- .addSecondaryAction(null, null, null) // ignored
- .addSecondaryAction(secondaryIntent0, secondaryLabel0, secondaryIcon0)
- .addSecondaryAction(secondaryIntent1, secondaryLabel1, secondaryIcon1)
- .addSecondaryAction(secondaryIntent2, secondaryLabel2, secondaryIcon2)
- .addSecondaryAction(secondaryIntent3, secondaryLabel3, secondaryIcon3)
+ .addAction(remoteAction0)
+ .addAction(remoteAction1)
.setEntityType(TextClassifier.TYPE_ADDRESS, 0.3f)
.setEntityType(TextClassifier.TYPE_PHONE, 0.7f)
.setSignature(signature)
@@ -95,45 +96,25 @@
assertEquals(text, result.getText());
assertEquals(signature, result.getSignature());
- assertEquals(4, result.getSecondaryActionsCount());
+ assertEquals(2, result.getActions().size());
- // Primary action (re-use existing icon).
- final Bitmap resPrimaryIcon = ((BitmapDrawable) result.getIcon()).getBitmap();
- assertEquals(primaryIcon.getBitmap().getPixel(0, 0), resPrimaryIcon.getPixel(0, 0));
- assertEquals(16, resPrimaryIcon.getWidth());
- assertEquals(16, resPrimaryIcon.getHeight());
- assertEquals(primaryLabel, result.getLabel());
- assertEquals(primaryIntent.getAction(), result.getIntent().getAction());
- assertEquals(null, result.getOnClickListener()); // Non-parcelable.
+ // Legacy API.
+ assertNull(result.getIcon());
+ assertNull(result.getLabel());
+ assertNull(result.getIntent());
+ assertNull(result.getOnClickListener());
- // Secondary action 0 (scale with height limit).
- final Bitmap resSecondaryIcon0 = ((BitmapDrawable) result.getSecondaryIcon(0)).getBitmap();
- assertEquals(secondaryIcon0.getBitmap().getPixel(0, 0), resSecondaryIcon0.getPixel(0, 0));
- assertEquals(16, resSecondaryIcon0.getWidth());
- assertEquals(144, resSecondaryIcon0.getHeight());
- assertEquals(secondaryLabel0, result.getSecondaryLabel(0));
- assertEquals(secondaryIntent0.getAction(), result.getSecondaryIntent(0).getAction());
+ // Primary action.
+ final RemoteAction primaryAction = result.getActions().get(0);
+ assertEquals(primaryLabel, primaryAction.getTitle());
+ assertEquals(primaryDescription, primaryAction.getContentDescription());
+ assertEquals(primaryPendingIntent, primaryAction.getActionIntent());
- // Secondary action 1 (scale with width limit).
- final Bitmap resSecondaryIcon1 = ((BitmapDrawable) result.getSecondaryIcon(1)).getBitmap();
- assertEquals(secondaryIcon1.getBitmap().getPixel(0, 0), resSecondaryIcon1.getPixel(0, 0));
- assertEquals(144, resSecondaryIcon1.getWidth());
- assertEquals(72, resSecondaryIcon1.getHeight());
- assertEquals(secondaryLabel1, result.getSecondaryLabel(1));
- assertEquals(null, result.getSecondaryIntent(1));
-
- // Secondary action 2 (no icon).
- assertEquals(null, result.getSecondaryIcon(2));
- assertEquals(null, result.getSecondaryLabel(2));
- assertEquals(secondaryIntent2.getAction(), result.getSecondaryIntent(2).getAction());
-
- // Secondary action 3 (convert non-bitmap drawable with negative size).
- final Bitmap resSecondaryIcon3 = ((BitmapDrawable) result.getSecondaryIcon(3)).getBitmap();
- assertEquals(secondaryIcon3.getColor(), resSecondaryIcon3.getPixel(0, 0));
- assertEquals(1, resSecondaryIcon3.getWidth());
- assertEquals(1, resSecondaryIcon3.getHeight());
- assertEquals(null, result.getSecondaryLabel(3));
- assertEquals(null, result.getSecondaryIntent(3));
+ // Secondary action.
+ final RemoteAction secondaryAction = result.getActions().get(1);
+ assertEquals(secondaryLabel, secondaryAction.getTitle());
+ assertEquals(secondaryDescription, secondaryAction.getContentDescription());
+ assertEquals(secondaryPendingIntent, secondaryAction.getActionIntent());
// Entities.
assertEquals(2, result.getEntityCount());
@@ -144,6 +125,43 @@
}
@Test
+ public void testParcelLegacy() {
+ final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final String text = "text";
+
+ final Icon icon = generateTestIcon(384, 192, Color.BLUE);
+ final String label = "label";
+ final Intent intent = new Intent("intent");
+ final View.OnClickListener onClickListener = v -> { };
+
+ final String signature = "signature";
+ final TextClassification reference = new TextClassification.Builder()
+ .setText(text)
+ .setIcon(icon.loadDrawable(context))
+ .setLabel(label)
+ .setIntent(intent)
+ .setOnClickListener(onClickListener)
+ .setEntityType(TextClassifier.TYPE_ADDRESS, 0.3f)
+ .setEntityType(TextClassifier.TYPE_PHONE, 0.7f)
+ .setSignature(signature)
+ .build();
+
+ // Parcel and unparcel
+ final Parcel parcel = Parcel.obtain();
+ reference.writeToParcel(parcel, reference.describeContents());
+ parcel.setDataPosition(0);
+ final TextClassification result = TextClassification.CREATOR.createFromParcel(parcel);
+
+ final Bitmap resultIcon = ((BitmapDrawable) result.getIcon()).getBitmap();
+ assertEquals(icon.getBitmap().getPixel(0, 0), resultIcon.getPixel(0, 0));
+ assertEquals(192, resultIcon.getWidth());
+ assertEquals(96, resultIcon.getHeight());
+ assertEquals(label, result.getLabel());
+ assertEquals(intent.getAction(), result.getIntent().getAction());
+ assertNull(result.getOnClickListener());
+ }
+
+ @Test
public void testParcelOptions() {
Calendar referenceTime = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US);
referenceTime.setTimeInMillis(946771200000L); // 2000-01-02