Merge changes from topic "reference_time_tc"

* changes:
  Populate person and reference time, uses more than the last message in NAS
  Pass reference time / locales of messages to the model
diff --git a/api/current.txt b/api/current.txt
index 6ecb914..a769e7f 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -52563,19 +52563,19 @@
     method public int describeContents();
     method public android.app.Person getAuthor();
     method public android.os.Bundle getExtras();
+    method public java.time.ZonedDateTime getReferenceTime();
     method public java.lang.CharSequence getText();
-    method public java.time.ZonedDateTime getTime();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.view.textclassifier.ConversationActions.Message> CREATOR;
     field public static final android.app.Person PERSON_USER_LOCAL;
+    field public static final android.app.Person PERSON_USER_REMOTE;
   }
 
   public static final class ConversationActions.Message.Builder {
-    ctor public ConversationActions.Message.Builder();
+    ctor public ConversationActions.Message.Builder(android.app.Person);
     method public android.view.textclassifier.ConversationActions.Message build();
-    method public android.view.textclassifier.ConversationActions.Message.Builder setAuthor(android.app.Person);
-    method public android.view.textclassifier.ConversationActions.Message.Builder setComposeTime(java.time.ZonedDateTime);
     method public android.view.textclassifier.ConversationActions.Message.Builder setExtras(android.os.Bundle);
+    method public android.view.textclassifier.ConversationActions.Message.Builder setReferenceTime(java.time.ZonedDateTime);
     method public android.view.textclassifier.ConversationActions.Message.Builder setText(java.lang.CharSequence);
   }
 
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)
diff --git a/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java b/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
index f0faaf6..4a6c093 100644
--- a/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
@@ -16,6 +16,9 @@
 
 package android.view.textclassifier;
 
+import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_LOCAL;
+import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_REMOTE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Person;
@@ -27,16 +30,26 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Locale;
+import java.util.function.Function;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ActionsSuggestionsHelperTest {
+    private static final String LOCALE_TAG = Locale.US.toLanguageTag();
+    private static final Function<CharSequence, String> LANGUAGE_DETECTOR =
+            charSequence -> LOCALE_TAG;
+
     @Test
     public void testToNativeMessages_emptyInput() {
         ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
-                ActionsSuggestionsHelper.toNativeMessages(Collections.emptyList());
+                ActionsSuggestionsHelper.toNativeMessages(
+                        Collections.emptyList(), LANGUAGE_DETECTOR);
 
         assertThat(conversationMessages).isEmpty();
     }
@@ -44,114 +57,89 @@
     @Test
     public void testToNativeMessages_noTextMessages() {
         ConversationActions.Message messageWithoutText =
-                new ConversationActions.Message.Builder().build();
+                new ConversationActions.Message.Builder(PERSON_USER_REMOTE).build();
 
         ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
                 ActionsSuggestionsHelper.toNativeMessages(
-                        Collections.singletonList(messageWithoutText));
+                        Collections.singletonList(messageWithoutText), LANGUAGE_DETECTOR);
 
         assertThat(conversationMessages).isEmpty();
     }
 
     @Test
