diff --git a/api/current.txt b/api/current.txt index e84bc8db67da30e6bc6db9a68cc109b8756c4cd3..b719c46cd939bd507acc9c8c4df822306a91a13f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -51896,6 +51896,7 @@ package android.view.textclassifier { method public java.time.ZonedDateTime getTime(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final android.app.Person PERSON_USER_LOCAL; } public static final class ConversationActions.Message.Builder { diff --git a/api/system-current.txt b/api/system-current.txt index bf2c35715d73ce09fda19b1c0b8cc8f9b5edd622..7bd9ad581ba12c7f2b7c2067a4ed84b837e8bc83 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -441,10 +441,10 @@ package android.app { } public class KeyguardManager { - method public void setPrivateNotificationsAllowed(boolean); - method public boolean getPrivateNotificationsAllowed(); method public android.content.Intent createConfirmFactoryResetCredentialIntent(java.lang.CharSequence, java.lang.CharSequence, java.lang.CharSequence); + method public boolean getPrivateNotificationsAllowed(); method public void requestDismissKeyguard(android.app.Activity, java.lang.CharSequence, android.app.KeyguardManager.KeyguardDismissCallback); + method public void setPrivateNotificationsAllowed(boolean); } public class Notification implements android.os.Parcelable { diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java index a2dae3b9de039744459a98e8e8baa17e53ca87c4..0abc99821c1e591b47149b4ba4a37f907de6e2cc 100644 --- a/core/java/android/app/Person.java +++ b/core/java/android/app/Person.java @@ -127,8 +127,8 @@ public final class Person implements Parcelable { 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 0000000000000000000000000000000000000000..8df83c0e5dffa8a1c66158f781ee24c833f13421 --- /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. + *

+ * 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: + *

+ * 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 messages) { + List 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 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 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 5fcf22771ec1ca68e20adeee6a4e7ab9033b1a01..1a7b91127a7b22b0ec02b3d3c9b2460a225b02a6 100644 --- a/core/java/android/view/textclassifier/ConversationActions.java +++ b/core/java/android/view/textclassifier/ConversationActions.java @@ -345,6 +345,16 @@ public final class ConversationActions implements Parcelable { /** 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 @@ public final class ConversationActions implements Parcelable { @Nullable private Bundle mExtras; - /** Sets the person who composed this message. */ + /** + * Sets the person who composed this message. + *

+ * 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 798a8208e24002a9bdd13c74d6cf3059ff63c053..66da45dee3d9614e2cad78bbd7b20309667d46e3 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -40,7 +40,6 @@ import android.os.UserManager; 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 @@ public final class TextClassifierImpl implements TextClassifier { final ZonedDateTime refTime = ZonedDateTime.now(); final Collection 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 @@ public final class TextClassifierImpl implements TextClassifier { // Actions model is optional, fallback if it is not available. return mFallback.suggestConversationActions(request); } - List 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 0000000000000000000000000000000000000000..f0faaf6153b1a943165de1c2e3d84a649989e270 --- /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); + } +}