Merge "Adding intent generation for dates and flights"
diff --git a/api/current.txt b/api/current.txt
index 26d02bd..b7016a2 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -49896,7 +49896,9 @@
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 @@
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 2c93a19..8edf97e 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 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 7089677..54e93d5 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -36,6 +36,7 @@
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 static final class Options implements Parcelable {
private @Nullable LocaleList mDefaultLocales;
+ private @Nullable Calendar mReferenceTime;
public Options() {}
@@ -606,6 +608,16 @@
}
/**
+ * @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 @@
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 @@
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 @@
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 e9715c5..04ab447 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -47,12 +47,26 @@
/** @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 @@
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 7db0e76..f434452 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -18,7 +18,9 @@
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.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.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.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 @@
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 @@
.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 @@
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 @@
}
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 @@
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 @@
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 @@
/**
* 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)
+ return createForEmail(text);
+ case TextClassifier.TYPE_PHONE:
+ return createForPhone(text);
+ case TextClassifier.TYPE_ADDRESS:
+ return createForAddress(text);
+ case TextClassifier.TYPE_URL:
+ 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 {
+ return new ArrayList<>();
+ }
+ 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));
- break;
- 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)
+ }
+
+ @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));
- intents.add(new Intent(Intent.ACTION_SENDTO)
+ .putExtra(ContactsContract.Intents.Insert.PHONE, text),
+ new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("smsto:%s", text))));
- break;
- case TextClassifier.TYPE_ADDRESS:
- intents.add(new Intent(Intent.ACTION_VIEW)
- .setData(Uri.parse(String.format("geo:0,0?q=%s", text))));
- break;
- 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());
- } else {
- text = httpPrefix + text;
- }
- intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text))
- .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()));
- break;
+ }
+
+ @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 @@
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 @@
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 @@
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 012212f3..69d96fc 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 f94168d..710bbfb 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 9ee7fac..8a81743 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 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 @@
@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 @@
parcel);
assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags());
+ assertEquals(referenceTime, result.getReferenceTime());
}
}