-    public void testToNativeMessages_missingPersonInFirstMessage() {
-        ConversationActions.Message firstMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("first")
-                        .build();
-        ConversationActions.Message secondMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("second")
-                        .setAuthor(new Person.Builder().build())
-                        .build();
-        ConversationActions.Message thirdMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("third")
-                        .setAuthor(ConversationActions.Message.PERSON_USER_LOCAL)
-                        .build();
-
-        ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
-                ActionsSuggestionsHelper.toNativeMessages(
-                        Arrays.asList(firstMessage, secondMessage, thirdMessage));
-
-        assertThat(conversationMessages).hasLength(2);
-        assertNativeMessage(conversationMessages[0], secondMessage.getText(), 1);
-        assertNativeMessage(conversationMessages[1], thirdMessage.getText(), 0);
-    }
-
-    @Test
-    public void testToNativeMessages_missingPersonInMiddleOfConversation() {
-        ConversationActions.Message firstMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("first")
-                        .setAuthor(new Person.Builder().setName("first").build())
-                        .build();
-        ConversationActions.Message secondMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("second")
-                        .build();
-        ConversationActions.Message thirdMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("third")
-                        .setAuthor(new Person.Builder().setName("third").build())
-                        .build();
-        ConversationActions.Message fourthMessage =
-                new ConversationActions.Message.Builder()
-                        .setText("fourth")
-                        .setAuthor(new Person.Builder().setName("fourth").build())
-                        .build();
-
-        ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
-                ActionsSuggestionsHelper.toNativeMessages(
-                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage));
-
-        assertThat(conversationMessages).hasLength(2);
-        assertNativeMessage(conversationMessages[0], thirdMessage.getText(), 2);
-        assertNativeMessage(conversationMessages[1], fourthMessage.getText(), 1);
-    }
-
-    @Test
     public void testToNativeMessages_userIdEncoding() {
         Person userA = new Person.Builder().setName("userA").build();
         Person userB = new Person.Builder().setName("userB").build();
 
         ConversationActions.Message firstMessage =
-                new ConversationActions.Message.Builder()
+                new ConversationActions.Message.Builder(userB)
                         .setText("first")
-                        .setAuthor(userB)
                         .build();
         ConversationActions.Message secondMessage =
-                new ConversationActions.Message.Builder()
+                new ConversationActions.Message.Builder(userA)
                         .setText("second")
-                        .setAuthor(userA)
                         .build();
         ConversationActions.Message thirdMessage =
-                new ConversationActions.Message.Builder()
+                new ConversationActions.Message.Builder(PERSON_USER_LOCAL)
                         .setText("third")
-                        .setAuthor(ConversationActions.Message.PERSON_USER_LOCAL)
                         .build();
         ConversationActions.Message fourthMessage =
-                new ConversationActions.Message.Builder()
+                new ConversationActions.Message.Builder(userA)
                         .setText("fourth")
-                        .setAuthor(userA)
                         .build();
 
         ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
                 ActionsSuggestionsHelper.toNativeMessages(
-                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage));
+                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage),
+                        LANGUAGE_DETECTOR);
 
         assertThat(conversationMessages).hasLength(4);
