Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 82fa8d94 authored by Tony Mak's avatar Tony Mak
Browse files

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
parent af7deaba
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -52488,19 +52488,19 @@ package android.view.textclassifier {
    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);
  }
+9 −22
Original line number Diff line number Diff line
@@ -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.ArrayList;
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 @@ public final class ActionsSuggestionsHelper {
     * </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 @@ public final class ActionsSuggestionsHelper {
        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()]);
+46 −25
Original line number Diff line number Diff line
@@ -349,17 +349,31 @@ public final class ConversationActions implements Parcelable {
        /**
         * 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 @@ public final class ConversationActions implements Parcelable {

        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 @@ public final class ConversationActions implements Parcelable {
        @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 @@ public final class ConversationActions implements Parcelable {
                };

        /** 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 @@ public final class ConversationActions implements Parcelable {
            @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 final class ConversationActions implements Parcelable {
            public Message build() {
                return new Message(
                        mAuthor,
                        mComposeTime,
                        mReferenceTime,
                        mText == null ? null : new SpannedString(mText),
                        mExtras == null ? new Bundle() : mExtras.deepCopy());
            }
+22 −1
Original line number Diff line number Diff line
@@ -374,7 +374,8 @@ public final class TextClassifierImpl implements TextClassifier {
                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 @@ public final class TextClassifierImpl implements TextClassifier {
        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)
+50 −62
Original line number Diff line number Diff line
@@ -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 com.google.android.textclassifier.ActionsSuggestionsModel;
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 @@ public class ActionsSuggestionsHelperTest {
    @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);
    }
    public void testToNativeMessages_userIdEncoding() {
        Person userA = new Person.Builder().setName("userA").build();
        Person userB = new Person.Builder().setName("userB").build();

    @Test
    public void testToNativeMessages_missingPersonInMiddleOfConversation() {
        ConversationActions.Message firstMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(userB)
                        .setText("first")
                        .setAuthor(new Person.Builder().setName("first").build())
                        .build();
        ConversationActions.Message secondMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(userA)
                        .setText("second")
                        .build();
        ConversationActions.Message thirdMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(PERSON_USER_LOCAL)
                        .setText("third")
                        .setAuthor(new Person.Builder().setName("third").build())
                        .build();
        ConversationActions.Message fourthMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(userA)
                        .setText("fourth")
                        .setAuthor(new Person.Builder().setName("fourth").build())
                        .build();

        ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
                ActionsSuggestionsHelper.toNativeMessages(
                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage));
                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage),
                        LANGUAGE_DETECTOR);

        assertThat(conversationMessages).hasLength(2);
        assertNativeMessage(conversationMessages[0], thirdMessage.getText(), 2);
        assertNativeMessage(conversationMessages[1], fourthMessage.getText(), 1);
        assertThat(conversationMessages).hasLength(4);
        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_userIdEncoding() {
        Person userA = new Person.Builder().setName("userA").build();
        Person userB = new Person.Builder().setName("userB").build();

    public void testToNativeMessages_referenceTime() {
        ConversationActions.Message firstMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
                        .setText("first")
                        .setAuthor(userB)
                        .setReferenceTime(createZonedDateTimeFromMsUtc(1000))
                        .build();
        ConversationActions.Message secondMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
                        .setText("second")
                        .setAuthor(userA)
                        .build();
        ConversationActions.Message thirdMessage =
                new ConversationActions.Message.Builder()
                new ConversationActions.Message.Builder(PERSON_USER_REMOTE)
                        .setText("third")
                        .setAuthor(ConversationActions.Message.PERSON_USER_LOCAL)
                        .build();
        ConversationActions.Message fourthMessage =
                new ConversationActions.Message.Builder()
                        .setText("fourth")
                        .setAuthor(userA)
                        .setReferenceTime(createZonedDateTimeFromMsUtc(2000))
                        .build();

        ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
                ActionsSuggestionsHelper.toNativeMessages(
                        Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage));
                        Arrays.asList(firstMessage, secondMessage, thirdMessage),
                        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);
        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);
    }
}
Loading