Pass reference time / locales of messages to the model

1. It is required to set Person object when constructing a Message object
   now. As it is very important to know whether the message is from
   local user or remote user. Introduced PERSON_USER_REMOTE if
   the caller just want a simple way to specify a remote user.

2. Use detectLanguages to detect the locale of the messages
   If the model finds the detected language is not something
   it supports, model may suppress smart reply.

3. Pass the reference time to the model. So model can resolve
   the absolute time from a relative date string like "tomorrow 6pm".

BUG: 120809869

Test: atest ActionsSuggestionsHelperTest.java
Test: atest ConversationActionsTest.java

Change-Id: Ie079848e9b3d9bb8800f7f95d73e289e831968f8
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
index 797b861..b41096c 100644
--- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -16,7 +16,6 @@
 
 package android.view.textclassifier;
 
-import android.annotation.NonNull;
 import android.app.Person;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -30,6 +29,7 @@
 import java.util.Deque;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -57,9 +57,9 @@
      * </ul>
      * User A will be encoded as 2, user B will be encoded as 1 and local user will be encoded as 0.
      */
-    @NonNull
     public static ActionsSuggestionsModel.ConversationMessage[] toNativeMessages(
-            @NonNull List<ConversationActions.Message> messages) {
+            List<ConversationActions.Message> messages,
+            Function<CharSequence, String> languageDetector) {
         List<ConversationActions.Message> messagesWithText =
                 messages.stream()
                         .filter(message -> !TextUtils.isEmpty(message.getText()))
@@ -67,31 +67,18 @@
         if (messagesWithText.isEmpty()) {
             return new ActionsSuggestionsModel.ConversationMessage[0];
         }
-        int size = messagesWithText.size();
-        // If the last message (the most important one) does not have the Person object, we will
-        // just use the last message and consider this message is sent from a remote user.
-        ConversationActions.Message lastMessage = messages.get(size - 1);
-        boolean useLastMessageOnly = lastMessage.getAuthor() == null;
-        if (useLastMessageOnly) {
-            return new ActionsSuggestionsModel.ConversationMessage[]{
-                    new ActionsSuggestionsModel.ConversationMessage(
-                            FIRST_NON_LOCAL_USER,
-                            lastMessage.getText().toString(),
-                            0,
-                            null)};
-        }
-
-        // Encode the messages in the reverse order, stop whenever the Person object is missing.
         Deque<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayDeque<>();
         PersonEncoder personEncoder = new PersonEncoder();
+        int size = messagesWithText.size();
         for (int i = size - 1; i >= 0; i--) {
             ConversationActions.Message message = messagesWithText.get(i);
-            if (message.getAuthor() == null) {
-                break;
-            }
+            long referenceTime = message.getReferenceTime() == null
+                    ? 0
+                    : message.getReferenceTime().toInstant().toEpochMilli();
             nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
                     personEncoder.encode(message.getAuthor()),
-                    message.getText().toString(), 0, null));
+                    message.getText().toString(), referenceTime,
+                    languageDetector.apply(message.getText())));
         }
         return nativeMessages.toArray(
                 new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
diff --git a/core/java/android/view/textclassifier/ConversationActions.java b/core/java/android/view/textclassifier/ConversationActions.java
index 04b94b0..04924c9 100644
--- a/core/java/android/view/textclassifier/ConversationActions.java
+++ b/core/java/android/view/textclassifier/ConversationActions.java
@@ -349,17 +349,31 @@
         /**
          * Represents the local user.
          *
-         * @see Builder#setAuthor(Person)
+         * @see Builder#Builder(Person)
          */
         public static final Person PERSON_USER_LOCAL =
                 new Person.Builder()
                         .setKey("text-classifier-conversation-actions-local-user")
                         .build();
 
+        /**
+         * Represents the remote user.
+         * <p>
+         * If possible, you are suggested to create a {@link Person} object that can identify
+         * the remote user better, so that the underlying model could differentiate between
+         * different remote users.
+         *
+         * @see Builder#Builder(Person)
+         */
+        public static final Person PERSON_USER_REMOTE =
+                new Person.Builder()
+                        .setKey("text-classifier-conversation-actions-remote-user")
+                        .build();
+
         @Nullable
         private final Person mAuthor;
         @Nullable
-        private final ZonedDateTime mComposeTime;
+        private final ZonedDateTime mReferenceTime;
         @Nullable
         private final CharSequence mText;
         @NonNull
@@ -367,18 +381,18 @@
 
         private Message(
                 @Nullable Person author,
-                @Nullable ZonedDateTime composeTime,
+                @Nullable ZonedDateTime referenceTime,
                 @Nullable CharSequence text,
                 @NonNull Bundle bundle) {
             mAuthor = author;
-            mComposeTime = composeTime;
+            mReferenceTime = referenceTime;
             mText = text;
             mExtras = Preconditions.checkNotNull(bundle);
         }
 
         private Message(Parcel in) {
             mAuthor = in.readParcelable(null);
-            mComposeTime =
+            mReferenceTime =
                     in.readInt() == 0
                             ? null
                             : ZonedDateTime.parse(
@@ -390,9 +404,9 @@
         @Override
         public void writeToParcel(Parcel parcel, int flags) {
             parcel.writeParcelable(mAuthor, flags);
-            parcel.writeInt(mComposeTime != null ? 1 : 0);
-            if (mComposeTime != null) {
-                parcel.writeString(mComposeTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+            parcel.writeInt(mReferenceTime != null ? 1 : 0);
+            if (mReferenceTime != null) {
+                parcel.writeString(mReferenceTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
             }
             parcel.writeCharSequence(mText);
             parcel.writeBundle(mExtras);
@@ -417,15 +431,18 @@
                 };
 
         /** Returns the person that composed the message. */
-        @Nullable
+        @NonNull
         public Person getAuthor() {
             return mAuthor;
         }
 
-        /** Returns the compose time of the message. */
+        /**
+         * Returns the reference time of the message, for example it could be the compose or send
+         * time of this message.
+         */
         @Nullable
-        public ZonedDateTime getTime() {
-            return mComposeTime;
+        public ZonedDateTime getReferenceTime() {
+            return mReferenceTime;
         }
 
         /** Returns the text of the message. */
@@ -451,34 +468,38 @@
             @Nullable
             private Person mAuthor;
             @Nullable
-            private ZonedDateTime mComposeTime;
+            private ZonedDateTime mReferenceTime;
             @Nullable
             private CharSequence mText;
             @Nullable
             private Bundle mExtras;
 
             /**
-             * Sets the person who composed this message.
-             * <p>
-             * Use {@link #PERSON_USER_LOCAL} to represent the local user.
+             * Constructs a builder.
+             *
+             * @param author the person that composed the message, use {@link #PERSON_USER_LOCAL}
+             *               to represent the local user. If it is not possible to identify the
+             *               remote user that the local user is conversing with, use
+             *               {@link #PERSON_USER_REMOTE} to represent a remote user.
              */
-            @NonNull
-            public Builder setAuthor(@Nullable Person author) {
-                mAuthor = author;
-                return this;
+            public Builder(@NonNull Person author) {
+                mAuthor = Preconditions.checkNotNull(author);
             }
 
-            /** Sets the text of this message */
+            /** Sets the text of this message. */
             @NonNull
             public Builder setText(@Nullable CharSequence text) {
                 mText = text;
                 return this;
             }
 
-            /** Sets the compose time of this message */
+            /**
+             * Sets the reference time of this message, for example it could be the compose or send
+             * time of this message.
+             */
             @NonNull
-            public Builder setComposeTime(@Nullable ZonedDateTime composeTime) {
-                mComposeTime = composeTime;
+            public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+                mReferenceTime = referenceTime;
                 return this;
             }
 
@@ -494,7 +515,7 @@
             public Message build() {
                 return new Message(
                         mAuthor,
-                        mComposeTime,
+                        mReferenceTime,
                         mText == null ? null : new SpannedString(mText),
                         mExtras == null ? new Bundle() : mExtras.deepCopy());
             }
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 8e14dfd..4e9ab50 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -374,7 +374,8 @@
                 return mFallback.suggestConversationActions(request);
             }
             ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
-                    ActionsSuggestionsHelper.toNativeMessages(request.getConversation());
+                    ActionsSuggestionsHelper.toNativeMessages(request.getConversation(),
+                            this::detectLanguageTagsFromText);
             if (nativeMessages.length == 0) {
                 return mFallback.suggestConversationActions(request);
             }
@@ -407,6 +408,26 @@
         return mFallback.suggestConversationActions(request);
     }
 
+    @Nullable
+    private String detectLanguageTagsFromText(CharSequence text) {
+        TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
+        TextLanguage textLanguage = detectLanguage(request);
+        int localeHypothesisCount = textLanguage.getLocaleHypothesisCount();
+        List<String> languageTags = new ArrayList<>();
+        // TODO: Reconsider this and probably make the score threshold configurable.
+        for (int i = 0; i < localeHypothesisCount; i++) {
+            ULocale locale = textLanguage.getLocale(i);
+            if (textLanguage.getConfidenceScore(locale) < 0.5) {
+                break;
+            }
+            languageTags.add(locale.toLanguageTag());
+        }
+        if (languageTags.isEmpty()) {
+            return LocaleList.getDefault().toLanguageTags();
+        }
+        return String.join(",", languageTags);
+    }
+
     private Collection<String> resolveActionTypesFromRequest(ConversationActions.Request request) {
         List<String> defaultActionTypes =
                 request.getHints().contains(ConversationActions.HINT_FOR_NOTIFICATION)