-        assertNativeMessage(conversationMessages[0], firstMessage.getText(), 2);
-        assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1);
-        assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 0);
-        assertNativeMessage(conversationMessages[3], fourthMessage.getText(), 1);
+        assertNativeMessage(conversationMessages[0], firstMessage.getText(), 2, 0);
+        assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
+        assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 0, 0);
+        assertNativeMessage(conversationMessages[3], fourthMessage.getText(), 1, 0);
+    }
+
+    @Test
+    public void testToNativeMessages_referenceTime() {
+        ConversationActions.Message firstMessage =
+                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
+                        .setText("first")
+                        .setReferenceTime(createZonedDateTimeFromMsUtc(1000))
+                        .build();
+        ConversationActions.Message secondMessage =
+                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
+                        .setText("second")
+                        .build();
+        ConversationActions.Message thirdMessage =
+                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
+                        .setText("third")
+                        .setReferenceTime(createZonedDateTimeFromMsUtc(2000))
+                        .build();
+
+        ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
+                ActionsSuggestionsHelper.toNativeMessages(
+                        Arrays.asList(firstMessage, secondMessage, thirdMessage),
+                        LANGUAGE_DETECTOR);
+
+        assertThat(conversationMessages).hasLength(3);
+        assertNativeMessage(conversationMessages[0], firstMessage.getText(), 1, 1000);
+        assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
+        assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 1, 2000);
+    }
+
+    private ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
+        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneId.of("UTC"));
     }
 
     private static void assertNativeMessage(
             ActionsSuggestionsModel.ConversationMessage nativeMessage,
             CharSequence text,
-            int userId) {
+            int userId,
+            long referenceTimeInMsUtc) {
         assertThat(nativeMessage.getText()).isEqualTo(text.toString());
         assertThat(nativeMessage.getUserId()).isEqualTo(userId);
+        assertThat(nativeMessage.getLocales()).isEqualTo(LOCALE_TAG);
+        assertThat(nativeMessage.getReferenceTimeMsUtc()).isEqualTo(referenceTimeInMsUtc);
     }
 }
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
index aec4571..9b5c034 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
@@ -373,7 +373,10 @@
     public void testSuggestConversationActions_textReplyOnly_maxThree() {
         if (isTextClassifierDisabled()) return;
         ConversationActions.Message message =
-                new ConversationActions.Message.Builder().setText("Where are you?").build();
+                new ConversationActions.Message.Builder(
+                        ConversationActions.Message.PERSON_USER_REMOTE)
+                        .setText("Hello")
+                        .build();
         ConversationActions.TypeConfig typeConfig =
                 new ConversationActions.TypeConfig.Builder().includeTypesFromTextClassifier(false)
                         .setIncludedTypes(
diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
index 6f2b6c9..38df9b0 100644
--- a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
+++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
+import android.app.Person;
 import android.app.RemoteAction;
 import android.content.Context;
 import android.os.Bundle;
@@ -31,8 +32,14 @@
 import android.view.textclassifier.TextClassifier;
 import android.view.textclassifier.TextLinks;
 
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Deque;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -50,6 +57,8 @@
     private static final int MAX_ACTIONS_PER_LINK = 1;
     private static final int MAX_SMART_ACTIONS = 3;
     private static final int MAX_SUGGESTED_REPLIES = 3;
+    // TODO: Make this configurable.
+    private static final int MAX_MESSAGES_TO_EXTRACT = 5;
 
     private static final ConversationActions.TypeConfig TYPE_CONFIG =
             new ConversationActions.TypeConfig.Builder().setIncludedTypes(
@@ -64,9 +73,6 @@
 
     /**
      * Adds action adjustments based on the notification contents.
-     *
-     * TODO: Once we have a API in {@link TextClassificationManager} to predict smart actions
-     * from notification text / message, we can replace most of the code here by consuming that API.
      */
     @NonNull
     ArrayList<Notification.Action> suggestActions(@Nullable Context context,
@@ -84,9 +90,13 @@
         if (tcm == null) {
             return EMPTY_ACTION_LIST;
         }
+        List<ConversationActions.Message> messages = extractMessages(entry.getNotification());
+        if (messages.isEmpty()) {
+            return EMPTY_ACTION_LIST;
+        }
+        // TODO: Move to TextClassifier.suggestConversationActions once it is ready.
         return suggestActionsFromText(
-                tcm,
-                getMostSalientActionText(entry.getNotification()), MAX_SMART_ACTIONS);
+                tcm, messages.get(messages.size() - 1).getText(), MAX_SMART_ACTIONS);
     }
 
     ArrayList<CharSequence> suggestReplies(@Nullable Context context,
@@ -104,14 +114,12 @@
         if (tcm == null) {
             return EMPTY_REPLY_LIST;
         }
-        CharSequence text = getMostSalientActionText(entry.getNotification());
-        ConversationActions.Message message =
-                new ConversationActions.Message.Builder()
-                        .setText(text)
-                        .build();
-
+        List<ConversationActions.Message> messages = extractMessages(entry.getNotification());
+        if (messages.isEmpty()) {
+            return EMPTY_REPLY_LIST;
+        }
         ConversationActions.Request request =
-                new ConversationActions.Request.Builder(Collections.singletonList(message))
+                new ConversationActions.Request.Builder(messages)
                         .setMaxSuggestions(MAX_SUGGESTED_REPLIES)
                         .setHints(HINTS)
                         .setTypeConfig(TYPE_CONFIG)
@@ -140,10 +148,6 @@
         if (!Process.myUserHandle().equals(entry.getSbn().getUser())) {
             return false;
         }
-        if (notification.actions != null
-                && notification.actions.length >= Notification.MAX_ACTION_BUTTONS) {
-            return false;
-        }
         if ((notification.flags & FLAG_MASK_INELGIBILE_FOR_ACTIONS) != 0) {
             return false;
         }
@@ -176,21 +180,41 @@
 
     /** Returns the text most salient for action extraction in a notification. */
     @Nullable
-    private CharSequence getMostSalientActionText(@NonNull Notification notification) {
-        /* If it's messaging style, use the most recent message. */
-        // TODO: Use the last few X messages instead and take the Person object into consideration.
+    private List<ConversationActions.Message> extractMessages(@NonNull Notification notification) {
         Parcelable[] messages = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
-        if (messages != null && messages.length != 0) {
-            Bundle lastMessage = (Bundle) messages[messages.length - 1];
-            CharSequence lastMessageText =
-                    lastMessage.getCharSequence(Notification.MessagingStyle.Message.KEY_TEXT);
-            if (!TextUtils.isEmpty(lastMessageText)) {
-                return lastMessageText;
+        if (messages == null || messages.length == 0) {
+            return Arrays.asList(new ConversationActions.Message.Builder(
+                    ConversationActions.Message.PERSON_USER_REMOTE)
+                    .setText(notification.extras.getCharSequence(Notification.EXTRA_TEXT))
+                    .build());
+        }
+        Person localUser = notification.extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON);
+        Deque<ConversationActions.Message> extractMessages = new ArrayDeque<>();
+        for (int i = messages.length - 1; i >= 0; i--) {
+            Notification.MessagingStyle.Message message =
+                    Notification.MessagingStyle.Message.getMessageFromBundle((Bundle) messages[i]);
+            if (message == null) {
+                continue;
+            }
+            Person senderPerson = message.getSenderPerson();
+            // Skip encoding once the sender is missing as it is important to distinguish
+            // local user and remote user when generating replies.
+            if (senderPerson == null) {
+                break;
+            }
+            Person author = localUser != null && localUser.equals(senderPerson)
+                    ? ConversationActions.Message.PERSON_USER_LOCAL : senderPerson;
+            extractMessages.push(new ConversationActions.Message.Builder(author)
+                    .setText(message.getText())
+                    .setReferenceTime(
+                            ZonedDateTime.ofInstant(Instant.ofEpochMilli(message.getTimestamp()),
+                                    ZoneOffset.systemDefault()))
+                    .build());
+            if (extractMessages.size() >= MAX_MESSAGES_TO_EXTRACT) {
+                break;
             }
         }
-
-        // Fall back to using the normal text.
-        return notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+        return new ArrayList<>(extractMessages);
     }
 
     /** Returns a list of actions to act on entities in a given piece of text. */
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java
new file mode 100644
index 0000000..60d31fc
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java
@@ -0,0 +1,236 @@
+/**
+ * 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 android.ext.services.notification;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.Person;
+import android.content.Context;
+import android.os.Process;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.textclassifier.ConversationActions;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.Subject;
+import com.google.common.truth.SubjectFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+@RunWith(AndroidJUnit4.class)
+public class SmartActionHelperTest {
+
+    private SmartActionsHelper mSmartActionsHelper = new SmartActionsHelper();
+    private Context mContext;
+    @Mock private TextClassifier mTextClassifier;
+    @Mock private NotificationEntry mNotificationEntry;
+    @Mock private StatusBarNotification mStatusBarNotification;
+    private Notification.Builder mNotificationBuilder;
+    private AssistantSettings mSettings;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getTargetContext();
+
+        mContext.getSystemService(TextClassificationManager.class)
+                .setTextClassifier(mTextClassifier);
+        when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
+                .thenReturn(new ConversationActions(Collections.emptyList()));
+
+        when(mNotificationEntry.getSbn()).thenReturn(mStatusBarNotification);
+        // The notification is eligible to have smart suggestions.
+        when(mNotificationEntry.hasInlineReply()).thenReturn(true);
+        when(mNotificationEntry.isMessaging()).thenReturn(true);
+        when(mStatusBarNotification.getPackageName()).thenReturn("random.app");
+        when(mStatusBarNotification.getUser()).thenReturn(Process.myUserHandle());
+        mNotificationBuilder = new Notification.Builder(mContext, "channel");
+        mSettings = AssistantSettings.createForTesting(
+                null, null, Process.myUserHandle().getIdentifier(), null);
+        mSettings.mGenerateActions = true;
+        mSettings.mGenerateReplies = true;
+    }
+
+    @Test
+    public void testSuggestReplies_notMessagingApp() {
+        when(mNotificationEntry.isMessaging()).thenReturn(false);
+        ArrayList<CharSequence> textReplies =
+                mSmartActionsHelper.suggestReplies(mContext, mNotificationEntry, mSettings);
+        assertThat(textReplies).isEmpty();
+    }
+
+    @Test
+    public void testSuggestReplies_noInlineReply() {
+        when(mNotificationEntry.hasInlineReply()).thenReturn(false);
+        ArrayList<CharSequence> textReplies =
+                mSmartActionsHelper.suggestReplies(mContext, mNotificationEntry, mSettings);
+        assertThat(textReplies).isEmpty();
+    }
+
+    @Test
+    public void testSuggestReplies_nonMessageStyle() {
+        Notification notification = mNotificationBuilder.setContentText("Where are you?").build();
+        when(mNotificationEntry.getNotification()).thenReturn(notification);
+
+        List<ConversationActions.Message> messages = getMessagesInRequest();
+        assertThat(messages).hasSize(1);
+        MessageSubject.assertThat(messages.get(0)).hasText("Where are you?");
+    }
+
+    @Test
+    public void testSuggestReplies_messageStyle() {
+        Person me = new Person.Builder().setName("Me").build();
+        Person userA = new Person.Builder().setName("A").build();
+        Person userB = new Person.Builder().setName("B").build();
+        Notification.MessagingStyle style =
+                new Notification.MessagingStyle(me)
+                        .addMessage("firstMessage", 1000, (Person) null)
+                        .addMessage("secondMessage", 2000, me)
+                        .addMessage("thirdMessage", 3000, userA)
+                        .addMessage("fourthMessage", 4000, userB);
+        Notification notification =
+                mNotificationBuilder
+                        .setContentText("You have three new messages")
+                        .setStyle(style)
+                        .build();
+        when(mNotificationEntry.getNotification()).thenReturn(notification);
+
+        List<ConversationActions.Message> messages = getMessagesInRequest();
+        assertThat(messages).hasSize(3);
+
+        ConversationActions.Message secondMessage = messages.get(0);
+        MessageSubject.assertThat(secondMessage).hasText("secondMessage");
+        MessageSubject.assertThat(secondMessage)
+                .hasPerson(ConversationActions.Message.PERSON_USER_LOCAL);
+        MessageSubject.assertThat(secondMessage)
+                .hasReferenceTime(createZonedDateTimeFromMsUtc(2000));
+
+        ConversationActions.Message thirdMessage = messages.get(1);
+        MessageSubject.assertThat(thirdMessage).hasText("thirdMessage");
+        MessageSubject.assertThat(thirdMessage).hasPerson(userA);
+        MessageSubject.assertThat(thirdMessage)
+                .hasReferenceTime(createZonedDateTimeFromMsUtc(3000));
+
+        ConversationActions.Message fourthMessage = messages.get(2);
+        MessageSubject.assertThat(fourthMessage).hasText("fourthMessage");
+        MessageSubject.assertThat(fourthMessage).hasPerson(userB);
+        MessageSubject.assertThat(fourthMessage)
+                .hasReferenceTime(createZonedDateTimeFromMsUtc(4000));
+    }
+
+    @Test
+    public void testSuggestReplies_messageStyle_noPerson() {
+        Person me = new Person.Builder().setName("Me").build();
+        Notification.MessagingStyle style =
+                new Notification.MessagingStyle(me).addMessage("message", 1000, (Person) null);
+        Notification notification =
+                mNotificationBuilder
+                        .setContentText("You have one new message")
+                        .setStyle(style)
+                        .build();
+        when(mNotificationEntry.getNotification()).thenReturn(notification);
+
+        mSmartActionsHelper.suggestReplies(mContext, mNotificationEntry, mSettings);
+
+        verify(mTextClassifier, never())
+                .suggestConversationActions(any(ConversationActions.Request.class));
+    }
+
+    private ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
+        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneOffset.systemDefault());
+    }
+
+    private List<ConversationActions.Message> getMessagesInRequest() {
+        mSmartActionsHelper.suggestReplies(mContext, mNotificationEntry, mSettings);
+
+        ArgumentCaptor<ConversationActions.Request> argumentCaptor =
+                ArgumentCaptor.forClass(ConversationActions.Request.class);
+        verify(mTextClassifier).suggestConversationActions(argumentCaptor.capture());
+        ConversationActions.Request request = argumentCaptor.getValue();
+        return request.getConversation();
+    }
+
+    private static final class MessageSubject
+            extends Subject<MessageSubject, ConversationActions.Message> {
+
+        private static final SubjectFactory<MessageSubject, ConversationActions.Message> FACTORY =
+                new SubjectFactory<MessageSubject, ConversationActions.Message>() {
+                    @Override
+                    public MessageSubject getSubject(
+                            @NonNull FailureStrategy failureStrategy,
+                            @NonNull ConversationActions.Message subject) {
+                        return new MessageSubject(failureStrategy, subject);
+                    }
+                };
+
+        private MessageSubject(
+                FailureStrategy failureStrategy, @Nullable ConversationActions.Message subject) {
+            super(failureStrategy, subject);
+        }
+
+        private void hasText(String text) {
+            if (!Objects.equals(text, getSubject().getText().toString())) {
+                failWithBadResults("has text", text, "has", getSubject().getText());
+            }
+        }
+
+        private void hasPerson(Person person) {
+            if (!Objects.equals(person, getSubject().getAuthor())) {
+                failWithBadResults("has author", person, "has", getSubject().getAuthor());
+            }
+        }
+
+        private void hasReferenceTime(ZonedDateTime referenceTime) {
+            if (!Objects.equals(referenceTime, getSubject().getReferenceTime())) {
+                failWithBadResults(
+                        "has reference time",
+                        referenceTime,
+                        "has",
+                        getSubject().getReferenceTime());
+            }
+        }
+
+        private static MessageSubject assertThat(ConversationActions.Message message) {
+            return assertAbout(FACTORY).that(message);
+        }
+    }
+}