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