Merge changes from topic "action-intent"

* changes:
  Update ExtService to use suggestConversationActions
  Support intent configuration for actions
This commit is contained in:
Tony Mak
2019-02-05 21:57:31 +00:00
committed by Android (Google) Code Review
15 changed files with 700 additions and 416 deletions

View File

@@ -53565,7 +53565,7 @@ package android.view.textclassifier {
method @NonNull public java.util.List<android.view.textclassifier.ConversationActions.Message> getConversation();
method @Nullable public String getConversationId();
method @Nullable public java.util.List<java.lang.String> getHints();
method @IntRange(from=0) public int getMaxSuggestions();
method @IntRange(from=0xffffffff) public int getMaxSuggestions();
method @NonNull public android.view.textclassifier.TextClassifier.EntityConfig getTypeConfig();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.view.textclassifier.ConversationActions.Request> CREATOR;
@@ -53578,7 +53578,7 @@ package android.view.textclassifier {
method @NonNull public android.view.textclassifier.ConversationActions.Request build();
method @NonNull public android.view.textclassifier.ConversationActions.Request.Builder setConversationId(@Nullable String);
method public android.view.textclassifier.ConversationActions.Request.Builder setHints(@Nullable java.util.List<java.lang.String>);
method @NonNull public android.view.textclassifier.ConversationActions.Request.Builder setMaxSuggestions(@IntRange(from=0) int);
method @NonNull public android.view.textclassifier.ConversationActions.Request.Builder setMaxSuggestions(@IntRange(from=0xffffffff) int);
method @NonNull public android.view.textclassifier.ConversationActions.Request.Builder setTypeConfig(@Nullable android.view.textclassifier.TextClassifier.EntityConfig);
}

View File

@@ -5752,6 +5752,8 @@ package android.provider {
public static interface DeviceConfig.NotificationAssistant {
field public static final String GENERATE_ACTIONS = "generate_actions";
field public static final String GENERATE_REPLIES = "generate_replies";
field public static final String MAX_MESSAGES_TO_EXTRACT = "max_messages_to_extract";
field public static final String MAX_SUGGESTIONS = "max_suggestions";
field public static final String NAMESPACE = "notification_assistant";
}

View File

@@ -147,6 +147,10 @@ public final class DeviceConfig {
* Whether the Notification Assistant should generate contextual actions for notifications.
*/
String GENERATE_ACTIONS = "generate_actions";
String MAX_MESSAGES_TO_EXTRACT = "max_messages_to_extract";
String MAX_SUGGESTIONS = "max_suggestions";
}
/**

View File

@@ -393,9 +393,10 @@ public final class ConversationActions implements Parcelable {
}
/**
* Return the maximal number of suggestions the caller wants, value 0 means no restriction.
* Return the maximal number of suggestions the caller wants, value -1 means no restriction
* and this is the default.
*/
@IntRange(from = 0)
@IntRange(from = -1)
public int getMaxSuggestions() {
return mMaxSuggestions;
}
@@ -443,7 +444,7 @@ public final class ConversationActions implements Parcelable {
private List<Message> mConversation;
@Nullable
private TextClassifier.EntityConfig mTypeConfig;
private int mMaxSuggestions;
private int mMaxSuggestions = -1;
@Nullable
private String mConversationId;
@Nullable
@@ -477,12 +478,11 @@ public final class ConversationActions implements Parcelable {
}
/**
* Sets the maximum number of suggestions you want.
* <p>
* Value 0 means no restriction.
* Sets the maximum number of suggestions you want. Value -1 means no restriction and
* this is the default.
*/
@NonNull
public Builder setMaxSuggestions(@IntRange(from = 0) int maxSuggestions) {
public Builder setMaxSuggestions(@IntRange(from = -1) int maxSuggestions) {
mMaxSuggestions = Preconditions.checkArgumentNonnegative(maxSuggestions);
return this;
}

View File

@@ -50,7 +50,8 @@ public interface IntentFactory {
new Intent(Intent.ACTION_TRANSLATE)
// TODO: Probably better to introduce a "translate" scheme instead of
// using EXTRA_TEXT.
.putExtra(Intent.EXTRA_TEXT, text),
.putExtra(Intent.EXTRA_TEXT, text)
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true),
text.hashCode()));
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2019 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.annotation.Nullable;
import android.content.Context;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.RemoteActionTemplate;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
/**
* Creates intents based on {@link RemoteActionTemplate} objects for a ClassificationResult.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class TemplateClassificationIntentFactory implements IntentFactory {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
private final TemplateIntentFactory mTemplateIntentFactory;
private final IntentFactory mFallback;
public TemplateClassificationIntentFactory(TemplateIntentFactory templateIntentFactory,
IntentFactory fallback) {
mTemplateIntentFactory = Preconditions.checkNotNull(templateIntentFactory);
mFallback = Preconditions.checkNotNull(fallback);
}
/**
* Returns a list of {@link android.view.textclassifier.TextClassifierImpl.LabeledIntent}
* that are constructed from the classification result.
*/
@NonNull
@Override
public List<TextClassifierImpl.LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification) {
if (classification == null) {
return Collections.emptyList();
}
RemoteActionTemplate[] remoteActionTemplates = classification.getRemoteActionTemplates();
if (ArrayUtils.isEmpty(remoteActionTemplates)) {
// RemoteActionTemplate is missing, fallback.
Log.w(TAG, "RemoteActionTemplate is missing, fallback to LegacyIntentFactory.");
return mFallback.create(context, text, foreignText, referenceTime, classification);
}
final List<TextClassifierImpl.LabeledIntent> labeledIntents =
mTemplateIntentFactory.create(remoteActionTemplates);
if (foreignText) {
IntentFactory.insertTranslateAction(labeledIntents, context, text.trim());
}
return labeledIntents;
}
}

View File

