Introduce Message.USER_LOCAL and added logic to map person to user id
1. Introduce Message.USER_LOCAL to allow caller to specify which
message is from the local user.
2. TextClassifierImpl will now encode the Person object to a user
id.
3. Fixed a bug in Person.equals check
BUG: 111437455
BUG: 111406942
Test: atest frameworks/base/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
Change-Id: I6629f42244a402fa210f87afa88a629c2ca4a510
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index a2dae3b..0abc998 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -127,8 +127,8 @@
if (obj instanceof Person) {
final Person other = (Person) obj;
return Objects.equals(mName, other.mName)
- && mIcon == null ? other.mIcon == null :
- (other.mIcon != null && mIcon.sameAs(other.mIcon))
+ && (mIcon == null ? other.mIcon == null :
+ (other.mIcon != null && mIcon.sameAs(other.mIcon)))
&& Objects.equals(mUri, other.mUri)
&& Objects.equals(mKey, other.mKey)
&& mIsBot == other.mIsBot
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
new file mode 100644
index 0000000..8df83c0
--- /dev/null
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -0,0 +1,115 @@
+/*
+ * 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.view.textclassifier;
+
+import android.annotation.NonNull;
+import android.app.Person;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.android.textclassifier.ActionsSuggestionsModel;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class for action suggestions.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class ActionsSuggestionsHelper {
+ private static final int USER_LOCAL = 0;
+ private static final int FIRST_NON_LOCAL_USER = 1;
+
+ private ActionsSuggestionsHelper() {}
+
+ /**
+ * Converts the messages to a list of native messages object that the model can understand.
+ * <p>
+ * User id encoding - local user is represented as 0, Other users are numbered according to
+ * how far before they spoke last time in the conversation. For example, considering this
+ * conversation:
+ * <ul>
+ * <li> User A: xxx
+ * <li> Local user: yyy
+ * <li> User B: zzz
+ * </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> messagesWithText =
+ messages.stream()
+ .filter(message -> !TextUtils.isEmpty(message.getText()))
+ .collect(Collectors.toCollection(ArrayList::new));
+ 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())};
+ }
+
+ // Encode the messages in the reverse order, stop whenever the Person object is missing.
+ Deque<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayDeque<>();
+ PersonEncoder personEncoder = new PersonEncoder();
+ for (int i = size - 1; i >= 0; i--) {
+ ConversationActions.Message message = messagesWithText.get(i);
+ if (message.getAuthor() == null) {
+ break;
+ }
+ nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
+ personEncoder.encode(message.getAuthor()),
+ message.getText().toString()));
+ }
+ return nativeMessages.toArray(
+ new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
+ }
+
+ private static final class PersonEncoder {
+ private final Map<Person, Integer> mMapping = new ArrayMap<>();
+ private int mNextUserId = FIRST_NON_LOCAL_USER;
+
+ private int encode(Person person) {
+ if (ConversationActions.Message.PERSON_USER_LOCAL.equals(person)) {
+ return USER_LOCAL;
+ }
+ Integer result = mMapping.get(person);
+ if (result == null) {
+ mMapping.put(person, mNextUserId);
+ result = mNextUserId;
+ mNextUserId++;
+ }
+ return result;
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/ConversationActions.java b/core/java/android/view/textclassifier/ConversationActions.java
index 5fcf227..1a7b911 100644
--- a/core/java/android/view/textclassifier/ConversationActions.java
+++ b/core/java/android/view/textclassifier/ConversationActions.java
@@ -345,6 +345,16 @@
/** Represents a message in the conversation. */
public static final class Message implements Parcelable {
+ /**
+ * Represents the local user.
+ *
+ * @see Builder#setAuthor(Person)
+ */
+ public static final Person PERSON_USER_LOCAL =
+ new Person.Builder()
+ .setKey("text-classifier-conversation-actions-local-user")
+ .build();
+
@Nullable
private final Person mAuthor;
@Nullable
@@ -446,7 +456,11 @@
@Nullable
private Bundle mExtras;
- /** Sets the person who composed this message. */
+ /**
+ * Sets the person who composed this message.
+ * <p>
+ * Use {@link #PERSON_USER_LOCAL} to represent the local user.
+ */
@NonNull
public Builder setAuthor(@Nullable Person author) {
mAuthor = author;
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 798a820..66da45d 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -40,7 +40,6 @@
import android.provider.Browser;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
-import android.text.TextUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
@@ -269,17 +268,17 @@
final ZonedDateTime refTime = ZonedDateTime.now();
final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
? request.getEntityConfig().resolveEntityListModifications(
- getEntitiesForHints(request.getEntityConfig().getHints()))
+ getEntitiesForHints(request.getEntityConfig().getHints()))
: mSettings.getEntityListDefault();
final AnnotatorModel annotatorImpl =
getAnnotatorImpl(request.getDefaultLocales());
final AnnotatorModel.AnnotatedSpan[] annotations =
annotatorImpl.annotate(
- textString,
- new AnnotatorModel.AnnotationOptions(
- refTime.toInstant().toEpochMilli(),
- refTime.getZone().getId(),
- concatenateLocales(request.getDefaultLocales())));
+ textString,
+ new AnnotatorModel.AnnotationOptions(
+ refTime.toInstant().toEpochMilli(),
+ refTime.getZone().getId(),
+ concatenateLocales(request.getDefaultLocales())));
for (AnnotatorModel.AnnotatedSpan span : annotations) {
final AnnotatorModel.ClassificationResult[] results =
span.getClassification();
@@ -373,20 +372,13 @@
// Actions model is optional, fallback if it is not available.
return mFallback.suggestConversationActions(request);
}
- List<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayList<>();
- for (ConversationActions.Message message : request.getConversation()) {
- if (TextUtils.isEmpty(message.getText())) {
- continue;
- }
- // TODO: We need to map the Person object to user id.
- int userId = 1;
- nativeMessages.add(
- new ActionsSuggestionsModel.ConversationMessage(
- userId, message.getText().toString()));
+ ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
+ ActionsSuggestionsHelper.toNativeMessages(request.getConversation());
+ if (nativeMessages.length == 0) {
+ return mFallback.suggestConversationActions(request);
}
ActionsSuggestionsModel.Conversation nativeConversation =
- new ActionsSuggestionsModel.Conversation(nativeMessages.toArray(
- new ActionsSuggestionsModel.ConversationMessage[0]));
+ new ActionsSuggestionsModel.Conversation(nativeMessages);
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
actionsImpl.suggestActions(nativeConversation, null);
diff --git a/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java b/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
new file mode 100644
index 0000000..f0faaf6
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.view.textclassifier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Person;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.google.android.textclassifier.ActionsSuggestionsModel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionsSuggestionsHelperTest {
+ @Test
+ public void testToNativeMessages_emptyInput() {
+ ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
+ ActionsSuggestionsHelper.toNativeMessages(Collections.emptyList());
+
+ assertThat(conversationMessages).isEmpty();
+ }
+
+ @Test
+ public void testToNativeMessages_noTextMessages() {
+ ConversationActions.Message messageWithoutText =
+ new ConversationActions.Message.Builder().build();
+
+ ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
+ ActionsSuggestionsHelper.toNativeMessages(
+ Collections.singletonList(messageWithoutText));
+
+ 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()
+ .setText("first")
+ .setAuthor(userB)
+ .build();
+ ConversationActions.Message secondMessage =
+ new ConversationActions.Message.Builder()
+ .setText("second")
+ .setAuthor(userA)
+ .build();
+ ConversationActions.Message thirdMessage =
+ new ConversationActions.Message.Builder()
+ .setText("third")
+ .setAuthor(ConversationActions.Message.PERSON_USER_LOCAL)
+ .build();
+ ConversationActions.Message fourthMessage =
+ new ConversationActions.Message.Builder()
+ .setText("fourth")
+ .setAuthor(userA)
+ .build();
+
+ ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
+ ActionsSuggestionsHelper.toNativeMessages(
+ Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage));
+
+ 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);
+ }
+
+ private static void assertNativeMessage(
+ ActionsSuggestionsModel.ConversationMessage nativeMessage,
+ CharSequence text,
+ int userId) {
+ assertThat(nativeMessage.getText()).isEqualTo(text.toString());
+ assertThat(nativeMessage.getUserId()).isEqualTo(userId);
+ }
+}