diff options
| -rw-r--r-- | api/current.txt | 5 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/SmartSelection.java | 14 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassification.java | 28 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifier.java | 17 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifierImpl.java | 209 | ||||
| -rw-r--r-- | core/res/res/values/strings.xml | 9 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 3 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java | 7 |
8 files changed, 228 insertions, 64 deletions
diff --git a/api/current.txt b/api/current.txt index 26d02bd81482..b7016a2221f1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -49896,7 +49896,9 @@ package android.view.textclassifier { ctor public TextClassification.Options(); method public int describeContents(); method public android.os.LocaleList getDefaultLocales(); + method public java.util.Calendar getReferenceTime(); method public android.view.textclassifier.TextClassification.Options setDefaultLocales(android.os.LocaleList); + method public android.view.textclassifier.TextClassification.Options setReferenceTime(java.util.Calendar); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassification.Options> CREATOR; } @@ -49921,7 +49923,10 @@ package android.view.textclassifier { field public static final int ENTITY_PRESET_NONE = 1; // 0x1 field public static final android.view.textclassifier.TextClassifier NO_OP; field public static final java.lang.String TYPE_ADDRESS = "address"; + field public static final java.lang.String TYPE_DATE = "date"; + field public static final java.lang.String TYPE_DATE_TIME = "datetime"; field public static final java.lang.String TYPE_EMAIL = "email"; + field public static final java.lang.String TYPE_FLIGHT_NUMBER = "flight"; field public static final java.lang.String TYPE_OTHER = "other"; field public static final java.lang.String TYPE_PHONE = "phone"; field public static final java.lang.String TYPE_UNKNOWN = ""; diff --git a/core/java/android/view/textclassifier/SmartSelection.java b/core/java/android/view/textclassifier/SmartSelection.java index 2c93a19bbe0e..8edf97ea0336 100644 --- a/core/java/android/view/textclassifier/SmartSelection.java +++ b/core/java/android/view/textclassifier/SmartSelection.java @@ -16,6 +16,7 @@ package android.view.textclassifier; +import android.annotation.Nullable; import android.content.res.AssetFileDescriptor; /** @@ -146,11 +147,24 @@ final class SmartSelection { final String mCollection; /** float range: 0 - 1 */ final float mScore; + @Nullable final DatetimeParseResult mDatetime; ClassificationResult(String collection, float score) { mCollection = collection; mScore = score; + mDatetime = null; } + + ClassificationResult(String collection, float score, DatetimeParseResult datetime) { + mCollection = collection; + mScore = score; + mDatetime = datetime; + } + } + + /** Parsed date information for the classification result. */ + static final class DatetimeParseResult { + long mMsSinceEpoch; } /** Represents a result of Annotate call. */ diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index 7089677d744b..54e93d5afd88 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -36,6 +36,7 @@ import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.Map; @@ -592,6 +593,7 @@ public final class TextClassification { public static final class Options implements Parcelable { private @Nullable LocaleList mDefaultLocales; + private @Nullable Calendar mReferenceTime; public Options() {} @@ -606,6 +608,16 @@ public final class TextClassification { } /** + * @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; + } + + /** * @return ordered list of locale preferences that can be used to disambiguate * the provided text. */ @@ -614,6 +626,15 @@ public final class TextClassification { return mDefaultLocales; } + /** + * @return reference time based on which relative dates (e.g. "tomorrow") should be + * interpreted. + */ + @Nullable + public Calendar getReferenceTime() { + return mReferenceTime; + } + @Override public int describeContents() { return 0; @@ -625,6 +646,10 @@ public final class TextClassification { if (mDefaultLocales != null) { mDefaultLocales.writeToParcel(dest, flags); } + dest.writeInt(mReferenceTime != null ? 1 : 0); + if (mReferenceTime != null) { + dest.writeSerializable(mReferenceTime); + } } public static final Parcelable.Creator<Options> CREATOR = @@ -644,6 +669,9 @@ public final class TextClassification { if (in.readInt() > 0) { mDefaultLocales = LocaleList.CREATOR.createFromParcel(in); } + if (in.readInt() > 0) { + mReferenceTime = (Calendar) in.readSerializable(); + } } } diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index e9715c5121eb..04ab4474a40c 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -47,12 +47,26 @@ public interface TextClassifier { /** @hide */ String DEFAULT_LOG_TAG = "androidtc"; + /** The TextClassifier failed to run. */ String TYPE_UNKNOWN = ""; + /** The classifier ran, but didn't recognize a known entity. */ String TYPE_OTHER = "other"; + /** E-mail address (e.g. "noreply@android.com"). */ String TYPE_EMAIL = "email"; + /** Phone number (e.g. "555-123 456"). */ String TYPE_PHONE = "phone"; + /** Physical address. */ String TYPE_ADDRESS = "address"; + /** Web URL. */ String TYPE_URL = "url"; + /** Time reference that is no more specific than a date. May be absolute such as "01/01/2000" or + * relative like "tomorrow". **/ + String TYPE_DATE = "date"; + /** Time reference that includes a specific time. May be absolute such as "01/01/2000 5:30pm" or + * relative like "tomorrow at 5:30pm". **/ + String TYPE_DATE_TIME = "datetime"; + /** Flight number in IATA format. */ + String TYPE_FLIGHT_NUMBER = "flight"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -63,6 +77,9 @@ public interface TextClassifier { TYPE_PHONE, TYPE_ADDRESS, TYPE_URL, + TYPE_DATE, + TYPE_DATE_TIME, + TYPE_FLIGHT_NUMBER, }) @interface EntityType {} diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 7db0e76d901f..f434452bb9ec 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -18,7 +18,9 @@ package android.view.textclassifier; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.SearchManager; import android.content.ComponentName; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -28,6 +30,7 @@ import android.net.Uri; import android.os.LocaleList; import android.os.ParcelFileDescriptor; import android.provider.Browser; +import android.provider.CalendarContract; import android.provider.ContactsContract; import android.provider.Settings; import android.text.util.Linkify; @@ -42,6 +45,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -49,6 +53,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,7 +78,10 @@ final class TextClassifierImpl implements TextClassifier { TextClassifier.TYPE_ADDRESS, TextClassifier.TYPE_EMAIL, TextClassifier.TYPE_PHONE, - TextClassifier.TYPE_URL)); + TextClassifier.TYPE_URL, + TextClassifier.TYPE_DATE, + TextClassifier.TYPE_DATE_TIME, + TextClassifier.TYPE_FLIGHT_NUMBER)); private static final List<String> ENTITY_TYPES_BASE = Collections.unmodifiableList(Arrays.asList( TextClassifier.TYPE_ADDRESS, @@ -167,9 +175,8 @@ final class TextClassifierImpl implements TextClassifier { .classifyText(string, startIndex, endIndex, getHintFlags(string, startIndex, endIndex)); if (results.length > 0) { - final TextClassification classificationResult = - createClassificationResult(results, string, startIndex, endIndex); - return classificationResult; + return createClassificationResult( + results, string, startIndex, endIndex, options.getReferenceTime()); } } } catch (Throwable t) { @@ -410,18 +417,24 @@ final class TextClassifierImpl implements TextClassifier { private TextClassification createClassificationResult( SmartSelection.ClassificationResult[] classifications, - String text, int start, int end) { + String text, int start, int end, @Nullable Calendar referenceTime) { final String classifiedText = text.substring(start, end); final TextClassification.Builder builder = new TextClassification.Builder() .setText(classifiedText); final int size = classifications.length; + SmartSelection.ClassificationResult highestScoringResult = null; + float highestScore = Float.MIN_VALUE; for (int i = 0; i < size; i++) { builder.setEntityType(classifications[i].mCollection, classifications[i].mScore); + if (classifications[i].mScore > highestScore) { + highestScoringResult = classifications[i]; + highestScore = classifications[i].mScore; + } } - final String type = getHighestScoringType(classifications); - addActions(builder, IntentFactory.create(mContext, type, classifiedText)); + addActions(builder, IntentFactory.create( + mContext, referenceTime, highestScoringResult, classifiedText)); return builder.setSignature(getSignature(text, start, end)).build(); } @@ -441,11 +454,10 @@ final class TextClassifierImpl implements TextClassifier { } if (resolveInfo != null && resolveInfo.activityInfo != null) { final String packageName = resolveInfo.activityInfo.packageName; - CharSequence label; + final String label = IntentFactory.getLabel(mContext, intent); Drawable icon; if ("android".equals(packageName)) { // Requires the chooser to find an activity to handle the intent. - label = IntentFactory.getLabel(mContext, intent); icon = null; } else { // A default activity will handle the intent. @@ -455,16 +467,11 @@ final class TextClassifierImpl implements TextClassifier { if (icon == null) { icon = resolveInfo.loadIcon(pm); } - label = resolveInfo.activityInfo.loadLabel(pm); - if (label == null) { - label = resolveInfo.loadLabel(pm); - } } - final String labelString = (label != null) ? label.toString() : null; if (i == 0) { - builder.setPrimaryAction(intent, labelString, icon); + builder.setPrimaryAction(intent, label, icon); } else { - builder.addSecondaryAction(intent, labelString, icon); + builder.addSecondaryAction(intent, label, icon); } } } @@ -483,23 +490,6 @@ final class TextClassifierImpl implements TextClassifier { return flag; } - private static String getHighestScoringType(SmartSelection.ClassificationResult[] types) { - if (types.length < 1) { - return ""; - } - - String type = types[0].mCollection; - float highestScore = types[0].mScore; - final int size = types.length; - for (int i = 1; i < size; i++) { - if (types[i].mScore > highestScore) { - type = types[i].mCollection; - highestScore = types[i].mScore; - } - } - return type; - } - /** * Closes the ParcelFileDescriptor and logs any errors that occur. */ @@ -514,58 +504,139 @@ final class TextClassifierImpl implements TextClassifier { /** * Creates intents based on the classification type. */ - private static final class IntentFactory { + static final class IntentFactory { + + private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5); + private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1); private IntentFactory() {} @NonNull - public static List<Intent> create(Context context, String type, String text) { - final List<Intent> intents = new ArrayList<>(); - type = type.trim().toLowerCase(Locale.ENGLISH); + public static List<Intent> create( + Context context, + @Nullable Calendar referenceTime, + SmartSelection.ClassificationResult classification, + String text) { + final String type = classification.mCollection.trim().toLowerCase(Locale.ENGLISH); text = text.trim(); switch (type) { case TextClassifier.TYPE_EMAIL: - intents.add(new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse(String.format("mailto:%s", text)))); - intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) - .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) - .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); - break; + return createForEmail(text); case TextClassifier.TYPE_PHONE: - intents.add(new Intent(Intent.ACTION_DIAL) - .setData(Uri.parse(String.format("tel:%s", text)))); - intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) - .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) - .putExtra(ContactsContract.Intents.Insert.PHONE, text)); - intents.add(new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse(String.format("smsto:%s", text)))); - break; + return createForPhone(text); case TextClassifier.TYPE_ADDRESS: - intents.add(new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); - break; + return createForAddress(text); case TextClassifier.TYPE_URL: - final String httpPrefix = "http://"; - final String httpsPrefix = "https://"; - if (text.toLowerCase().startsWith(httpPrefix)) { - text = httpPrefix + text.substring(httpPrefix.length()); - } else if (text.toLowerCase().startsWith(httpsPrefix)) { - text = httpsPrefix + text.substring(httpsPrefix.length()); + return createForUrl(context, text); + case TextClassifier.TYPE_DATE: + case TextClassifier.TYPE_DATE_TIME: + if (classification.mDatetime != null) { + Calendar eventTime = Calendar.getInstance(); + eventTime.setTimeInMillis(classification.mDatetime.mMsSinceEpoch); + return createForDatetime(type, referenceTime, eventTime); } else { - text = httpPrefix + text; + return new ArrayList<>(); } - intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text)) - .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())); - break; + case TextClassifier.TYPE_FLIGHT_NUMBER: + return createForFlight(text); + default: + return new ArrayList<>(); + } + } + + @NonNull + private static List<Intent> createForEmail(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)); + } + + @NonNull + private static List<Intent> createForPhone(String text) { + return Arrays.asList( + new Intent(Intent.ACTION_DIAL) + .setData(Uri.parse(String.format("tel:%s", text))), + new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, text), + new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("smsto:%s", text)))); + } + + @NonNull + private static List<Intent> createForAddress(String text) { + return Arrays.asList(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); + } + + @NonNull + private static List<Intent> createForUrl(Context context, String text) { + final String httpPrefix = "http://"; + final String httpsPrefix = "https://"; + if (text.toLowerCase().startsWith(httpPrefix)) { + text = httpPrefix + text.substring(httpPrefix.length()); + } else if (text.toLowerCase().startsWith(httpsPrefix)) { + text = httpsPrefix + text.substring(httpsPrefix.length()); + } else { + text = httpPrefix + text; + } + return Arrays.asList(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) { + if (referenceTime == null) { + // If no reference time was given, use now. + referenceTime = Calendar.getInstance(); + } + List<Intent> intents = new ArrayList<>(); + intents.add(createCalendarViewIntent(eventTime)); + final long millisSinceReference = + eventTime.getTimeInMillis() - referenceTime.getTimeInMillis(); + if (millisSinceReference > MIN_EVENT_FUTURE_MILLIS) { + intents.add(createCalendarCreateEventIntent(eventTime, type)); } return intents; } + @NonNull + private static List<Intent> createForFlight(String text) { + return Arrays.asList(new Intent(Intent.ACTION_WEB_SEARCH) + .putExtra(SearchManager.QUERY, text)); + } + + @NonNull + private static Intent createCalendarViewIntent(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()); + } + + @NonNull + private static Intent createCalendarCreateEventIntent( + 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); @@ -578,6 +649,11 @@ final class TextClassifierImpl implements TextClassifier { default: 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: switch (intent.getDataString()) { case ContactsContract.Contacts.CONTENT_ITEM_TYPE: @@ -586,6 +662,9 @@ final class TextClassifierImpl implements TextClassifier { return null; } case Intent.ACTION_VIEW: + if (CalendarContract.AUTHORITY.equals(authority)) { + return context.getString(com.android.internal.R.string.view_calendar); + } switch (intent.getScheme()) { case "geo": return context.getString(com.android.internal.R.string.map); @@ -595,6 +674,8 @@ final class TextClassifierImpl implements TextClassifier { default: return null; } + case Intent.ACTION_WEB_SEARCH: + return context.getString(com.android.internal.R.string.view_flight); default: return null; } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 012212f32cdb..69d96fcaf9dd 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2731,6 +2731,15 @@ <!-- Label for item in the text selection menu to trigger adding a contact [CHAR LIMIT=20] --> <string name="add_contact">Add</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> + + <!-- 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> + + <!-- Label for item in the text selection menu to track a selected flight number [CHAR LIMIT=20] --> + <string name="view_flight">Track</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 f94168d97030..710bbfbb1b14 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -533,6 +533,9 @@ <java-symbol type="string" name="browse" /> <java-symbol type="string" name="sms" /> <java-symbol type="string" name="add_contact" /> + <java-symbol type="string" name="view_calendar" /> + <java-symbol type="string" name="add_calendar_event" /> + <java-symbol type="string" name="view_flight" /> <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/TextClassificationTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java index 9ee7facce47f..8a81743c8154 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java @@ -32,7 +32,9 @@ import android.view.View; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Calendar; import java.util.Locale; +import java.util.TimeZone; @SmallTest @RunWith(AndroidJUnit4.class) @@ -146,8 +148,12 @@ public class TextClassificationTest { @Test public void testParcelOptions() { + Calendar referenceTime = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US); + referenceTime.setTimeInMillis(946771200000L); // 2000-01-02 + TextClassification.Options reference = new TextClassification.Options(); reference.setDefaultLocales(new LocaleList(Locale.US, Locale.GERMANY)); + reference.setReferenceTime(referenceTime); // Parcel and unparcel. final Parcel parcel = Parcel.obtain(); @@ -157,5 +163,6 @@ public class TextClassificationTest { parcel); assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags()); + assertEquals(referenceTime, result.getReferenceTime()); } } |