blob: 3056c9d044f97bfad12a58a6661f4f79717bf202 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.textclassifier;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.core.os.LocaleListCompat;
import androidx.core.util.Preconditions;
import androidx.textclassifier.TextClassifier.EntityType;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Information for generating a widget to handle classified text.
*
* <p>A TextClassification object contains icons, labels, and intents that may be used to build a
* widget that can be used to act on classified text. There is the concept of a <i>primary
* action</i> and other <i>secondary actions</i>.
*
* <p>e.g. building a view that, when clicked, shares the classified text with the preferred app:
*
* <pre>{@code
* // Called preferably outside the UiThread.
* TextClassification classification = textClassifier.classifyText(allText, 10, 25);
*
* // Called on the UiThread.
* Button button = new Button(context);
* button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
* button.setText(classification.getLabel());
* button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
* }</pre>
*
* TODO: describe how to start action mode for classified text.
*/
public final class TextClassification implements Parcelable {
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
static final TextClassification EMPTY = new TextClassification.Builder().build();
// TODO: 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;
@Nullable private final String mText;
@Nullable private final Drawable mPrimaryIcon;
@Nullable private final String mPrimaryLabel;
@Nullable private final Intent mPrimaryIntent;
@NonNull private final List<Drawable> mSecondaryIcons;
@NonNull private final List<String> mSecondaryLabels;
@NonNull private final List<Intent> mSecondaryIntents;
@NonNull private final EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextClassification(
@Nullable String text,
@Nullable Drawable primaryIcon,
@Nullable String primaryLabel,
@Nullable Intent primaryIntent,
@NonNull List<Drawable> secondaryIcons,
@NonNull List<String> secondaryLabels,
@NonNull List<Intent> secondaryIntents,
@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;
mSecondaryIcons = secondaryIcons;
mSecondaryLabels = secondaryLabels;
mSecondaryIntents = secondaryIntents;
mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
/**
* Gets the classified text.
*/
@Nullable
public String getText() {
return mText;
}
/**
* Returns the number of entities found in the classified text.
*/
@IntRange(from = 0)
public int getEntityCount() {
return mEntityConfidence.getEntities().size();
}
/**
* Returns the entity at the specified index. Entities are ordered from high confidence
* to low confidence.
*
* @throws IndexOutOfBoundsException if the specified index is out of range.
* @see #getEntityCount() for the number of entities available.
*/
@NonNull
public @EntityType String getEntity(int index) {
return mEntityConfidence.getEntities().get(index);
}
/**
* Returns the confidence score for the specified entity. The value ranges from
* 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
* classified text.
*/
@FloatRange(from = 0.0, to = 1.0)
public float getConfidenceScore(@EntityType String entity) {
return mEntityConfidence.getConfidenceScore(entity);
}
/**
* 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()
*/
@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)
*/
@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);
}
/**
* 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)
*/
@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);
}
/**
* Returns the <i>primary</i> intent that may be fired to act on the classified text.
*
* @see #getSecondaryIntent(int)
*/
@Nullable
public Intent getIntent() {
return mPrimaryIntent;
}
/**
* Returns the signature for this object.
* The TextClassifier that generates this object may use it as a way to internally identify
* this object.
*/
@NonNull
public String getSignature() {
return mSignature;
}
@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);
}
@Override
public int describeContents() {
return 0;
}
@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);
}
dest.writeString(mPrimaryLabel);
dest.writeInt(mPrimaryIntent != null ? 1 : 0);
if (mPrimaryIntent != null) {
mPrimaryIntent.writeToParcel(dest, flags);
}
dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
dest.writeStringList(mSecondaryLabels);
dest.writeTypedList(mSecondaryIntents);
mEntityConfidence.writeToParcel(dest, flags);
dest.writeString(mSignature);
}
public static final Parcelable.Creator<TextClassification> CREATOR =
new Parcelable.Creator<TextClassification>() {
@Override
public TextClassification createFromParcel(Parcel in) {
return new TextClassification(in);
}
@Override
public TextClassification[] newArray(int size) {
return new TextClassification[size];
}
};
private TextClassification(Parcel in) {
mText = in.readString();
mPrimaryIcon = in.readInt() == 0
? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
mPrimaryLabel = in.readString();
mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
mSecondaryLabels = in.createStringArrayList();
mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
mSignature = in.readString();
}
/**
* Returns a Bitmap representation of the Drawable
*
* @param drawable The drawable to convert.
* @param maxDims The maximum edge length of the resulting bitmap (in pixels).
*/
@Nullable
private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
if (drawable == null) {
return null;
}
final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
final double scaleWidth = ((double) maxDims) / actualWidth;
final double scaleHeight = ((double) maxDims) / actualHeight;
final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
final int width = (int) (actualWidth * scale);
final int height = (int) (actualHeight * scale);
if (drawable instanceof BitmapDrawable) {
final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if (actualWidth != width || actualHeight != height) {
return Bitmap.createScaledBitmap(
bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
} else {
return bitmapDrawable.getBitmap();
}
} else {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
/**
* 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(null, bitmap));
} else {
drawables.add(null);
}
}
return drawables;
}
/**
* Builder for building {@link TextClassification} objects.
*
* <p>e.g.
*
* <pre>{@code
* TextClassification classification = new TextClassification.Builder()
* .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)
* .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 final Map<String, Float> mEntityConfidence = new ArrayMap<>();
@Nullable Drawable mPrimaryIcon;
@Nullable String mPrimaryLabel;
@Nullable Intent mPrimaryIntent;
@NonNull private String mSignature = "";
/**
* Sets the classified text.
*/
public Builder setText(@Nullable String text) {
mText = text;
return this;
}
/**
* Sets an entity type for the classification result and assigns a confidence score.
* If a confidence score had already been set for the specified entity type, this will
* override that score.
*
* @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
* 0 implies the entity does not exist for the classified text.
* Values greater than 1 are clamped to 1.
*/
public Builder setEntityType(
@NonNull @EntityType String type,
@FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
mEntityConfidence.put(type, confidenceScore);
return this;
}
/**
* 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)
*/
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();
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)
*/
public Builder setIcon(@Nullable Drawable icon) {
mPrimaryIcon = icon;
return this;
}
/**
* 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)
*/
public Builder setLabel(@Nullable String label) {
mPrimaryLabel = label;
return this;
}
/**
* Sets the intent for the <i>primary</i> action that may be fired to act on the classified
* text.
*
* @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setIntent(@Nullable Intent intent) {
mPrimaryIntent = intent;
return this;
}
/**
* Sets a signature for the TextClassification object.
* The TextClassifier that generates the TextClassification object may use it as a way to
* internally identify the TextClassification object.
*/
public Builder setSignature(@NonNull String signature) {
mSignature = Preconditions.checkNotNull(signature);
return this;
}
/**
* Builds and returns a {@link TextClassification} object.
*/
public TextClassification build() {
return new TextClassification(
mText,
mPrimaryIcon, mPrimaryLabel, mPrimaryIntent,
mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
mEntityConfidence, mSignature);
}
}
/**
* Optional input parameters for generating TextClassification.
*/
public static final class Options implements Parcelable {
private @Nullable LocaleListCompat mDefaultLocales;
private @Nullable Calendar mReferenceTime;
private @Nullable String mCallingPackageName;
public Options() {}
/**
* @param defaultLocales ordered list of locale preferences that may be used to disambiguate
* the provided text. If no locale preferences exist, set this to null or an empty
* locale list.
*/
public Options setDefaultLocales(@Nullable LocaleListCompat defaultLocales) {
mDefaultLocales = defaultLocales;
return this;
}
/**
* @param referenceTime reference time based on which relative dates (e.g. "tomorrow" should
* be interpreted. This should usually be the time when the text was originally
* composed. If no reference time is set, now is used.
*/
public Options setReferenceTime(Calendar referenceTime) {
mReferenceTime = referenceTime;
return this;
}
/**
* @param packageName name of the package from which the call was made.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public Options setCallingPackageName(@Nullable String packageName) {
mCallingPackageName = packageName;
return this;
}
/**
* @return ordered list of locale preferences that can be used to disambiguate
* the provided text.
*/
@Nullable
public LocaleListCompat getDefaultLocales() {
return mDefaultLocales;
}
/**
* @return reference time based on which relative dates (e.g. "tomorrow") should be
* interpreted.
*/
@Nullable
public Calendar getReferenceTime() {
return mReferenceTime;
}
/**
* @return name of the package from which the call was made.
*/
@Nullable
public String getCallingPackageName() {
return mCallingPackageName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mDefaultLocales != null ? 1 : 0);
if (mDefaultLocales != null) {
dest.writeString(mDefaultLocales.toLanguageTags());
}
dest.writeInt(mReferenceTime != null ? 1 : 0);
if (mReferenceTime != null) {
dest.writeSerializable(mReferenceTime);
}
dest.writeString(mCallingPackageName);
}
public static final Parcelable.Creator<Options> CREATOR =
new Parcelable.Creator<Options>() {
@Override
public Options createFromParcel(Parcel in) {
return new Options(in);
}
@Override
public Options[] newArray(int size) {
return new Options[size];
}
};
private Options(Parcel in) {
if (in.readInt() > 0) {
mDefaultLocales = LocaleListCompat.forLanguageTags(in.readString());
}
if (in.readInt() > 0) {
mReferenceTime = (Calendar) in.readSerializable();
}
mCallingPackageName = in.readString();
}
}
}