@@ -17,7 +17,6 @@ package android.view.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -25,64 +24,29 @@ import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.NamedVariant;
import com.google.android.textclassifier.RemoteActionTemplate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Creates intents based on {@link RemoteActionTemplate} objects.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class TemplateIntentFactory implements IntentFactory {
public final class TemplateIntentFactory {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
private final IntentFactory mFallback;
public TemplateIntentFactory(IntentFactory fallback) {
mFallback = Preconditions.checkNotNull(fallback);
}
/**
* Returns a list of {@link android.view.textclassifier.TextClassifierImpl.LabeledIntent}
* that are constructed from the classification result.
*/
@NonNull
@Override
public List<TextClassifierImpl.LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification) {
if (classification == null) {
@Nullable RemoteActionTemplate[] remoteActionTemplates) {
if (ArrayUtils.isEmpty(remoteActionTemplates)) {
return Collections.emptyList();
}
RemoteActionTemplate[] remoteActionTemplates = classification.getRemoteActionTemplates();
if (ArrayUtils.isEmpty(remoteActionTemplates)) {
// RemoteActionTemplate is missing, fallback.
Log.w(TAG, "RemoteActionTemplate is missing, fallback to LegacyIntentFactory.");
return mFallback.create(context, text, foreignText, referenceTime, classification);
}
final List<TextClassifierImpl.LabeledIntent> labeledIntents =
new ArrayList<>(createFromRemoteActionTemplates(remoteActionTemplates));
if (foreignText) {
IntentFactory.insertTranslateAction(labeledIntents, context, text.trim());
}
labeledIntents.forEach(
action -> action.getIntent()
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
return labeledIntents;
}
private static List<TextClassifierImpl.LabeledIntent> createFromRemoteActionTemplates(
RemoteActionTemplate[] remoteActionTemplates) {
final List<TextClassifierImpl.LabeledIntent> labeledIntents = new ArrayList<>();
for (RemoteActionTemplate remoteActionTemplate : remoteActionTemplates) {
Intent intent = createIntent(remoteActionTemplate);
@@ -100,6 +64,9 @@ public final class TemplateIntentFactory implements IntentFactory {
);
labeledIntents.add(labeledIntent);
}
labeledIntents.forEach(
action -> action.getIntent()
.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
return labeledIntents;
}

View File

@@ -113,6 +113,7 @@ public final class TextClassifierImpl implements TextClassifier {
private final ModelFileManager mActionsModelFileManager;
private final IntentFactory mIntentFactory;
private final TemplateIntentFactory mTemplateIntentFactory;
public TextClassifierImpl(
Context context, TextClassificationConstants settings, TextClassifier fallback) {
@@ -142,8 +143,10 @@ public final class TextClassifierImpl implements TextClassifier {
ActionsSuggestionsModel::getVersion,
ActionsSuggestionsModel::getLocales));
mTemplateIntentFactory = new TemplateIntentFactory();
mIntentFactory = mSettings.isTemplateIntentFactoryEnabled()
? new TemplateIntentFactory(new LegacyIntentFactory())
? new TemplateClassificationIntentFactory(
mTemplateIntentFactory, new LegacyIntentFactory())
: new LegacyIntentFactory();
}
@@ -189,7 +192,10 @@ public final class TextClassifierImpl implements TextClassifier {
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString),
mContext);
// Passing null here to suppress intent generation
// TODO: Use an explicit flag to suppress it.
/* appContext */ null,
/* deviceLocales */null);
final int size = results.length;
for (int i = 0; i < size; i++) {
tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
@@ -233,7 +239,11 @@ public final class TextClassifierImpl implements TextClassifier {
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString),
mContext);
mContext,
// TODO: Pass the locale list once it is supported in
// native side.
LocaleList.getDefault().get(0).toLanguageTag()
);
if (results.length > 0) {
return createClassificationResult(
results, string,
@@ -389,32 +399,13 @@ public final class TextClassifierImpl implements TextClassifier {
new ActionsSuggestionsModel.Conversation(nativeMessages);
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
actionsImpl.suggestActions(nativeConversation, null);
Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
List<ConversationAction> conversationActions = new ArrayList<>();
int maxSuggestions = nativeSuggestions.length;
if (request.getMaxSuggestions() > 0) {
maxSuggestions = Math.min(request.getMaxSuggestions(), nativeSuggestions.length);
}
for (int i = 0; i < maxSuggestions; i++) {
ActionsSuggestionsModel.ActionSuggestion nativeSuggestion = nativeSuggestions[i];
String actionType = nativeSuggestion.getActionType();
if (!expectedTypes.contains(actionType)) {
continue;
}
conversationActions.add(
new ConversationAction.Builder(actionType)
.setTextReply(nativeSuggestion.getResponseText())
.setConfidenceScore(nativeSuggestion.getScore())
.build());
}
String resultId = ActionsSuggestionsHelper.createResultId(
mContext,
request.getConversation(),
mActionModelInUse.getVersion(),
mActionModelInUse.getSupportedLocales());
return new ConversationActions(conversationActions, resultId);
actionsImpl.suggestActionsWithIntents(
nativeConversation,
null,
mContext,
// TODO: Pass the locale list once it is supported in native side.
LocaleList.getDefault().get(0).toLanguageTag());
return createConversationActionResult(request, nativeSuggestions);
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
@@ -422,6 +413,43 @@ public final class TextClassifierImpl implements TextClassifier {
return mFallback.suggestConversationActions(request);
}
private ConversationActions createConversationActionResult(
ConversationActions.Request request,
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
List<ConversationAction> conversationActions = new ArrayList<>();
for (ActionsSuggestionsModel.ActionSuggestion nativeSuggestion : nativeSuggestions) {
if (request.getMaxSuggestions() >= 0
&& conversationActions.size() == request.getMaxSuggestions()) {
break;
}
String actionType = nativeSuggestion.getActionType();
if (!expectedTypes.contains(actionType)) {
continue;
}
List<LabeledIntent> labeledIntents =
mTemplateIntentFactory.create(nativeSuggestion.getRemoteActionTemplates());
RemoteAction remoteAction = null;
// Given that we only support implicit intent here, we should expect there is just one
// intent for each action type.
if (!labeledIntents.isEmpty()) {
remoteAction = labeledIntents.get(0).asRemoteAction(mContext);
}
conversationActions.add(
new ConversationAction.Builder(actionType)
.setConfidenceScore(nativeSuggestion.getScore())
.setTextReply(nativeSuggestion.getResponseText())
.setAction(remoteAction)
.build());
}
String resultId = ActionsSuggestionsHelper.createResultId(
mContext,
request.getConversation(),
mActionModelInUse.getVersion(),
mActionModelInUse.getSupportedLocales());
return new ConversationActions(conversationActions, resultId);
}
@Nullable
private String detectLanguageTagsFromText(CharSequence text) {
TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
@@ -462,11 +490,13 @@ public final class TextClassifierImpl implements TextClassifier {
}
if (mAnnotatorImpl == null || !Objects.equals(mAnnotatorModelInUse, bestModel)) {
Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
destroyAnnotatorImplIfExistsLocked();
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
try {
if (pfd != null) {
// The current annotator model may be still used by another thread / model.
// Do not call close() here, and let the GC to clean it up when no one else
// is using it.
mAnnotatorImpl = new AnnotatorModel(pfd.getFd());
mAnnotatorModelInUse = bestModel;
}
@@ -478,14 +508,6 @@ public final class TextClassifierImpl implements TextClassifier {
}
}
@GuardedBy("mLock") // Do not call outside this lock.
private void destroyAnnotatorImplIfExistsLocked() {
if (mAnnotatorImpl != null) {
mAnnotatorImpl.close();
mAnnotatorImpl = null;
}
}
private LangIdModel getLangIdImpl() throws FileNotFoundException {
synchronized (mLock) {
if (mLangIdImpl == null) {
@@ -522,7 +544,8 @@ public final class TextClassifierImpl implements TextClassifier {
new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
try {
if (pfd != null) {
mActionsImpl = new ActionsSuggestionsModel(pfd.getFd());
mActionsImpl = new ActionsSuggestionsModel(
pfd.getFd(), getAnnotatorImpl(LocaleList.getDefault()));
mActionModelInUse = bestModel;
}
} finally {

View File

@@ -0,0 +1,150 @@
/*
* Copyright (C) 2019 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.content.Intent;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.RemoteActionTemplate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TemplateClassificationIntentFactoryTest {
private static final String TEXT = "text";
private static final String TITLE = "Map";
private static final String ACTION = Intent.ACTION_VIEW;
@Mock
private IntentFactory mFallback;
private TemplateClassificationIntentFactory mTemplateClassificationIntentFactory;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mTemplateClassificationIntentFactory = new TemplateClassificationIntentFactory(
new TemplateIntentFactory(),
mFallback);
}
@Test
public void create_foreignText() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE,
null,
ACTION,
null,
null,
null,
null,
null,
null,
null
);
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents =
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ true,
null,
classificationResult);
assertThat(intents).hasSize(2);
TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
Intent intent = labeledIntent.getIntent();
assertThat(intent.getAction()).isEqualTo(ACTION);
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
labeledIntent = intents.get(1);
intent = labeledIntent.getIntent();
assertThat(intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
}
@Test
public void create_notForeignText() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE,
null,
ACTION,
null,
null,
null,
null,
null,
null,
null
);
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents =
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ false,
null,
classificationResult);
assertThat(intents).hasSize(1);
TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.getTitle()).isEqualTo(TITLE);
Intent intent = labeledIntent.getIntent();
assertThat(intent.getAction()).isEqualTo(ACTION);
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
}
}

View File

@@ -13,7 +13,6 @@
* 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;
@@ -21,18 +20,15 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Intent;
import android.net.Uri;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.NamedVariant;
import com.google.android.textclassifier.RemoteActionTemplate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
@@ -62,14 +58,12 @@ public class TemplateIntentFactoryTest {
};
private static final Integer REQUEST_CODE = 10;
@Mock
private IntentFactory mFallback;
private TemplateIntentFactory mTemplateIntentFactory;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mTemplateIntentFactory = new TemplateIntentFactory(mFallback);
mTemplateIntentFactory = new TemplateIntentFactory();
}
@Test
@@ -87,25 +81,9 @@ public class TemplateIntentFactoryTest {
REQUEST_CODE
);
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents = mTemplateIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
false,
null,
classificationResult);
List<TextClassifierImpl.LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
assertThat(intents).hasSize(1);
TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
@@ -122,12 +100,11 @@ public class TemplateIntentFactoryTest {
assertThat(
intent.getStringExtra(KEY_ONE)).isEqualTo(VALUE_ONE);
assertThat(intent.getIntExtra(KEY_TWO, 0)).isEqualTo(VALUE_TWO);
assertThat(
intent.getBooleanExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, false)).isTrue();
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
}
@Test
public void create_pacakgeIsNotNull() {
public void create_packageIsNotNull() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE,
DESCRIPTION,
@@ -141,25 +118,8 @@ public class TemplateIntentFactoryTest {
REQUEST_CODE
);
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents = mTemplateIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
false,
null,
classificationResult);
List<TextClassifierImpl.LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).hasSize(0);
}
@@ -179,25 +139,9 @@ public class TemplateIntentFactoryTest {
null
);
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
List<TextClassifierImpl.LabeledIntent> intents = mTemplateIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
false,
null,
classificationResult);
assertThat(intents).hasSize(1);
TextClassifierImpl.LabeledIntent labeledIntent = intents.get(0);
@@ -212,7 +156,6 @@ public class TemplateIntentFactoryTest {
assertThat(intent.getFlags()).isEqualTo(0);
assertThat(intent.getCategories()).isNull();
assertThat(intent.getPackage()).isNull();
assertThat(
intent.getBooleanExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, false)).isTrue();
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
}
}

View File

@@ -23,7 +23,6 @@ import android.os.Handler;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.KeyValueListParser;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -37,6 +36,9 @@ final class AssistantSettings extends ContentObserver {
private static final boolean DEFAULT_GENERATE_REPLIES = true;
private static final boolean DEFAULT_GENERATE_ACTIONS = true;
private static final int DEFAULT_NEW_INTERRUPTION_MODEL_INT = 1;
private static final int DEFAULT_MAX_MESSAGES_TO_EXTRACT = 5;
@VisibleForTesting
static final int DEFAULT_MAX_SUGGESTIONS = 3;
private static final Uri STREAK_LIMIT_URI =
Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
@@ -46,7 +48,6 @@ final class AssistantSettings extends ContentObserver {
private static final Uri NOTIFICATION_NEW_INTERRUPTION_MODEL_URI =
Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL);
private final KeyValueListParser mParser = new KeyValueListParser(',');
private final ContentResolver mResolver;
private final int mUserId;
@@ -55,12 +56,14 @@ final class AssistantSettings extends ContentObserver {
@VisibleForTesting
protected final Runnable mOnUpdateRunnable;
// Actuall configuration settings.
// Actual configuration settings.
float mDismissToViewRatioLimit;
int mStreakLimit;
boolean mGenerateReplies = DEFAULT_GENERATE_REPLIES;
boolean mGenerateActions = DEFAULT_GENERATE_ACTIONS;
boolean mNewInterruptionModel;
int mMaxMessagesToExtract = DEFAULT_MAX_MESSAGES_TO_EXTRACT;
int mMaxSuggestions = DEFAULT_MAX_SUGGESTIONS;
private AssistantSettings(Handler handler, ContentResolver resolver, int userId,
Runnable onUpdateRunnable) {
@@ -124,27 +127,18 @@ final class AssistantSettings extends ContentObserver {
}
private void updateFromDeviceConfigFlags() {
String generateRepliesFlag = DeviceConfig.getProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES);
if (TextUtils.isEmpty(generateRepliesFlag)) {
mGenerateReplies = DEFAULT_GENERATE_REPLIES;
} else {
// parseBoolean returns false for everything that isn't 'true' so there's no need to
// sanitise the flag string here.
mGenerateReplies = Boolean.parseBoolean(generateRepliesFlag);
}
mGenerateReplies = DeviceConfigHelper.getBoolean(
DeviceConfig.NotificationAssistant.GENERATE_REPLIES, DEFAULT_GENERATE_REPLIES);
String generateActionsFlag = DeviceConfig.getProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS);
if (TextUtils.isEmpty(generateActionsFlag)) {
mGenerateActions = DEFAULT_GENERATE_ACTIONS;
} else {
// parseBoolean returns false for everything that isn't 'true' so there's no need to
// sanitise the flag string here.
mGenerateActions = Boolean.parseBoolean(generateActionsFlag);
}
mGenerateActions = DeviceConfigHelper.getBoolean(
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS, DEFAULT_GENERATE_ACTIONS);
mMaxMessagesToExtract = DeviceConfigHelper.getInteger(
DeviceConfig.NotificationAssistant.MAX_MESSAGES_TO_EXTRACT,
DEFAULT_MAX_MESSAGES_TO_EXTRACT);
mMaxSuggestions = DeviceConfigHelper.getInteger(
DeviceConfig.NotificationAssistant.MAX_SUGGESTIONS, DEFAULT_MAX_SUGGESTIONS);
mOnUpdateRunnable.run();
}
@@ -175,8 +169,37 @@ final class AssistantSettings extends ContentObserver {
mOnUpdateRunnable.run();
}
static class DeviceConfigHelper {
static int getInteger(String key, int defaultValue) {
String value = getValue(key);
if (TextUtils.isEmpty(value)) {
return defaultValue;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
return defaultValue;
}
}
static boolean getBoolean(String key, boolean defaultValue) {
String value = getValue(key);
if (TextUtils.isEmpty(value)) {
return defaultValue;
}
return Boolean.parseBoolean(value);
}
private static String getValue(String key) {
return DeviceConfig.getProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
key);
}
}
public interface Factory {
AssistantSettings createAndRegister(Handler handler, ContentResolver resolver, int userId,
Runnable onUpdateRunnable);
}
}
}

View File

@@ -29,28 +29,22 @@ import android.text.TextUtils;
import android.util.LruCache;
import android.view.textclassifier.ConversationAction;
import android.view.textclassifier.ConversationActions;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextClassifierEvent;
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;
public class SmartActionsHelper {
private static final ArrayList<Notification.Action> EMPTY_ACTION_LIST = new ArrayList<>();
private static final ArrayList<CharSequence> EMPTY_REPLY_LIST = new ArrayList<>();
private static final String KEY_ACTION_TYPE = "action_type";
// If a notification has any of these flags set, it's inelgibile for actions being added.
private static final int FLAG_MASK_INELGIBILE_FOR_ACTIONS =
@@ -58,19 +52,8 @@ public class SmartActionsHelper {
| Notification.FLAG_FOREGROUND_SERVICE
| Notification.FLAG_GROUP_SUMMARY
| Notification.FLAG_NO_CLEAR;
private static final int MAX_ACTION_EXTRACTION_TEXT_LENGTH = 400;
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 int MAX_RESULT_ID_TO_CACHE = 20;
private static final TextClassifier.EntityConfig TYPE_CONFIG =
new TextClassifier.EntityConfig.Builder().setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_TEXT_REPLY))
.includeTypesFromTextClassifier(false)
.build();
private static final List<String> HINTS =
Collections.singletonList(ConversationActions.Request.HINT_FOR_NOTIFICATION);
@@ -92,20 +75,30 @@ public class SmartActionsHelper {
mSettings = settings;
}
@NonNull
SmartSuggestions suggest(@NonNull NotificationEntry entry) {
// Whenever suggest() is called on a notification, its previous session is ended.
mNotificationKeyToResultIdCache.remove(entry.getSbn().getKey());
ArrayList<Notification.Action> actions = suggestActions(entry);
ArrayList<CharSequence> replies = suggestReplies(entry);
boolean eligibleForReplyAdjustment =
mSettings.mGenerateReplies && isEligibleForReplyAdjustment(entry);
boolean eligibleForActionAdjustment =
mSettings.mGenerateActions && isEligibleForActionAdjustment(entry);
// Not logging subsequent events of this notification if we didn't generate any suggestion
// for it.
if (replies.isEmpty() && actions.isEmpty()) {
mNotificationKeyToResultIdCache.remove(entry.getSbn().getKey());
}
List<ConversationAction> conversationActions =
suggestConversationActions(
entry,
eligibleForReplyAdjustment,
eligibleForActionAdjustment);
ArrayList<CharSequence> replies = conversationActions.stream()
.map(ConversationAction::getTextReply)
.filter(textReply -> !TextUtils.isEmpty(textReply))
.collect(Collectors.toCollection(ArrayList::new));
ArrayList<Notification.Action> actions = conversationActions.stream()
.filter(conversationAction -> conversationAction.getAction() != null)
.map(action -> createNotificationAction(action.getAction(), action.getType()))
.collect(Collectors.toCollection(ArrayList::new));
return new SmartSuggestions(replies, actions);
}
@@ -113,61 +106,48 @@ public class SmartActionsHelper {
* Adds action adjustments based on the notification contents.
*/
@NonNull
ArrayList<Notification.Action> suggestActions(@NonNull NotificationEntry entry) {
if (!mSettings.mGenerateActions) {
return EMPTY_ACTION_LIST;
}
if (!isEligibleForActionAdjustment(entry)) {
return EMPTY_ACTION_LIST;
private List<ConversationAction> suggestConversationActions(
@NonNull NotificationEntry entry,
boolean includeReplies,
boolean includeActions) {
if (!includeReplies && !includeActions) {
return Collections.emptyList();
}
if (mTextClassifier == null) {
return EMPTY_ACTION_LIST;
return Collections.emptyList();
}
List<ConversationActions.Message> messages = extractMessages(entry.getNotification());
if (messages.isEmpty()) {
return EMPTY_ACTION_LIST;
return Collections.emptyList();
}
// TODO: Move to TextClassifier.suggestConversationActions once it is ready.
return suggestActionsFromText(
messages.get(messages.size() - 1).getText(), MAX_SMART_ACTIONS);
}
@NonNull
ArrayList<CharSequence> suggestReplies(@NonNull NotificationEntry entry) {
if (!mSettings.mGenerateReplies) {
return EMPTY_REPLY_LIST;
}
if (!isEligibleForReplyAdjustment(entry)) {
return EMPTY_REPLY_LIST;
}
if (mTextClassifier == null) {
return EMPTY_REPLY_LIST;
}
List<ConversationActions.Message> messages = extractMessages(entry.getNotification());
if (messages.isEmpty()) {
return EMPTY_REPLY_LIST;
TextClassifier.EntityConfig.Builder typeConfigBuilder =
new TextClassifier.EntityConfig.Builder();
if (!includeReplies) {
typeConfigBuilder.setExcludedTypes(
Collections.singletonList(ConversationAction.TYPE_TEXT_REPLY));
} else if (!includeActions) {
typeConfigBuilder
.setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_TEXT_REPLY))
.includeTypesFromTextClassifier(false);
}
ConversationActions.Request request =
new ConversationActions.Request.Builder(messages)
.setMaxSuggestions(MAX_SUGGESTED_REPLIES)
.setMaxSuggestions(mSettings.mMaxSuggestions)
.setHints(HINTS)
.setTypeConfig(TYPE_CONFIG)
.setTypeConfig(typeConfigBuilder.build())
.build();
ConversationActions conversationActionsResult =
mTextClassifier.suggestConversationActions(request);
List<ConversationAction> conversationActions =
conversationActionsResult.getConversationActions();
ArrayList<CharSequence> replies = conversationActions.stream()
.map(conversationAction -> conversationAction.getTextReply())
.filter(textReply -> !TextUtils.isEmpty(textReply))
.collect(Collectors.toCollection(ArrayList::new));
String resultId = conversationActionsResult.getId();
if (resultId != null) {
if (!TextUtils.isEmpty(resultId)
&& !conversationActionsResult.getConversationActions().isEmpty()) {
mNotificationKeyToResultIdCache.put(entry.getSbn().getKey(), resultId);
}
return replies;
return conversationActionsResult.getConversationActions();
}
void onNotificationExpansionChanged(@NonNull NotificationEntry entry, boolean isUserAction,
@@ -248,6 +228,17 @@ public class SmartActionsHelper {
mTextClassifier.onTextClassifierEvent(textClassifierEvent);
}
private Notification.Action createNotificationAction(
RemoteAction remoteAction, String actionType) {
return new Notification.Action.Builder(
remoteAction.getIcon(),
remoteAction.getTitle(),
remoteAction.getActionIntent())
.setContextual(true)
.addExtras(Bundle.forPair(KEY_ACTION_TYPE, actionType))
.build();
}
private TextClassifierEvent.Builder createTextClassifierEventBuilder(
int eventType, @NonNull String resultId) {
return new TextClassifierEvent.Builder(
@@ -308,7 +299,7 @@ public class SmartActionsHelper {
private List<ConversationActions.Message> extractMessages(@NonNull Notification notification) {
Parcelable[] messages = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
if (messages == null || messages.length == 0) {
return Arrays.asList(new ConversationActions.Message.Builder(
return Collections.singletonList(new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText(notification.extras.getCharSequence(Notification.EXTRA_TEXT))
.build());
@@ -335,75 +326,13 @@ public class SmartActionsHelper {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(message.getTimestamp()),
ZoneOffset.systemDefault()))
.build());
if (extractMessages.size() >= MAX_MESSAGES_TO_EXTRACT) {
if (extractMessages.size() >= mSettings.mMaxMessagesToExtract) {
break;
}
}
return new ArrayList<>(extractMessages);
}
/** Returns a list of actions to act on entities in a given piece of text. */
@NonNull
private ArrayList<Notification.Action> suggestActionsFromText(
@Nullable CharSequence text, int maxSmartActions) {
if (TextUtils.isEmpty(text)) {
return EMPTY_ACTION_LIST;
}
// We want to process only text visible to the user to avoid confusing suggestions, so we
// truncate the text to a reasonable length. This is particularly important for e.g.
// email apps that sometimes include the text for the entire thread.
text = text.subSequence(0, Math.min(text.length(), MAX_ACTION_EXTRACTION_TEXT_LENGTH));
// Extract all entities.
TextLinks.Request textLinksRequest = new TextLinks.Request.Builder(text)
.setEntityConfig(
TextClassifier.EntityConfig.createWithHints(
Collections.singletonList(
TextClassifier.HINT_TEXT_IS_NOT_EDITABLE)))
.build();
TextLinks links = mTextClassifier.generateLinks(textLinksRequest);
EntityTypeCounter entityTypeCounter = EntityTypeCounter.fromTextLinks(links);
ArrayList<Notification.Action> actions = new ArrayList<>();
for (TextLinks.TextLink link : links.getLinks()) {
// Ignore any entity type for which we have too many entities. This is to handle the
// case where a notification contains e.g. a list of phone numbers. In such cases, the
// user likely wants to act on the whole list rather than an individual entity.
if (link.getEntityCount() == 0
|| entityTypeCounter.getCount(link.getEntity(0)) != 1) {
continue;
}
// Generate the actions, and add the most prominent ones to the action bar.
TextClassification classification =
mTextClassifier.classifyText(
new TextClassification.Request.Builder(
text, link.getStart(), link.getEnd()).build());
if (classification.getEntityCount() == 0) {
continue;
}
int numOfActions = Math.min(
MAX_ACTIONS_PER_LINK, classification.getActions().size());
for (int i = 0; i < numOfActions; ++i) {
RemoteAction remoteAction = classification.getActions().get(i);
Notification.Action action = new Notification.Action.Builder(
remoteAction.getIcon(),
remoteAction.getTitle(),
remoteAction.getActionIntent())
.setContextual(true)
.addExtras(Bundle.forPair(KEY_ACTION_TYPE, classification.getEntity(0)))
.build();
actions.add(action);
// We have enough smart actions.
if (actions.size() >= maxSmartActions) {
return actions;
}
}
}
return actions;
}
static class SmartSuggestions {
public final ArrayList<CharSequence> replies;
public final ArrayList<Notification.Action> actions;

View File

@@ -15,6 +15,7 @@ android_test {
static_libs: [
"ExtServices-core",
"android-support-test",
"compatibility-device-util",
"mockito-target-minus-junit4",
"espresso-core",
"truth-prebuilt",

View File

@@ -16,6 +16,10 @@
package android.ext.services.notification;
import static android.ext.services.notification.AssistantSettings.DEFAULT_MAX_SUGGESTIONS;
import static android.provider.DeviceConfig.NotificationAssistant;
import static android.provider.DeviceConfig.setProperty;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@@ -26,12 +30,13 @@ import static org.mockito.Mockito.verify;
import android.content.ContentResolver;
import android.os.Handler;
import android.os.Looper;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.UiDevice;
import android.testing.TestableContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -39,8 +44,13 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
public class AssistantSettingsTest {
private static final String CLEAR_DEVICE_CONFIG_KEY_CMD =
"device_config delete " + NotificationAssistant.NAMESPACE;
private static final int USER_ID = 5;
@Rule
@@ -69,16 +79,21 @@ public class AssistantSettingsTest {
handler, mResolver, USER_ID, mOnUpdateRunnable);
}
@After
public void tearDown() throws IOException {
clearDeviceConfig();
}
@Test
public void testGenerateRepliesDisabled() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"false",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"false");
assertFalse(mAssistantSettings.mGenerateReplies);
@@ -86,14 +101,14 @@ public class AssistantSettingsTest {
@Test
public void testGenerateRepliesEnabled() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"true",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"true");
assertTrue(mAssistantSettings.mGenerateReplies);
@@ -101,26 +116,26 @@ public class AssistantSettingsTest {
@Test
public void testGenerateRepliesEmptyFlag() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"false",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"false");
assertFalse(mAssistantSettings.mGenerateReplies);
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_REPLIES,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_REPLIES,
"");
// Go back to the default value.
@@ -129,14 +144,14 @@ public class AssistantSettingsTest {
@Test
public void testGenerateActionsDisabled() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"false",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"false");
assertFalse(mAssistantSettings.mGenerateActions);
@@ -144,14 +159,14 @@ public class AssistantSettingsTest {
@Test
public void testGenerateActionsEnabled() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"true",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"true");
assertTrue(mAssistantSettings.mGenerateActions);
@@ -159,32 +174,72 @@ public class AssistantSettingsTest {
@Test
public void testGenerateActionsEmptyFlag() {
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"false",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"false");
assertFalse(mAssistantSettings.mGenerateActions);
DeviceConfig.setProperty(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
DeviceConfig.NotificationAssistant.NAMESPACE,
DeviceConfig.NotificationAssistant.GENERATE_ACTIONS,
NotificationAssistant.NAMESPACE,
NotificationAssistant.GENERATE_ACTIONS,
"");
// Go back to the default value.
assertTrue(mAssistantSettings.mGenerateActions);
}
@Test
public void testMaxMessagesToExtract() {
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.MAX_MESSAGES_TO_EXTRACT,
"10",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
NotificationAssistant.NAMESPACE,
NotificationAssistant.MAX_MESSAGES_TO_EXTRACT,
"10");
assertEquals(10, mAssistantSettings.mMaxMessagesToExtract);
}
@Test
public void testMaxSuggestions() {
setProperty(
NotificationAssistant.NAMESPACE,
NotificationAssistant.MAX_SUGGESTIONS,
"5",
false /* makeDefault */);
mAssistantSettings.onDeviceConfigPropertyChanged(
NotificationAssistant.NAMESPACE,
NotificationAssistant.MAX_SUGGESTIONS,
"5");
assertEquals(5, mAssistantSettings.mMaxSuggestions);
}
@Test
public void testMaxSuggestionsEmpty() {
mAssistantSettings.onDeviceConfigPropertyChanged(
NotificationAssistant.NAMESPACE,
NotificationAssistant.MAX_SUGGESTIONS,
"");
assertEquals(DEFAULT_MAX_SUGGESTIONS, mAssistantSettings.mMaxSuggestions);
}
@Test
public void testStreakLimit() {
verify(mOnUpdateRunnable, never()).run();
@@ -219,4 +274,17 @@ public class AssistantSettingsTest {
assertEquals(newDismissToViewRatioLimit, mAssistantSettings.mDismissToViewRatioLimit, 1e-6);
verify(mOnUpdateRunnable).run();
}
private static void clearDeviceConfig() throws IOException {
UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
uiDevice.executeShellCommand(
CLEAR_DEVICE_CONFIG_KEY_CMD + " " + NotificationAssistant.GENERATE_ACTIONS);
uiDevice.executeShellCommand(
CLEAR_DEVICE_CONFIG_KEY_CMD + " " + NotificationAssistant.GENERATE_REPLIES);
uiDevice.executeShellCommand(
CLEAR_DEVICE_CONFIG_KEY_CMD + " " + NotificationAssistant.MAX_MESSAGES_TO_EXTRACT);
uiDevice.executeShellCommand(
CLEAR_DEVICE_CONFIG_KEY_CMD + " " + NotificationAssistant.MAX_SUGGESTIONS);
}
}

View File

@@ -25,8 +25,15 @@ import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.graphics.drawable.Icon;
import android.os.Process;
import android.service.notification.NotificationAssistantService;
import android.service.notification.StatusBarNotification;
@@ -53,7 +60,6 @@ import org.mockito.MockitoAnnotations;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -62,20 +68,26 @@ import java.util.Objects;
import javax.annotation.Nullable;
@RunWith(AndroidJUnit4.class)
public class SmartActionHelperTest {
public class SmartActionsHelperTest {
private static final String NOTIFICATION_KEY = "key";
private static final String RESULT_ID = "id";
private static final ConversationAction REPLY_ACTION =
new ConversationAction.Builder(ConversationAction.TYPE_TEXT_REPLY)
.setTextReply("Home")
.build();
.setTextReply("Home")
.build();
private static final String MESSAGE = "Where are you?";
@Mock
IPackageManager mIPackageManager;
@Mock
private TextClassifier mTextClassifier;
@Mock
private StatusBarNotification mStatusBarNotification;
@Mock
private SmsHelper mSmsHelper;
private SmartActionsHelper mSmartActionsHelper;
private Context mContext;
@Mock private TextClassifier mTextClassifier;
@Mock private NotificationEntry mNotificationEntry;
@Mock private StatusBarNotification mStatusBarNotification;
private Notification.Builder mNotificationBuilder;
private AssistantSettings mSettings;
@@ -89,10 +101,6 @@ public class SmartActionHelperTest {
when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
.thenReturn(new ConversationActions(Arrays.asList(REPLY_ACTION), RESULT_ID));
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());
when(mStatusBarNotification.getKey()).thenReturn(NOTIFICATION_KEY);
@@ -105,33 +113,96 @@ public class SmartActionHelperTest {
}
@Test
public void testSuggestReplies_notMessagingApp() {
when(mNotificationEntry.isMessaging()).thenReturn(false);
ArrayList<CharSequence> textReplies =
mSmartActionsHelper.suggestReplies(mNotificationEntry);
assertThat(textReplies).isEmpty();
public void testSuggest_notMessageNotification() {
Notification notification = mNotificationBuilder.setContentText(MESSAGE).build();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggest(createNotificationEntry());
verify(mTextClassifier, never())
.suggestConversationActions(any(ConversationActions.Request.class));
}
@Test
public void testSuggestReplies_noInlineReply() {
when(mNotificationEntry.hasInlineReply()).thenReturn(false);
ArrayList<CharSequence> textReplies =
mSmartActionsHelper.suggestReplies(mNotificationEntry);
assertThat(textReplies).isEmpty();
public void testSuggest_noInlineReply() {
Notification notification =
mNotificationBuilder
.setContentText(MESSAGE)
.setCategory(Notification.CATEGORY_MESSAGE)
.build();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
ConversationActions.Request request = runSuggestAndCaptureRequest();
// actions are enabled, but replies are not.
assertThat(
request.getTypeConfig().resolveEntityListModifications(
Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
ConversationAction.TYPE_OPEN_URL)))
.containsExactly(ConversationAction.TYPE_OPEN_URL);
}
@Test
public void testSuggestReplies_nonMessageStyle() {
Notification notification = mNotificationBuilder.setContentText("Where are you?").build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
public void testSuggest_settingsOff() {
mSettings.mGenerateActions = false;
mSettings.mGenerateReplies = false;
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
List<ConversationActions.Message> messages = getMessagesInRequest();
mSmartActionsHelper.suggest(createNotificationEntry());
verify(mTextClassifier, never())
.suggestConversationActions(any(ConversationActions.Request.class));
}
@Test
public void testSuggest_settings_repliesOnActionsOff() {
mSettings.mGenerateReplies = true;
mSettings.mGenerateActions = false;
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
ConversationActions.Request request = runSuggestAndCaptureRequest();
// replies are enabled, but actions are not.
assertThat(
request.getTypeConfig().resolveEntityListModifications(
Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
ConversationAction.TYPE_OPEN_URL)))
.containsExactly(ConversationAction.TYPE_TEXT_REPLY);
}
@Test
public void testSuggest_settings_repliesOffActionsOn() {
mSettings.mGenerateReplies = false;
mSettings.mGenerateActions = true;
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
ConversationActions.Request request = runSuggestAndCaptureRequest();
// actions are enabled, but replies are not.
assertThat(
request.getTypeConfig().resolveEntityListModifications(
Arrays.asList(ConversationAction.TYPE_TEXT_REPLY,
ConversationAction.TYPE_OPEN_URL)))
.containsExactly(ConversationAction.TYPE_OPEN_URL);
}
@Test
public void testSuggest_nonMessageStyleMessageNotification() {
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
List<ConversationActions.Message> messages =
runSuggestAndCaptureRequest().getConversation();
assertThat(messages).hasSize(1);
MessageSubject.assertThat(messages.get(0)).hasText("Where are you?");
MessageSubject.assertThat(messages.get(0)).hasText(MESSAGE);
}
@Test
public void testSuggestReplies_messageStyle() {
public void testSuggest_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();
@@ -145,10 +216,12 @@ public class SmartActionHelperTest {
mNotificationBuilder
.setContentText("You have three new messages")
.setStyle(style)
.setActions(createReplyAction())
.build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
when(mStatusBarNotification.getNotification()).thenReturn(notification);
List<ConversationActions.Message> messages = getMessagesInRequest();
List<ConversationActions.Message> messages =
runSuggestAndCaptureRequest().getConversation();
assertThat(messages).hasSize(3);
ConversationActions.Message secondMessage = messages.get(0);
@@ -172,7 +245,7 @@ public class SmartActionHelperTest {
}
@Test
public void testSuggestReplies_messageStyle_noPerson() {
public void testSuggest_messageStyle_noPerson() {
Person me = new Person.Builder().setName("Me").build();
Notification.MessagingStyle style =
new Notification.MessagingStyle(me).addMessage("message", 1000, (Person) null);
@@ -180,10 +253,11 @@ public class SmartActionHelperTest {
mNotificationBuilder
.setContentText("You have one new message")
.setStyle(style)
.setActions(createReplyAction())
.build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.suggest(createNotificationEntry());
verify(mTextClassifier, never())
.suggestConversationActions(any(ConversationActions.Request.class));
@@ -191,13 +265,12 @@ public class SmartActionHelperTest {
@Test
public void testOnSuggestedReplySent() {
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onSuggestedReplySent(
NOTIFICATION_KEY, message, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
NOTIFICATION_KEY, MESSAGE, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
ArgumentCaptor<TextClassifierEvent> argumentCaptor =
ArgumentCaptor.forClass(TextClassifierEvent.class);
@@ -208,13 +281,12 @@ public class SmartActionHelperTest {
@Test
public void testOnSuggestedReplySent_anotherNotification() {
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onSuggestedReplySent(
"something_else", message, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
"something_else", MESSAGE, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
verify(mTextClassifier, never())
.onTextClassifierEvent(Mockito.any(TextClassifierEvent.class));
@@ -225,13 +297,12 @@ public class SmartActionHelperTest {
when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
.thenReturn(new ConversationActions(Collections.emptyList(), null));
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onSuggestedReplySent(
"something_else", message, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
"something_else", MESSAGE, NotificationAssistantService.SOURCE_FROM_ASSISTANT);
verify(mTextClassifier, never())
.onTextClassifierEvent(Mockito.any(TextClassifierEvent.class));
@@ -239,10 +310,10 @@ public class SmartActionHelperTest {
@Test
public void testOnNotificationDirectReply() {
Notification notification = mNotificationBuilder.setContentText("Where are you?").build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onNotificationDirectReplied(NOTIFICATION_KEY);
ArgumentCaptor<TextClassifierEvent> argumentCaptor =
@@ -254,12 +325,11 @@ public class SmartActionHelperTest {
@Test
public void testOnNotificationExpansionChanged() {
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.onNotificationExpansionChanged(mNotificationEntry, true, true);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), true, true);
ArgumentCaptor<TextClassifierEvent> argumentCaptor =
ArgumentCaptor.forClass(TextClassifierEvent.class);
@@ -270,12 +340,11 @@ public class SmartActionHelperTest {
@Test
public void testOnNotificationsSeen_notExpanded() {
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.onNotificationExpansionChanged(mNotificationEntry, false, false);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), false, false);
verify(mTextClassifier, never()).onTextClassifierEvent(
Mockito.any(TextClassifierEvent.class));
@@ -283,12 +352,11 @@ public class SmartActionHelperTest {
@Test
public void testOnNotifications_expanded() {
final String message = "Where are you?";
Notification notification = mNotificationBuilder.setContentText(message).build();
when(mNotificationEntry.getNotification()).thenReturn(notification);
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
mSmartActionsHelper.suggestReplies(mNotificationEntry);
mSmartActionsHelper.onNotificationExpansionChanged(mNotificationEntry, false, true);
mSmartActionsHelper.suggest(createNotificationEntry());
mSmartActionsHelper.onNotificationExpansionChanged(createNotificationEntry(), false, true);
ArgumentCaptor<TextClassifierEvent> argumentCaptor =
ArgumentCaptor.forClass(TextClassifierEvent.class);
@@ -301,14 +369,41 @@ public class SmartActionHelperTest {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneOffset.systemDefault());
}
private List<ConversationActions.Message> getMessagesInRequest() {
mSmartActionsHelper.suggestReplies(mNotificationEntry);
private ConversationActions.Request runSuggestAndCaptureRequest() {
mSmartActionsHelper.suggest(createNotificationEntry());
ArgumentCaptor<ConversationActions.Request> argumentCaptor =
ArgumentCaptor.forClass(ConversationActions.Request.class);
verify(mTextClassifier).suggestConversationActions(argumentCaptor.capture());
ConversationActions.Request request = argumentCaptor.getValue();
return request.getConversation();
return argumentCaptor.getValue();
}
private Notification.Action createReplyAction() {
PendingIntent pendingIntent =
PendingIntent.getActivity(mContext, 0, new Intent(mContext, this.getClass()), 0);
RemoteInput remoteInput = new RemoteInput.Builder("result")
.setAllowFreeFormInput(true)
.build();
return new Notification.Action.Builder(
Icon.createWithResource(mContext.getResources(),
android.R.drawable.stat_sys_warning),
"Reply", pendingIntent)
.addRemoteInput(remoteInput)
.build();
}
private NotificationEntry createNotificationEntry() {
NotificationChannel channel =
new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT);
return new NotificationEntry(mIPackageManager, mStatusBarNotification, channel, mSmsHelper);
}
private Notification createMessageNotification() {
return mNotificationBuilder
.setContentText(MESSAGE)
.setCategory(Notification.CATEGORY_MESSAGE)
.setActions(createReplyAction())
.build();
}
private void assertTextClassifierEvent(