blob: 84cf4fbb479fd27010d54446246c362140710162 [file] [log] [blame]
/*
* 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 com.android.textclassifier.notification;
import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_SELF;
import static com.google.common.truth.Truth.assertThat;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteAction;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
import android.service.notification.NotificationAssistantService;
import android.service.notification.StatusBarNotification;
import android.view.textclassifier.ConversationAction;
import android.view.textclassifier.ConversationActions;
import android.view.textclassifier.ConversationActions.Message;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextClassifierEvent;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class SmartSuggestionsHelperTest {
private static final String PACKAGE_NAME = "random.app";
private static final String MESSAGE = "Where are you?";
private static final CharSequence SMART_REPLY = "Home";
private static final CharSequence ACTION_TITLE = "Open";
private static final String RESULT_ID = "id";
private static final float REPLY_SCORE = 0.7f;
private static final float ACTION_SCORE = 1.0f;
private final Context context = ApplicationProvider.getApplicationContext();
private final FakeTextClassifier fakeTextClassifier = new FakeTextClassifier();
private final TestConfig config = new TestConfig();
private TestableSmartSuggestionsHelper smartActions;
private Notification.Builder notificationBuilder;
@Before
public void setup() {
TextClassificationManager textClassificationManager =
context.getSystemService(TextClassificationManager.class);
textClassificationManager.setTextClassifier(fakeTextClassifier);
smartActions = new TestableSmartSuggestionsHelper(context, config);
notificationBuilder = new Notification.Builder(context, "id");
}
static class TestableSmartSuggestionsHelper extends SmartSuggestionsHelper {
private int numOfSessionsCreated = 0;
TestableSmartSuggestionsHelper(Context context, SmartSuggestionsConfig config) {
super(context, config);
}
@Override
TextClassifier createTextClassificationSession() {
numOfSessionsCreated += 1;
return super.createTextClassificationSession();
}
int getNumOfSessionsCreated() {
return numOfSessionsCreated;
}
}
@Test
public void onNotificationEnqueued_notMessageCategory() {
Notification notification = notificationBuilder.setContentText(MESSAGE).build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getReplies()).isEmpty();
assertThat(smartSuggestions.getActions()).isEmpty();
// Ideally, we should verify that createTextClassificationSession
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(0);
}
@Test
public void onNotificationEnqueued_fromSystem() {
Notification notification =
notificationBuilder
.setContentText(MESSAGE)
.setCategory(Notification.CATEGORY_MESSAGE)
.setActions(createReplyAction())
.build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, "android");
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getReplies()).isEmpty();
assertThat(smartSuggestions.getActions()).isEmpty();
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(0);
}
@Test
public void onNotificationEnqueued_noInlineReply() {
Notification notification =
notificationBuilder
.setContentText(MESSAGE)
.setCategory(Notification.CATEGORY_MESSAGE)
.build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getReplies()).isEmpty();
assertAdjustmentWithSmartAction(smartSuggestions);
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(1);
}
@Test
public void onNotificationEnqueued_messageCategoryNotification() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertAdjustmentWithSmartReply(smartSuggestions);
assertAdjustmentWithSmartAction(smartSuggestions);
ConversationActions.Request request = fakeTextClassifier.getLastRequest();
List<Message> messages = request.getConversation();
assertThat(messages).hasSize(1);
assertThat(messages.get(0).getText().toString()).isEqualTo(MESSAGE);
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(1);
}
@Test
public void onNotificationEnqueued_messageStyleNotification() {
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 =
notificationBuilder
.setContentText("You have three new messages")
.setStyle(style)
.setActions(createReplyAction())
.build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertAdjustmentWithSmartReply(smartSuggestions);
assertAdjustmentWithSmartAction(smartSuggestions);
ConversationActions.Request request = fakeTextClassifier.getLastRequest();
List<ConversationActions.Message> messages = request.getConversation();
assertThat(messages).hasSize(4);
assertMessage(messages.get(0), "firstMessage", PERSON_USER_SELF, 1000);
assertMessage(messages.get(1), "secondMessage", PERSON_USER_SELF, 2000);
assertMessage(messages.get(2), "thirdMessage", userA, 3000);
assertMessage(messages.get(3), "fourthMessage", userB, 4000);
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(1);
}
@Test
public void onNotificationEnqueued_lastMessageFromLocalUser() {
Person me = new Person.Builder().setName("Me").build();
Person userA = new Person.Builder().setName("A").build();
Notification.MessagingStyle style =
new Notification.MessagingStyle(me)
.addMessage("firstMessage", 1000, userA)
.addMessage("secondMessage", 2000, me);
Notification notification =
notificationBuilder
.setContentText("You have two new messages")
.setStyle(style)
.setActions(createReplyAction())
.build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getReplies()).isEmpty();
assertThat(smartSuggestions.getActions()).isEmpty();
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(0);
}
@Test
public void onNotificationEnqueued_messageStyleNotification_missingPerson() {
Person me = new Person.Builder().setName("Me").build();
Notification.MessagingStyle style =
new Notification.MessagingStyle(me).addMessage("message", 1000, (Person) null);
Notification notification =
notificationBuilder
.setContentText("You have one new message")
.setStyle(style)
.setActions(createReplyAction())
.build();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getReplies()).isEmpty();
assertThat(smartSuggestions.getActions()).isEmpty();
assertThat(smartActions.getNumOfSessionsCreated()).isEqualTo(0);
}
@Test
public void onSuggestedReplySent() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onSuggestedReplySent(
statusBarNotification.getKey(),
SMART_REPLY,
NotificationAssistantService.SOURCE_FROM_ASSISTANT);
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(2);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
assertThat(firstEvent.getEntityTypes())
.asList()
.containsExactly(ConversationAction.TYPE_TEXT_REPLY, ConversationAction.TYPE_OPEN_URL);
TextClassifierEvent secondEvent = textClassifierEvents.get(1);
assertThat(secondEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_SMART_ACTION);
assertThat(secondEvent.getEntityTypes()[0]).isEqualTo(ConversationAction.TYPE_TEXT_REPLY);
}
@Test
public void onSuggestedReplySent_noMatchingSession() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onSuggestedReplySent(
"something_else", MESSAGE, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
// No matching session, so TYPE_SMART_ACTION should not be logged.
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(1);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
}
@Test
public void onNotificationDirectReply() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onNotificationDirectReplied(statusBarNotification.getKey());
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(2);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
TextClassifierEvent secondEvent = textClassifierEvents.get(1);
assertThat(secondEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_MANUAL_REPLY);
}
@Test
public void oNotificationExpansionChanged_expanded() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onNotificationExpansionChanged(statusBarNotification, /* isExpanded= */ true);
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(2);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
TextClassifierEvent secondEvent = textClassifierEvents.get(1);
assertThat(secondEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_SHOWN);
}
@Test
public void oNotificationExpansionChanged_notExpanded() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onNotificationExpansionChanged(statusBarNotification, /* isExpanded= */ false);
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(1);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
}
@Test
public void oNotificationExpansionChanged_expanded_logShownEventOnce() {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
smartActions.onNotificationExpansionChanged(statusBarNotification, /* isExpanded= */ true);
smartActions.onNotificationExpansionChanged(statusBarNotification, /* isExpanded= */ true);
List<TextClassifierEvent> textClassifierEvents = fakeTextClassifier.getTextClassifierEvents();
assertThat(textClassifierEvents).hasSize(2);
TextClassifierEvent firstEvent = textClassifierEvents.get(0);
assertThat(firstEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_GENERATED);
TextClassifierEvent secondEvent = textClassifierEvents.get(1);
assertThat(secondEvent.getEventType()).isEqualTo(TextClassifierEvent.TYPE_ACTIONS_SHOWN);
}
@Test
public void copyCodeAction() {
Bundle extras = new Bundle();
Bundle entitiesExtras = new Bundle();
entitiesExtras.putString(SmartSuggestionsHelper.KEY_TEXT, "12345");
extras.putParcelable(SmartSuggestionsHelper.ENTITIES_EXTRAS, entitiesExtras);
ConversationAction conversationAction =
new ConversationAction.Builder(SmartSuggestionsHelper.TYPE_COPY).setExtras(extras).build();
fakeTextClassifier.setSuggestConversationActionsResponse(
new ConversationActions(ImmutableList.of(conversationAction), RESULT_ID));
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
SmartSuggestions smartSuggestions = smartActions.onNotificationEnqueued(statusBarNotification);
assertThat(smartSuggestions.getActions()).hasSize(1);
assertThat(smartSuggestions.getActions().get(0).title.toString()).isEqualTo("12345");
}
@Ignore // Disabled because it is way too slow to run on an emulator.
@Test
public void noBinderLeakage() {
// Use the real text classifier from system.
TextClassificationManager textClassificationManager =
context.getSystemService(TextClassificationManager.class);
textClassificationManager.setTextClassifier(null);
// System server crashes when there are more than 20,000 leaked binder proxy.
// See
// http://cs/android/frameworks/base/core/java/android/os/BinderProxy.java?l=73&rcl=ae52315c8c7d0391bd3c7bca0525a98eeb4cd840.
for (int i = 0; i < 20000; i++) {
Notification notification = createMessageCategoryNotification();
StatusBarNotification statusBarNotification =
createStatusBarNotification(notification, PACKAGE_NAME);
smartActions.onNotificationEnqueued(statusBarNotification);
}
}
private Notification createMessageCategoryNotification() {
return notificationBuilder
.setContentText(MESSAGE)
.setCategory(Notification.CATEGORY_MESSAGE)
.setActions(createReplyAction())
.build();
}
private static StatusBarNotification createStatusBarNotification(
Notification notification, String packageName) {
return new StatusBarNotification(
packageName,
packageName,
/* id= */ 2,
"tag",
/* uid= */ 1,
/* initialPid= */ 1,
/* score= */ 1,
notification,
Process.myUserHandle(),
System.currentTimeMillis());
}
private Notification.Action createReplyAction() {
return new Notification.Action.Builder(
Icon.createWithResource(context, android.R.drawable.stat_sys_warning),
"Reply",
PendingIntent.getActivity(context, 0, new Intent(context, this.getClass()), 0))
.addRemoteInput(new RemoteInput.Builder("result").setAllowFreeFormInput(true).build())
.build();
}
private static void assertMessage(
ConversationActions.Message subject,
String expectedMessage,
Person expectedAuthor,
long expectedReferenceTime) {
assertThat(subject.getText().toString()).isEqualTo(expectedMessage);
assertThat(subject.getAuthor()).isEqualTo(expectedAuthor);
assertThat(subject.getReferenceTime().toInstant())
.isEqualTo(Instant.ofEpochMilli(expectedReferenceTime));
}
private static void assertAdjustmentWithSmartReply(SmartSuggestions smartSuggestions) {
assertThat(smartSuggestions.getReplies()).containsExactly(SMART_REPLY);
}
private static void assertAdjustmentWithSmartAction(SmartSuggestions smartSuggestions) {
assertThat(smartSuggestions.getActions().get(0).title.toString())
.isEqualTo(ACTION_TITLE.toString());
}
private static class FakeTextClassifier implements TextClassifier {
private ConversationActions.Request lastRequest;
private final List<TextClassifierEvent> textClassifierEvents = new ArrayList<>();
private ConversationActions conversationActions;
@Override
public ConversationActions suggestConversationActions(ConversationActions.Request request) {
lastRequest = request;
if (conversationActions != null) {
return conversationActions;
}
Collection<String> types =
request
.getTypeConfig()
.resolveEntityListModifications(
Arrays.asList(
ConversationAction.TYPE_OPEN_URL, ConversationAction.TYPE_TEXT_REPLY));
List<ConversationAction> result = new ArrayList<>();
if (types.contains(ConversationAction.TYPE_TEXT_REPLY)) {
ConversationAction smartReply =
new ConversationAction.Builder(ConversationAction.TYPE_TEXT_REPLY)
.setTextReply(SMART_REPLY)
.setConfidenceScore(REPLY_SCORE)
.build();
result.add(smartReply);
}
if (types.contains(ConversationAction.TYPE_OPEN_URL)) {
Intent webIntent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("www.android.com"));
ConversationAction smartAction =
new ConversationAction.Builder(ConversationAction.TYPE_OPEN_URL)
.setConfidenceScore(ACTION_SCORE)
.setAction(
new RemoteAction(
Icon.createWithData(new byte[0], 0, 0),
ACTION_TITLE,
ACTION_TITLE,
PendingIntent.getActivity(
ApplicationProvider.getApplicationContext(), 0, webIntent, 0)))
.build();
result.add(smartAction);
}
return new ConversationActions(result, RESULT_ID);
}
@Override
public void onTextClassifierEvent(TextClassifierEvent event) {
textClassifierEvents.add(event);
}
private void setSuggestConversationActionsResponse(ConversationActions conversationActions) {
this.conversationActions = conversationActions;
}
private ConversationActions.Request getLastRequest() {
return lastRequest;
}
private List<TextClassifierEvent> getTextClassifierEvents() {
return ImmutableList.copyOf(textClassifierEvents);
}
}
private static class TestConfig implements SmartSuggestionsConfig {
private final boolean shouldGenerateReplies = true;
private final boolean shouldGenerateActions = true;
private final int maxSuggestions = 3;
private final int maxMessagesToExtract = 5;
@Override
public boolean shouldGenerateReplies() {
return shouldGenerateReplies;
}
@Override
public boolean shouldGenerateActions() {
return shouldGenerateActions;
}
@Override
public int getMaxSuggestions() {
return maxSuggestions;
}
@Override
public int getMaxMessagesToExtract() {
return maxMessagesToExtract;
}
}
}