diff options
| -rw-r--r-- | api/current.txt | 27 | ||||
| -rw-r--r-- | core/java/android/app/RemoteAction.java | 24 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassification.java | 355 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifierImpl.java | 290 | ||||
| -rw-r--r-- | core/java/android/widget/Editor.java | 76 | ||||
| -rw-r--r-- | core/res/res/values/strings.xml | 27 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 9 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java | 68 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java | 154 |
9 files changed, 473 insertions, 557 deletions
diff --git a/api/current.txt b/api/current.txt index dda00ba8e94d..9e18b73a8732 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5877,6 +5877,8 @@ package android.app { 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 @@ package android.view.textclassifier { 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 @@ package android.view.textclassifier { 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 e7fe407b29b3..47741c0215b4 100644 --- a/core/java/android/app/RemoteAction.java +++ b/core/java/android/app/RemoteAction.java @@ -18,14 +18,9 @@ package android.app; 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 @@ public final class RemoteAction implements Parcelable { 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 @@ public final class RemoteAction implements Parcelable { 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 @@ public final class RemoteAction implements Parcelable { mContentDescription = contentDescription; mActionIntent = intent; mEnabled = true; + mShouldShowIcon = true; } /** @@ -79,6 +77,20 @@ public final class RemoteAction implements Parcelable { } /** + * 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 @@ public final class RemoteAction implements Parcelable { 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 @@ public final class RemoteAction implements Parcelable { 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 b5c9de99e0c2..630007bad9b1 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -21,6 +21,8 @@ import android.annotation.IntDef; 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.Retention; 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 @@ import java.util.Map; * 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 @@ public final class TextClassification implements Parcelable { */ 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 @@ public final class TextClassification implements Parcelable { } @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 @@ public final class TextClassification implements Parcelable { } /** - * 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) - */ - @IntRange(from = 0) - public int getSecondaryActionsCount() { - return mSecondaryIntents.size(); - } - - /** - * Returns one of the <i>secondary</i> 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 #getSecondaryActionsCount() for the number of actions available. - * @see #getSecondaryIntent(int) - * @see #getSecondaryLabel(int) - * @see #getIcon() + * 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. */ - @Nullable - public Drawable getSecondaryIcon(int index) { - return mSecondaryIcons.get(index); + public List<RemoteAction> getActions() { + return mActions; } /** - * Returns an icon for the <i>primary</i> intent that may be 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. * - * @see #getSecondaryIcon(int) + * @deprecated Use {@link #getActions()} instead. */ + @Deprecated @Nullable public Drawable getIcon() { - return mPrimaryIcon; - } - - /** - * Returns one of the <i>secondary</i> 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 #getSecondaryActionsCount() - * @see #getSecondaryIntent(int) - * @see #getSecondaryIcon(int) - * @see #getLabel() - */ - @Nullable - public CharSequence getSecondaryLabel(int index) { - return mSecondaryLabels.get(index); + return mLegacyIcon; } /** - * Returns a label for the <i>primary</i> intent 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. * - * @see #getSecondaryLabel(int) + * @deprecated Use {@link #getActions()} instead. */ + @Deprecated @Nullable public CharSequence getLabel() { - return mPrimaryLabel; - } - - /** - * Returns one of the <i>secondary</i> 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 #getSecondaryActionsCount() - * @see #getSecondaryLabel(int) - * @see #getSecondaryIcon(int) - * @see #getIntent() - */ - @Nullable - public Intent getSecondaryIntent(int index) { - return mSecondaryIntents.get(index); + return mLegacyLabel; } /** - * Returns the <i>primary</i> intent that may be fired to act on the classified text. + * Returns an intent that may be fired to act on the classified text. * - * @see #getSecondaryIntent(int) + * @deprecated Use {@link #getActions()} instead. */ + @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 @@ public final class TextClassification implements Parcelable { @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 @@ public final class TextClassification implements Parcelable { } /** - * 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 @@ public final class TextClassification implements Parcelable { * .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 @@ public final class TextClassification implements Parcelable { } /** - * 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); - } - return this; - } - - /** - * Removes all the <i>secondary</i> actions. - */ - public Builder clearSecondaryActions() { - mSecondaryIntents.clear(); - mSecondaryLabels.clear(); - mSecondaryIcons.clear(); + public Builder addAction(@NonNull RemoteAction action) { + Preconditions.checkArgument(action != null); + mActions.add(action); 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 @@ public final class TextClassification implements Parcelable { * 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 @@ public final class TextClassification implements Parcelable { * 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 @@ public final class TextClassification implements Parcelable { * 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 @@ public final class TextClassification implements Parcelable { * 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 @@ public final class TextClassification implements Parcelable { @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 @@ public final class TextClassification implements Parcelable { 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 c2fb032d3e60..a0f4d5c2054e 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -19,6 +19,7 @@ package android.view.textclassifier; 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.Context; 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,48 +419,25 @@ public final class TextClassifierImpl implements TextClassifier { } } - addActions(builder, IntentFactory.create( - mContext, referenceTime, highestScoringResult, classifiedText)); - - 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); - } + 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(); } /** @@ -588,6 +566,60 @@ public final class TextClassifierImpl implements TextClassifier { } /** + * 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 @@ public final class TextClassifierImpl implements TextClassifier { 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 @@ public final class TextClassifierImpl implements TextClassifier { 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 @@ public final class TextClassifierImpl implements TextClassifier { 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 @@ public final class TextClassifierImpl implements TextClassifier { } 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 57d64b93872c..92f496a87c3f 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; 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,41 +4046,44 @@ public class Editor { 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())); - } - 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))); + mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener( + TextClassification.createPendingIntent(mTextView.getContext(), + textClassification.getIntent()))); + } + 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) { @@ -4094,15 +4098,11 @@ public class Editor { } } - @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 @@ public class Editor { 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 2e8f663a9306..149c88a3c0e7 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 2f7ae271b579..1182ca7c7c09 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 57db153ddcd5..f96027db5db5 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { 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 @@ public class TextClassificationManagerTest { } 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 @@ public class TextClassificationManagerTest { @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 ada19fc59264..afc4bd5aa783 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 @@ import java.util.TimeZone; @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 @@ public class TextClassificationTest { assertEquals(text, result.getText()); assertEquals(signature, result.getSignature()); - assertEquals(4, result.getSecondaryActionsCount()); - - // 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. - - // 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()); - - // 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)); + assertEquals(2, result.getActions().size()); + + // Legacy API. + assertNull(result.getIcon()); + assertNull(result.getLabel()); + assertNull(result.getIntent()); + assertNull(result.getOnClickListener()); + + // Primary action. + final RemoteAction primaryAction = result.getActions().get(0); + assertEquals(primaryLabel, primaryAction.getTitle()); + assertEquals(primaryDescription, primaryAction.getContentDescription()); + assertEquals(primaryPendingIntent, primaryAction.getActionIntent()); + + // 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 @@ public class TextClassificationTest { } @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 |