Merge "Change TextClassification to use RemoteActions" into pi-dev

am: 08a75e0ee7

Change-Id: I411c8b207e24c6a4e9ca8660619fae1e0a3a8ef7
This commit is contained in:
Jan Althaus
2018-03-28 17:23:56 +00:00
committed by android-build-merger
9 changed files with 469 additions and 553 deletions

View File

@@ -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);
}

View File

@@ -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;
}
/**
@@ -78,6 +76,20 @@ public final class RemoteAction implements Parcelable {
return mEnabled;
}
/**
* 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.
*/
@@ -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();
}

View File

@@ -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)
* 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()
*/
@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 Use {@link #getActions()} instead.
*/
@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()
*/
@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 Use {@link #getActions()} instead.
*/
@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()
*/
@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 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;
}
@@ -433,33 +378,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.
*
@@ -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);
}
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 @@ 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();
}

View File

@@ -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,50 +419,27 @@ public final class TextClassifierImpl implements TextClassifier {
}
}
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.
*/
@@ -587,6 +565,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.
*/
@@ -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));
}
}
}

View File

@@ -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,43 +4046,46 @@ 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()));
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 @@ 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) {

View File

@@ -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. -->

View File

@@ -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" />

View File

@@ -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);
}
};
}

View File

@@ -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());
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());
@@ -143,6 +124,43 @@ public class TextClassificationTest {
assertEquals(0.3f, result.getConfidenceScore(TextClassifier.TYPE_ADDRESS), 1e-7f);
}
@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);