Remove local text classifier and related tests. am: 293bdf360a am: e94d4b04bc

Change-Id: I7ca571e879d61b0c76d0a2424fcfb91b06be5d74
This commit is contained in:
Automerger Merge Worker
2020-03-17 12:30:34 +00:00
38 changed files with 61 additions and 7015 deletions

View File

@ -790,10 +790,6 @@ java_library {
"libphonenumber-platform",
"tagsoup",
"rappor",
"libtextclassifier-java",
],
required: [
"libtextclassifier",
],
dxflags: ["--core-library"],
}

View File

@ -77,7 +77,6 @@ public class TextClassificationManagerPerfTest {
BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
textClassificationManager.getTextClassifier();
textClassificationManager.invalidateForTesting();
}
}
@ -90,7 +89,6 @@ public class TextClassificationManagerPerfTest {
BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
textClassificationManager.getTextClassifier();
textClassificationManager.invalidateForTesting();
}
}

View File

@ -41,7 +41,6 @@ import android.util.Slog;
import android.view.textclassifier.ConversationActions;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassificationSessionId;
@ -405,13 +404,6 @@ public abstract class TextClassifierService extends Service {
*/
@NonNull
public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) {
final TextClassificationManager tcm =
context.getSystemService(TextClassificationManager.class);
if (tcm == null) {
return TextClassifier.NO_OP;
}
TextClassificationConstants settings = new TextClassificationConstants();
if (settings.getUseDefaultTextClassifierAsDefaultImplementation()) {
final String defaultTextClassifierPackageName =
context.getPackageManager().getDefaultTextClassifierPackageName();
if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
@ -422,10 +414,9 @@ public abstract class TextClassifierService extends Service {
"The default text classifier itself should not call the"
+ "getDefaultTextClassifierImplementation() method.");
}
return tcm.getTextClassifier(TextClassifier.DEFAULT_SERVICE);
} else {
return tcm.getTextClassifier(TextClassifier.LOCAL);
}
final TextClassificationManager tcm =
context.getSystemService(TextClassificationManager.class);
return tcm.getTextClassifier(TextClassifier.DEFAULT_SYSTEM);
}
/** @hide **/

View File

@ -1,210 +0,0 @@
/*
* 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.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
import android.util.KeyValueListParser;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Parses the {@link Settings.Global#TEXT_CLASSIFIER_ACTION_MODEL_PARAMS} flag.
*
* @hide
*/
public final class ActionsModelParamsSupplier implements
Supplier<ActionsModelParamsSupplier.ActionsModelParams> {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
@VisibleForTesting
static final String KEY_REQUIRED_MODEL_VERSION = "required_model_version";
@VisibleForTesting
static final String KEY_REQUIRED_LOCALES = "required_locales";
@VisibleForTesting
static final String KEY_SERIALIZED_PRECONDITIONS = "serialized_preconditions";
private final Context mAppContext;
private final SettingsObserver mSettingsObserver;
private final Object mLock = new Object();
private final Runnable mOnChangedListener;
@Nullable
@GuardedBy("mLock")
private ActionsModelParams mActionsModelParams;
@GuardedBy("mLock")
private boolean mParsed = true;
public ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener) {
final Context appContext = Preconditions.checkNotNull(context).getApplicationContext();
// Some contexts don't have an app context.
mAppContext = appContext != null ? appContext : context;
mOnChangedListener = onChangedListener == null ? () -> {} : onChangedListener;
mSettingsObserver = new SettingsObserver(mAppContext, () -> {
synchronized (mLock) {
Log.v(TAG, "Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS is updated");
mParsed = true;
mOnChangedListener.run();
}
});
}
/**
* Returns the parsed actions params or {@link ActionsModelParams#INVALID} if the value is
* invalid.
*/
@Override
public ActionsModelParams get() {
synchronized (mLock) {
if (mParsed) {
mActionsModelParams = parse(mAppContext.getContentResolver());
mParsed = false;
}
}
return mActionsModelParams;
}
private ActionsModelParams parse(ContentResolver contentResolver) {
String settingStr = Settings.Global.getString(contentResolver,
Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS);
if (TextUtils.isEmpty(settingStr)) {
return ActionsModelParams.INVALID;
}
try {
KeyValueListParser keyValueListParser = new KeyValueListParser(',');
keyValueListParser.setString(settingStr);
int version = keyValueListParser.getInt(KEY_REQUIRED_MODEL_VERSION, -1);
if (version == -1) {
Log.w(TAG, "ActionsModelParams.Parse, invalid model version");
return ActionsModelParams.INVALID;
}
String locales = keyValueListParser.getString(KEY_REQUIRED_LOCALES, null);
if (locales == null) {
Log.w(TAG, "ActionsModelParams.Parse, invalid locales");
return ActionsModelParams.INVALID;
}
String serializedPreconditionsStr =
keyValueListParser.getString(KEY_SERIALIZED_PRECONDITIONS, null);
if (serializedPreconditionsStr == null) {
Log.w(TAG, "ActionsModelParams.Parse, invalid preconditions");
return ActionsModelParams.INVALID;
}
byte[] serializedPreconditions =
Base64.decode(serializedPreconditionsStr, Base64.NO_WRAP);
return new ActionsModelParams(version, locales, serializedPreconditions);
} catch (Throwable t) {
Log.e(TAG, "Invalid TEXT_CLASSIFIER_ACTION_MODEL_PARAMS, ignore", t);
}
return ActionsModelParams.INVALID;
}
@Override
protected void finalize() throws Throwable {
try {
mAppContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
} finally {
super.finalize();
}
}
/**
* Represents the parsed result.
*/
public static final class ActionsModelParams {
public static final ActionsModelParams INVALID =
new ActionsModelParams(-1, "", new byte[0]);
/**
* The required model version to apply {@code mSerializedPreconditions}.
*/
private final int mRequiredModelVersion;
/**
* The required model locales to apply {@code mSerializedPreconditions}.
*/
private final String mRequiredModelLocales;
/**
* The serialized params that will be applied to the model file, if all requirements are
* met. Do not modify.
*/
private final byte[] mSerializedPreconditions;
public ActionsModelParams(int requiredModelVersion, String requiredModelLocales,
byte[] serializedPreconditions) {
mRequiredModelVersion = requiredModelVersion;
mRequiredModelLocales = Preconditions.checkNotNull(requiredModelLocales);
mSerializedPreconditions = Preconditions.checkNotNull(serializedPreconditions);
}
/**
* Returns the serialized preconditions. Returns {@code null} if the the model in use does
* not meet all the requirements listed in the {@code ActionsModelParams} or the params
* are invalid.
*/
@Nullable
public byte[] getSerializedPreconditions(ModelFileManager.ModelFile modelInUse) {
if (this == INVALID) {
return null;
}
if (modelInUse.getVersion() != mRequiredModelVersion) {
Log.w(TAG, String.format(
"Not applying mSerializedPreconditions, required version=%d, actual=%d",
mRequiredModelVersion, modelInUse.getVersion()));
return null;
}
if (!Objects.equals(modelInUse.getSupportedLocalesStr(), mRequiredModelLocales)) {
Log.w(TAG, String.format(
"Not applying mSerializedPreconditions, required locales=%s, actual=%s",
mRequiredModelLocales, modelInUse.getSupportedLocalesStr()));
return null;
}
return mSerializedPreconditions;
}
}
private static final class SettingsObserver extends ContentObserver {
private final WeakReference<Runnable> mOnChangedListener;
SettingsObserver(Context appContext, Runnable listener) {
super(null);
mOnChangedListener = new WeakReference<>(listener);
appContext.getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS),
false /* notifyForDescendants */,
this);
}
public void onChange(boolean selfChange) {
if (mOnChangedListener.get() != null) {
mOnChangedListener.get().run();
}
}
}
}

View File

@ -1,234 +0,0 @@
/*
* 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.Nullable;
import android.app.Person;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Pair;
import android.view.textclassifier.intent.LabeledIntent;
import android.view.textclassifier.intent.TemplateIntentFactory;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.textclassifier.ActionsSuggestionsModel;
import com.google.android.textclassifier.RemoteActionTemplate;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Helper class for action suggestions.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class ActionsSuggestionsHelper {
private static final String TAG = "ActionsSuggestions";
private static final int USER_LOCAL = 0;
private static final int FIRST_NON_LOCAL_USER = 1;
private ActionsSuggestionsHelper() {}
/**
* Converts the messages to a list of native messages object that the model can understand.
* <p>
* User id encoding - local user is represented as 0, Other users are numbered according to
* how far before they spoke last time in the conversation. For example, considering this
* conversation:
* <ul>
* <li> User A: xxx
* <li> Local user: yyy
* <li> User B: zzz
* </ul>
* User A will be encoded as 2, user B will be encoded as 1 and local user will be encoded as 0.
*/
public static ActionsSuggestionsModel.ConversationMessage[] toNativeMessages(
List<ConversationActions.Message> messages,
Function<CharSequence, String> languageDetector) {
List<ConversationActions.Message> messagesWithText =
messages.stream()
.filter(message -> !TextUtils.isEmpty(message.getText()))
.collect(Collectors.toCollection(ArrayList::new));
if (messagesWithText.isEmpty()) {
return new ActionsSuggestionsModel.ConversationMessage[0];
}
Deque<ActionsSuggestionsModel.ConversationMessage> nativeMessages = new ArrayDeque<>();
PersonEncoder personEncoder = new PersonEncoder();
int size = messagesWithText.size();
for (int i = size - 1; i >= 0; i--) {
ConversationActions.Message message = messagesWithText.get(i);
long referenceTime = message.getReferenceTime() == null
? 0
: message.getReferenceTime().toInstant().toEpochMilli();
String timeZone = message.getReferenceTime() == null
? null
: message.getReferenceTime().getZone().getId();
nativeMessages.push(new ActionsSuggestionsModel.ConversationMessage(
personEncoder.encode(message.getAuthor()),
message.getText().toString(), referenceTime, timeZone,
languageDetector.apply(message.getText())));
}
return nativeMessages.toArray(
new ActionsSuggestionsModel.ConversationMessage[nativeMessages.size()]);
}
/**
* Returns the result id for logging.
*/
public static String createResultId(
Context context,
List<ConversationActions.Message> messages,
int modelVersion,
List<Locale> modelLocales) {
final StringJoiner localesJoiner = new StringJoiner(",");
for (Locale locale : modelLocales) {
localesJoiner.add(locale.toLanguageTag());
}
final String modelName = String.format(
Locale.US, "%s_v%d", localesJoiner.toString(), modelVersion);
final int hash = Objects.hash(
messages.stream().mapToInt(ActionsSuggestionsHelper::hashMessage),
context.getPackageName(),
System.currentTimeMillis());
return SelectionSessionLogger.SignatureParser.createSignature(
SelectionSessionLogger.CLASSIFIER_ID, modelName, hash);
}
/**
* Generated labeled intent from an action suggestion and return the resolved result.
*/
@Nullable
public static LabeledIntent.Result createLabeledIntentResult(
Context context,
TemplateIntentFactory templateIntentFactory,
ActionsSuggestionsModel.ActionSuggestion nativeSuggestion) {
RemoteActionTemplate[] remoteActionTemplates =
nativeSuggestion.getRemoteActionTemplates();
if (remoteActionTemplates == null) {
Log.w(TAG, "createRemoteAction: Missing template for type "
+ nativeSuggestion.getActionType());
return null;
}
List<LabeledIntent> labeledIntents = templateIntentFactory.create(remoteActionTemplates);
if (labeledIntents.isEmpty()) {
return null;
}
// Given that we only support implicit intent here, we should expect there is just one
// intent for each action type.
LabeledIntent.TitleChooser titleChooser =
ActionsSuggestionsHelper.createTitleChooser(nativeSuggestion.getActionType());
return labeledIntents.get(0).resolve(context, titleChooser, null);
}
/**
* Returns a {@link LabeledIntent.TitleChooser} for conversation actions use case.
*/
@Nullable
public static LabeledIntent.TitleChooser createTitleChooser(String actionType) {
if (ConversationAction.TYPE_OPEN_URL.equals(actionType)) {
return (labeledIntent, resolveInfo) -> {
if (resolveInfo.handleAllWebDataURI) {
return labeledIntent.titleWithEntity;
}
if ("android".equals(resolveInfo.activityInfo.packageName)) {
return labeledIntent.titleWithEntity;
}
return labeledIntent.titleWithoutEntity;
};
}
return null;
}
/**
* Returns a list of {@link ConversationAction}s that have 0 duplicates. Two actions are
* duplicates if they may look the same to users. This function assumes every
* ConversationActions with a non-null RemoteAction also have a non-null intent in the extras.
*/
public static List<ConversationAction> removeActionsWithDuplicates(
List<ConversationAction> conversationActions) {
// Ideally, we should compare title and icon here, but comparing icon is expensive and thus
// we use the component name of the target handler as the heuristic.
Map<Pair<String, String>, Integer> counter = new ArrayMap<>();
for (ConversationAction conversationAction : conversationActions) {
Pair<String, String> representation = getRepresentation(conversationAction);
if (representation == null) {
continue;
}
Integer existingCount = counter.getOrDefault(representation, 0);
counter.put(representation, existingCount + 1);
}
List<ConversationAction> result = new ArrayList<>();
for (ConversationAction conversationAction : conversationActions) {
Pair<String, String> representation = getRepresentation(conversationAction);
if (representation == null || counter.getOrDefault(representation, 0) == 1) {
result.add(conversationAction);
}
}
return result;
}
@Nullable
private static Pair<String, String> getRepresentation(
ConversationAction conversationAction) {
RemoteAction remoteAction = conversationAction.getAction();
if (remoteAction == null) {
return null;
}
Intent actionIntent = ExtrasUtils.getActionIntent(conversationAction.getExtras());
ComponentName componentName = actionIntent.getComponent();
// Action without a component name will be considered as from the same app.
String packageName = componentName == null ? null : componentName.getPackageName();
return new Pair<>(
conversationAction.getAction().getTitle().toString(), packageName);
}
private static final class PersonEncoder {
private final Map<Person, Integer> mMapping = new ArrayMap<>();
private int mNextUserId = FIRST_NON_LOCAL_USER;
private int encode(Person person) {
if (ConversationActions.Message.PERSON_USER_SELF.equals(person)) {
return USER_LOCAL;
}
Integer result = mMapping.get(person);
if (result == null) {
mMapping.put(person, mNextUserId);
result = mNextUserId;
mNextUserId++;
}
return result;
}
}
private static int hashMessage(ConversationActions.Message message) {
return Objects.hash(message.getAuthor(), message.getText(), message.getReferenceTime());
}
}

View File

@ -19,15 +19,9 @@ package android.view.textclassifier;
import android.annotation.Nullable;
import android.app.RemoteAction;
import android.content.Intent;
import android.icu.util.ULocale;
import android.os.Bundle;
import com.android.internal.util.ArrayUtils;
import com.google.android.textclassifier.AnnotatorModel;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for inserting and retrieving data in TextClassifier request/response extras.
@ -37,52 +31,19 @@ import java.util.List;
public final class ExtrasUtils {
// Keys for response objects.
private static final String SERIALIZED_ENTITIES_DATA = "serialized-entities-data";
private static final String ENTITIES_EXTRAS = "entities-extras";
private static final String ACTION_INTENT = "action-intent";
private static final String ACTIONS_INTENTS = "actions-intents";
private static final String FOREIGN_LANGUAGE = "foreign-language";
private static final String ENTITY_TYPE = "entity-type";
private static final String SCORE = "score";
private static final String MODEL_VERSION = "model-version";
private static final String MODEL_NAME = "model-name";
private static final String TEXT_LANGUAGES = "text-languages";
private static final String ENTITIES = "entities";
// Keys for request objects.
private static final String IS_SERIALIZED_ENTITY_DATA_ENABLED =
"is-serialized-entity-data-enabled";
private ExtrasUtils() {}
/**
* Bundles and returns foreign language detection information for TextClassifier responses.
*/
static Bundle createForeignLanguageExtra(
String language, float score, int modelVersion) {
final Bundle bundle = new Bundle();
bundle.putString(ENTITY_TYPE, language);
bundle.putFloat(SCORE, score);
bundle.putInt(MODEL_VERSION, modelVersion);
bundle.putString(MODEL_NAME, "langId_v" + modelVersion);
return bundle;
}
/**
* Stores {@code extra} as foreign language information in TextClassifier response object's
* extras {@code container}.
*
* @see #getForeignLanguageExtra(TextClassification)
*/
static void putForeignLanguageExtra(Bundle container, Bundle extra) {
container.putParcelable(FOREIGN_LANGUAGE, extra);
private ExtrasUtils() {
}
/**
* Returns foreign language detection information contained in the TextClassification object.
* responses.
*
* @see #putForeignLanguageExtra(Bundle, Bundle)
*/
@Nullable
public static Bundle getForeignLanguageExtra(@Nullable TextClassification classification) {
@ -92,72 +53,6 @@ public final class ExtrasUtils {
return classification.getExtras().getBundle(FOREIGN_LANGUAGE);
}
/**
* @see #getTopLanguage(Intent)
*/
static void putTopLanguageScores(Bundle container, EntityConfidence languageScores) {
final int maxSize = Math.min(3, languageScores.getEntities().size());
final String[] languages = languageScores.getEntities().subList(0, maxSize)
.toArray(new String[0]);
final float[] scores = new float[languages.length];
for (int i = 0; i < languages.length; i++) {
scores[i] = languageScores.getConfidenceScore(languages[i]);
}
container.putStringArray(ENTITY_TYPE, languages);
container.putFloatArray(SCORE, scores);
}
/**
* @see #putTopLanguageScores(Bundle, EntityConfidence)
*/
@Nullable
public static ULocale getTopLanguage(@Nullable Intent intent) {
if (intent == null) {
return null;
}
final Bundle tcBundle = intent.getBundleExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER);
if (tcBundle == null) {
return null;
}
final Bundle textLanguagesExtra = tcBundle.getBundle(TEXT_LANGUAGES);
if (textLanguagesExtra == null) {
return null;
}
final String[] languages = textLanguagesExtra.getStringArray(ENTITY_TYPE);
final float[] scores = textLanguagesExtra.getFloatArray(SCORE);
if (languages == null || scores == null
|| languages.length == 0 || languages.length != scores.length) {
return null;
}
int highestScoringIndex = 0;
for (int i = 1; i < languages.length; i++) {
if (scores[highestScoringIndex] < scores[i]) {
highestScoringIndex = i;
}
}
return ULocale.forLanguageTag(languages[highestScoringIndex]);
}
public static void putTextLanguagesExtra(Bundle container, Bundle extra) {
container.putBundle(TEXT_LANGUAGES, extra);
}
/**
* Stores {@code actionIntents} information in TextClassifier response object's extras
* {@code container}.
*/
static void putActionsIntents(Bundle container, ArrayList<Intent> actionsIntents) {
container.putParcelableArrayList(ACTIONS_INTENTS, actionsIntents);
}
/**
* Stores {@code actionIntents} information in TextClassifier response object's extras
* {@code container}.
*/
public static void putActionIntent(Bundle container, @Nullable Intent actionIntent) {
container.putParcelable(ACTION_INTENT, actionIntent);
}
/**
* Returns {@code actionIntent} information contained in a TextClassifier response object.
*/
@ -166,48 +61,6 @@ public final class ExtrasUtils {
return container.getParcelable(ACTION_INTENT);
}
/**
* Stores serialized entity data information in TextClassifier response object's extras
* {@code container}.
*/
public static void putSerializedEntityData(
Bundle container, @Nullable byte[] serializedEntityData) {
container.putByteArray(SERIALIZED_ENTITIES_DATA, serializedEntityData);
}
/**
* Returns serialized entity data information contained in a TextClassifier response
* object.
*/
@Nullable
public static byte[] getSerializedEntityData(Bundle container) {
return container.getByteArray(SERIALIZED_ENTITIES_DATA);
}
/**
* Stores {@code entities} information in TextClassifier response object's extras
* {@code container}.
*
* @see {@link #getCopyText(Bundle)}
*/
public static void putEntitiesExtras(Bundle container, @Nullable Bundle entitiesExtras) {
container.putParcelable(ENTITIES_EXTRAS, entitiesExtras);
}
/**
* Returns {@code entities} information contained in a TextClassifier response object.
*
* @see {@link #putEntitiesExtras(Bundle, Bundle)}
*/
@Nullable
public static String getCopyText(Bundle container) {
Bundle entitiesExtras = container.getParcelable(ENTITIES_EXTRAS);
if (entitiesExtras == null) {
return null;
}
return entitiesExtras.getString("text");
}
/**
* Returns {@code actionIntents} information contained in the TextClassification object.
*/
@ -224,7 +77,7 @@ public final class ExtrasUtils {
* action string, {@code intentAction}.
*/
@Nullable
public static RemoteAction findAction(
private static RemoteAction findAction(
@Nullable TextClassification classification, @Nullable String intentAction) {
if (classification == null || intentAction == null) {
return null;
@ -283,53 +136,4 @@ public final class ExtrasUtils {
}
return extra.getString(MODEL_NAME);
}
/**
* Stores the entities from {@link AnnotatorModel.ClassificationResult} in {@code container}.
*/
public static void putEntities(
Bundle container,
@Nullable AnnotatorModel.ClassificationResult[] classifications) {
if (ArrayUtils.isEmpty(classifications)) {
return;
}
ArrayList<Bundle> entitiesBundle = new ArrayList<>();
for (AnnotatorModel.ClassificationResult classification : classifications) {
if (classification == null) {
continue;
}
Bundle entityBundle = new Bundle();
entityBundle.putString(ENTITY_TYPE, classification.getCollection());
entityBundle.putByteArray(
SERIALIZED_ENTITIES_DATA,
classification.getSerializedEntityData());
entitiesBundle.add(entityBundle);
}
if (!entitiesBundle.isEmpty()) {
container.putParcelableArrayList(ENTITIES, entitiesBundle);
}
}
/**
* Returns a list of entities contained in the {@code extra}.
*/
@Nullable
public static List<Bundle> getEntities(Bundle container) {
return container.getParcelableArrayList(ENTITIES);
}
/**
* Whether the annotator should populate serialized entity data into the result object.
*/
public static boolean isSerializedEntityDataEnabled(TextLinks.Request request) {
return request.getExtras().getBoolean(IS_SERIALIZED_ENTITY_DATA_ENABLED);
}
/**
* To indicate whether the annotator should populate serialized entity data in the result
* object.
*/
public static void putIsSerializedEntityDataEnabled(Bundle bundle, boolean isEnabled) {
bundle.putBoolean(IS_SERIALIZED_ENTITY_DATA_ENABLED, isEnabled);
}
}

View File

@ -1,160 +0,0 @@
/*
* Copyright (C) 2017 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.Nullable;
import android.metrics.LogMaker;
import android.util.ArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
/**
* A helper for logging calls to generateLinks.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class GenerateLinksLogger {
private static final String LOG_TAG = "GenerateLinksLogger";
private static final String ZERO = "0";
private final MetricsLogger mMetricsLogger;
private final Random mRng;
private final int mSampleRate;
/**
* @param sampleRate the rate at which log events are written. (e.g. 100 means there is a 0.01
* chance that a call to logGenerateLinks results in an event being written).
* To write all events, pass 1.
*/
public GenerateLinksLogger(int sampleRate) {
mSampleRate = sampleRate;
mRng = new Random(System.nanoTime());
mMetricsLogger = new MetricsLogger();
}
@VisibleForTesting
public GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger) {
mSampleRate = sampleRate;
mRng = new Random(System.nanoTime());
mMetricsLogger = metricsLogger;
}
/** Logs statistics about a call to generateLinks. */
public void logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName,
long latencyMs) {
Objects.requireNonNull(text);
Objects.requireNonNull(links);
Objects.requireNonNull(callingPackageName);
if (!shouldLog()) {
return;
}
// Always populate the total stats, and per-entity stats for each entity type detected.
final LinkifyStats totalStats = new LinkifyStats();
final Map<String, LinkifyStats> perEntityTypeStats = new ArrayMap<>();
for (TextLinks.TextLink link : links.getLinks()) {
if (link.getEntityCount() == 0) continue;
final String entityType = link.getEntity(0);
if (entityType == null
|| TextClassifier.TYPE_OTHER.equals(entityType)
|| TextClassifier.TYPE_UNKNOWN.equals(entityType)) {
continue;
}
totalStats.countLink(link);
perEntityTypeStats.computeIfAbsent(entityType, k -> new LinkifyStats()).countLink(link);
}
final String callId = UUID.randomUUID().toString();
writeStats(callId, callingPackageName, null, totalStats, text, latencyMs);
for (Map.Entry<String, LinkifyStats> entry : perEntityTypeStats.entrySet()) {
writeStats(callId, callingPackageName, entry.getKey(), entry.getValue(), text,
latencyMs);
}
}
/**
* Returns whether this particular event should be logged.
*
* Sampling is used to reduce the amount of logging data generated.
**/
private boolean shouldLog() {
if (mSampleRate <= 1) {
return true;
} else {
return mRng.nextInt(mSampleRate) == 0;
}
}
/** Writes a log event for the given stats. */
private void writeStats(String callId, String callingPackageName, @Nullable String entityType,
LinkifyStats stats, CharSequence text, long latencyMs) {
final LogMaker log = new LogMaker(MetricsEvent.TEXT_CLASSIFIER_GENERATE_LINKS)
.setPackageName(callingPackageName)
.addTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID, callId)
.addTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS, stats.mNumLinks)
.addTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH, stats.mNumLinksTextLength)
.addTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH, text.length())
.addTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY, latencyMs);
if (entityType != null) {
log.addTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE, entityType);
}
mMetricsLogger.write(log);
debugLog(log);
}
private static void debugLog(LogMaker log) {
if (!Log.ENABLE_FULL_LOGGING) {
return;
}
final String callId = Objects.toString(
log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
final String entityType = Objects.toString(
log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "ANY_ENTITY");
final int numLinks = Integer.parseInt(
Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS), ZERO));
final int linkLength = Integer.parseInt(
Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH), ZERO));
final int textLength = Integer.parseInt(
Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH), ZERO));
final int latencyMs = Integer.parseInt(
Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
Log.v(LOG_TAG,
String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
}
/** Helper class for storing per-entity type statistics. */
private static final class LinkifyStats {
int mNumLinks;
int mNumLinksTextLength;
void countLink(TextLinks.TextLink link) {
mNumLinks += 1;
mNumLinksTextLength += link.getEnd() - link.getStart();
}
}
}

View File

@ -32,7 +32,7 @@ public final class Log {
* false: Limits logging to debug level.
*/
static final boolean ENABLE_FULL_LOGGING =
android.util.Log.isLoggable(TextClassifier.DEFAULT_LOG_TAG, android.util.Log.VERBOSE);
android.util.Log.isLoggable(TextClassifier.LOG_TAG, android.util.Log.VERBOSE);
private Log() {
}

View File

@ -1,301 +0,0 @@
/*
* 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 android.view.textclassifier.TextClassifier.DEFAULT_LOG_TAG;
import android.annotation.Nullable;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Manages model files that are listed by the model files supplier.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class ModelFileManager {
private final Object mLock = new Object();
private final Supplier<List<ModelFile>> mModelFileSupplier;
private List<ModelFile> mModelFiles;
public ModelFileManager(Supplier<List<ModelFile>> modelFileSupplier) {
mModelFileSupplier = Objects.requireNonNull(modelFileSupplier);
}
/**
* Returns an unmodifiable list of model files listed by the given model files supplier.
* <p>
* The result is cached.
*/
public List<ModelFile> listModelFiles() {
synchronized (mLock) {
if (mModelFiles == null) {
mModelFiles = Collections.unmodifiableList(mModelFileSupplier.get());
}
return mModelFiles;
}
}
/**
* Returns the best model file for the given localelist, {@code null} if nothing is found.
*
* @param localeList the required locales, use {@code null} if there is no preference.
*/
public ModelFile findBestModelFile(@Nullable LocaleList localeList) {
final String languages = localeList == null || localeList.isEmpty()
? LocaleList.getDefault().toLanguageTags()
: localeList.toLanguageTags();
final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
ModelFile bestModel = null;
for (ModelFile model : listModelFiles()) {
if (model.isAnyLanguageSupported(languageRangeList)) {
if (model.isPreferredTo(bestModel)) {
bestModel = model;
}
}
}
return bestModel;
}
/**
* Default implementation of the model file supplier.
*/
public static final class ModelFileSupplierImpl implements Supplier<List<ModelFile>> {
private final File mUpdatedModelFile;
private final File mFactoryModelDir;
private final Pattern mModelFilenamePattern;
private final Function<Integer, Integer> mVersionSupplier;
private final Function<Integer, String> mSupportedLocalesSupplier;
public ModelFileSupplierImpl(
File factoryModelDir,
String factoryModelFileNameRegex,
File updatedModelFile,
Function<Integer, Integer> versionSupplier,
Function<Integer, String> supportedLocalesSupplier) {
mUpdatedModelFile = Objects.requireNonNull(updatedModelFile);
mFactoryModelDir = Objects.requireNonNull(factoryModelDir);
mModelFilenamePattern = Pattern.compile(
Objects.requireNonNull(factoryModelFileNameRegex));
mVersionSupplier = Objects.requireNonNull(versionSupplier);
mSupportedLocalesSupplier = Objects.requireNonNull(supportedLocalesSupplier);
}
@Override
public List<ModelFile> get() {
final List<ModelFile> modelFiles = new ArrayList<>();
// The update model has the highest precedence.
if (mUpdatedModelFile.exists()) {
final ModelFile updatedModel = createModelFile(mUpdatedModelFile);
if (updatedModel != null) {
modelFiles.add(updatedModel);
}
}
// Factory models should never have overlapping locales, so the order doesn't matter.
if (mFactoryModelDir.exists() && mFactoryModelDir.isDirectory()) {
final File[] files = mFactoryModelDir.listFiles();
for (File file : files) {
final Matcher matcher = mModelFilenamePattern.matcher(file.getName());
if (matcher.matches() && file.isFile()) {
final ModelFile model = createModelFile(file);
if (model != null) {
modelFiles.add(model);
}
}
}
}
return modelFiles;
}
/** Returns null if the path did not point to a compatible model. */
@Nullable
private ModelFile createModelFile(File file) {
if (!file.exists()) {
return null;
}
ParcelFileDescriptor modelFd = null;
try {
modelFd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
if (modelFd == null) {
return null;
}
final int modelFdInt = modelFd.getFd();
final int version = mVersionSupplier.apply(modelFdInt);
final String supportedLocalesStr = mSupportedLocalesSupplier.apply(modelFdInt);
if (supportedLocalesStr.isEmpty()) {
Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
return null;
}
final List<Locale> supportedLocales = new ArrayList<>();
for (String langTag : supportedLocalesStr.split(",")) {
supportedLocales.add(Locale.forLanguageTag(langTag));
}
return new ModelFile(
file,
version,
supportedLocales,
supportedLocalesStr,
ModelFile.LANGUAGE_INDEPENDENT.equals(supportedLocalesStr));
} catch (FileNotFoundException e) {
Log.e(DEFAULT_LOG_TAG, "Failed to find " + file.getAbsolutePath(), e);
return null;
} finally {
maybeCloseAndLogError(modelFd);
}
}
/**
* Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
*/
private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
if (fd == null) {
return;
}
try {
fd.close();
} catch (IOException e) {
Log.e(DEFAULT_LOG_TAG, "Error closing file.", e);
}
}
}
/**
* Describes TextClassifier model files on disk.
*/
public static final class ModelFile {
public static final String LANGUAGE_INDEPENDENT = "*";
private final File mFile;
private final int mVersion;
private final List<Locale> mSupportedLocales;
private final String mSupportedLocalesStr;
private final boolean mLanguageIndependent;
public ModelFile(File file, int version, List<Locale> supportedLocales,
String supportedLocalesStr,
boolean languageIndependent) {
mFile = Objects.requireNonNull(file);
mVersion = version;
mSupportedLocales = Objects.requireNonNull(supportedLocales);
mSupportedLocalesStr = Objects.requireNonNull(supportedLocalesStr);
mLanguageIndependent = languageIndependent;
}
/** Returns the absolute path to the model file. */
public String getPath() {
return mFile.getAbsolutePath();
}
/** Returns a name to use for id generation, effectively the name of the model file. */
public String getName() {
return mFile.getName();
}
/** Returns the version tag in the model's metadata. */
public int getVersion() {
return mVersion;
}
/** Returns whether the language supports any language in the given ranges. */
public boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
Objects.requireNonNull(languageRanges);
return mLanguageIndependent || Locale.lookup(languageRanges, mSupportedLocales) != null;
}
/** Returns an immutable lists of supported locales. */
public List<Locale> getSupportedLocales() {
return Collections.unmodifiableList(mSupportedLocales);
}
/** Returns the original supported locals string read from the model file. */
public String getSupportedLocalesStr() {
return mSupportedLocalesStr;
}
/**
* Returns if this model file is preferred to the given one.
*/
public boolean isPreferredTo(@Nullable ModelFile model) {
// A model is preferred to no model.
if (model == null) {
return true;
}
// A language-specific model is preferred to a language independent
// model.
if (!mLanguageIndependent && model.mLanguageIndependent) {
return true;
}
if (mLanguageIndependent && !model.mLanguageIndependent) {
return false;
}
// A higher-version model is preferred.
if (mVersion > model.getVersion()) {
return true;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(getPath());
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof ModelFile) {
final ModelFile otherModel = (ModelFile) other;
return TextUtils.equals(getPath(), otherModel.getPath());
}
return false;
}
@Override
public String toString() {
final StringJoiner localesJoiner = new StringJoiner(",");
for (Locale locale : mSupportedLocales) {
localesJoiner.add(locale.toLanguageTag());
}
return String.format(Locale.US,
"ModelFile { path=%s name=%s version=%d locales=%s }",
getPath(), getName(), mVersion, localesJoiner.toString());
}
}
}

View File

@ -158,7 +158,6 @@ public final class SelectionEvent implements Parcelable {
mEventType = in.readInt();
mEntityType = in.readString();
mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
// TODO: remove mPackageName once aiai does not need it
mPackageName = in.readString();
mWidgetType = in.readString();
mInvocationMethod = in.readInt();
@ -186,7 +185,6 @@ public final class SelectionEvent implements Parcelable {
if (mWidgetVersion != null) {
dest.writeString(mWidgetVersion);
}
// TODO: remove mPackageName once aiai does not need it
dest.writeString(mPackageName);
dest.writeString(mWidgetType);
dest.writeInt(mInvocationMethod);
@ -406,7 +404,7 @@ public final class SelectionEvent implements Parcelable {
*/
@NonNull
public String getPackageName() {
return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : "";
return mPackageName;
}
/**

View File

@ -16,251 +16,24 @@
package android.view.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.metrics.LogMaker;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.text.BreakIterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;
/**
* A helper for logging selection session events.
*
* @hide
*/
public final class SelectionSessionLogger {
private static final String LOG_TAG = "SelectionSessionLogger";
static final String CLASSIFIER_ID = "androidtc";
private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS;
private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX;
private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE;
private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION;
private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE;
private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START;
private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END;
private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START;
private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END;
private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID;
private static final String ZERO = "0";
private static final String UNKNOWN = "unknown";
private final MetricsLogger mMetricsLogger;
public SelectionSessionLogger() {
mMetricsLogger = new MetricsLogger();
}
@VisibleForTesting
public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) {
mMetricsLogger = Objects.requireNonNull(metricsLogger);
}
/** Emits a selection event to the logs. */
public void writeEvent(@NonNull SelectionEvent event) {
Objects.requireNonNull(event);
final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
.setType(getLogType(event))
.setSubtype(getLogSubType(event))
.setPackageName(event.getPackageName())
.addTaggedData(START_EVENT_DELTA, event.getDurationSinceSessionStart())
.addTaggedData(PREV_EVENT_DELTA, event.getDurationSincePreviousEvent())
.addTaggedData(INDEX, event.getEventIndex())
.addTaggedData(WIDGET_TYPE, event.getWidgetType())
.addTaggedData(WIDGET_VERSION, event.getWidgetVersion())
.addTaggedData(ENTITY_TYPE, event.getEntityType())
.addTaggedData(EVENT_START, event.getStart())
.addTaggedData(EVENT_END, event.getEnd());
if (isPlatformLocalTextClassifierSmartSelection(event.getResultId())) {
// Ensure result id and smart indices are only set for events with smart selection from
// the platform's textclassifier.
log.addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getResultId()))
.addTaggedData(SMART_START, event.getSmartStart())
.addTaggedData(SMART_END, event.getSmartEnd());
}
if (event.getSessionId() != null) {
log.addTaggedData(SESSION_ID, event.getSessionId().getValue());
}
mMetricsLogger.write(log);
debugLog(log);
}
private static int getLogType(SelectionEvent event) {
switch (event.getEventType()) {
case SelectionEvent.ACTION_OVERTYPE:
return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE;
case SelectionEvent.ACTION_COPY:
return MetricsEvent.ACTION_TEXT_SELECTION_COPY;
case SelectionEvent.ACTION_PASTE:
return MetricsEvent.ACTION_TEXT_SELECTION_PASTE;
case SelectionEvent.ACTION_CUT:
return MetricsEvent.ACTION_TEXT_SELECTION_CUT;
case SelectionEvent.ACTION_SHARE:
return MetricsEvent.ACTION_TEXT_SELECTION_SHARE;
case SelectionEvent.ACTION_SMART_SHARE:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
case SelectionEvent.ACTION_DRAG:
return MetricsEvent.ACTION_TEXT_SELECTION_DRAG;
case SelectionEvent.ACTION_ABANDON:
return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON;
case SelectionEvent.ACTION_OTHER:
return MetricsEvent.ACTION_TEXT_SELECTION_OTHER;
case SelectionEvent.ACTION_SELECT_ALL:
return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL;
case SelectionEvent.ACTION_RESET:
return MetricsEvent.ACTION_TEXT_SELECTION_RESET;
case SelectionEvent.EVENT_SELECTION_STARTED:
return MetricsEvent.ACTION_TEXT_SELECTION_START;
case SelectionEvent.EVENT_SELECTION_MODIFIED:
return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY;
case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE;
case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI;
case SelectionEvent.EVENT_AUTO_SELECTION:
return MetricsEvent.ACTION_TEXT_SELECTION_AUTO;
default:
return MetricsEvent.VIEW_UNKNOWN;
}
}
private static int getLogSubType(SelectionEvent event) {
switch (event.getInvocationMethod()) {
case SelectionEvent.INVOCATION_MANUAL:
return MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL;
case SelectionEvent.INVOCATION_LINK:
return MetricsEvent.TEXT_SELECTION_INVOCATION_LINK;
default:
return MetricsEvent.TEXT_SELECTION_INVOCATION_UNKNOWN;
}
}
private static String getLogTypeString(int logType) {
switch (logType) {
case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE:
return "OVERTYPE";
case MetricsEvent.ACTION_TEXT_SELECTION_COPY:
return "COPY";
case MetricsEvent.ACTION_TEXT_SELECTION_PASTE:
return "PASTE";
case MetricsEvent.ACTION_TEXT_SELECTION_CUT:
return "CUT";
case MetricsEvent.ACTION_TEXT_SELECTION_SHARE:
return "SHARE";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
return "SMART_SHARE";
case MetricsEvent.ACTION_TEXT_SELECTION_DRAG:
return "DRAG";
case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON:
return "ABANDON";
case MetricsEvent.ACTION_TEXT_SELECTION_OTHER:
return "OTHER";
case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL:
return "SELECT_ALL";
case MetricsEvent.ACTION_TEXT_SELECTION_RESET:
return "RESET";
case MetricsEvent.ACTION_TEXT_SELECTION_START:
return "SELECTION_STARTED";
case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY:
return "SELECTION_MODIFIED";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE:
return "SMART_SELECTION_SINGLE";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI:
return "SMART_SELECTION_MULTI";
case MetricsEvent.ACTION_TEXT_SELECTION_AUTO:
return "AUTO_SELECTION";
default:
return UNKNOWN;
}
}
private static String getLogSubTypeString(int logSubType) {
switch (logSubType) {
case MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL:
return "MANUAL";
case MetricsEvent.TEXT_SELECTION_INVOCATION_LINK:
return "LINK";
default:
return UNKNOWN;
}
}
// Keep this in sync with the ResultIdUtils in libtextclassifier.
private static final String CLASSIFIER_ID = "androidtc";
static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
return SelectionSessionLogger.CLASSIFIER_ID.equals(
SelectionSessionLogger.SignatureParser.getClassifierId(signature));
}
private static void debugLog(LogMaker log) {
if (!Log.ENABLE_FULL_LOGGING) {
return;
}
final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN);
final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), "");
final String widget = widgetVersion.isEmpty()
? widgetType : widgetType + "-" + widgetVersion;
final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO));
if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) {
String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), "");
sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1);
Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId));
}
final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN);
final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN);
final String type = getLogTypeString(log.getType());
final String subType = getLogSubTypeString(log.getSubtype());
final int smartStart = Integer.parseInt(
Objects.toString(log.getTaggedData(SMART_START), ZERO));
final int smartEnd = Integer.parseInt(
Objects.toString(log.getTaggedData(SMART_END), ZERO));
final int eventStart = Integer.parseInt(
Objects.toString(log.getTaggedData(EVENT_START), ZERO));
final int eventEnd = Integer.parseInt(
Objects.toString(log.getTaggedData(EVENT_END), ZERO));
Log.v(LOG_TAG,
String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd,
widget, model));
}
/**
* Returns a token iterator for tokenizing text for logging purposes.
*/
public static BreakIterator getTokenIterator(@NonNull Locale locale) {
return BreakIterator.getWordInstance(Objects.requireNonNull(locale));
}
/**
* Creates a string id that may be used to identify a TextClassifier result.
*/
public static String createId(
String text, int start, int end, Context context, int modelVersion,
List<Locale> locales) {
Objects.requireNonNull(text);
Objects.requireNonNull(context);
Objects.requireNonNull(locales);
final StringJoiner localesJoiner = new StringJoiner(",");
for (Locale locale : locales) {
localesJoiner.add(locale.toLanguageTag());
}
final String modelName = String.format(Locale.US, "%s_v%d", localesJoiner.toString(),
modelVersion);
final int hash = Objects.hash(text, start, end, context.getPackageName());
return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash);
}
/**
* Helper for creating and parsing string ids for
* {@link android.view.textclassifier.TextClassifierImpl}.
@ -268,10 +41,6 @@ public final class SelectionSessionLogger {
@VisibleForTesting
public static final class SignatureParser {
static String createSignature(String classifierId, String modelName, int hash) {
return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
}
static String getClassifierId(@Nullable String signature) {
if (signature == null) {
return "";
@ -282,29 +51,5 @@ public final class SelectionSessionLogger {
}
return "";
}
static String getModelName(@Nullable String signature) {
if (signature == null) {
return "";
}
final int start = signature.indexOf("|") + 1;
final int end = signature.indexOf("|", start);
if (start >= 1 && end >= start) {
return signature.substring(start, end);
}
return "";
}
static int getHash(@Nullable String signature) {
if (signature == null) {
return 0;
}
final int index1 = signature.indexOf("|");
final int index2 = signature.indexOf("|", index1);
if (index2 > 0) {
return Integer.parseInt(signature.substring(index2));
}
return 0;
}
}
}

View File

@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit;
@VisibleForTesting(visibility = Visibility.PACKAGE)
public final class SystemTextClassifier implements TextClassifier {
private static final String LOG_TAG = "SystemTextClassifier";
private static final String LOG_TAG = TextClassifier.LOG_TAG;
private final ITextClassifierService mManagerService;
private final TextClassificationConstants mSettings;

View File

@ -44,8 +44,6 @@ import android.view.textclassifier.TextClassifier.Utils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.google.android.textclassifier.AnnotatorModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.ZonedDateTime;
@ -327,9 +325,6 @@ public final class TextClassification implements Parcelable {
@NonNull private List<RemoteAction> mActions = new ArrayList<>();
@NonNull private final Map<String, Float> mTypeScoreMap = new ArrayMap<>();
@NonNull
private final Map<String, AnnotatorModel.ClassificationResult> mClassificationResults =
new ArrayMap<>();
@Nullable private String mText;
@Nullable private Drawable mLegacyIcon;
@Nullable private String mLegacyLabel;
@ -362,36 +357,7 @@ public final class TextClassification implements Parcelable {
public Builder setEntityType(
@NonNull @EntityType String type,
@FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
setEntityType(type, confidenceScore, null);
return this;
}
/**
* @see #setEntityType(String, float)
*
* @hide
*/
@NonNull
public Builder setEntityType(AnnotatorModel.ClassificationResult classificationResult) {
setEntityType(
classificationResult.getCollection(),
classificationResult.getScore(),
classificationResult);
return this;
}
/**
* @see #setEntityType(String, float)
*
* @hide
*/
@NonNull
private Builder setEntityType(
@NonNull @EntityType String type,
@FloatRange(from = 0.0, to = 1.0) float confidenceScore,
@Nullable AnnotatorModel.ClassificationResult classificationResult) {
mTypeScoreMap.put(type, confidenceScore);
mClassificationResults.put(type, classificationResult);
return this;
}
@ -517,25 +483,7 @@ public final class TextClassification implements Parcelable {
EntityConfidence entityConfidence = new EntityConfidence(mTypeScoreMap);
return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent,
mLegacyOnClickListener, mActions, entityConfidence, mId,
buildExtras(entityConfidence));
}
private Bundle buildExtras(EntityConfidence entityConfidence) {
final Bundle extras = mExtras == null ? new Bundle() : mExtras;
if (mActionIntents.stream().anyMatch(Objects::nonNull)) {
ExtrasUtils.putActionsIntents(extras, mActionIntents);
}
if (mForeignLanguageExtra != null) {
ExtrasUtils.putForeignLanguageExtra(extras, mForeignLanguageExtra);
}
List<String> sortedTypes = entityConfidence.getEntities();
ArrayList<AnnotatorModel.ClassificationResult> sortedEntities = new ArrayList<>();
for (String type : sortedTypes) {
sortedEntities.add(mClassificationResults.get(type));
}
ExtrasUtils.putEntities(
extras, sortedEntities.toArray(new AnnotatorModel.ClassificationResult[0]));
return extras.isEmpty() ? Bundle.EMPTY : extras;
mExtras == null ? Bundle.EMPTY : mExtras);
}
}

View File

@ -17,16 +17,11 @@
package android.view.textclassifier;
import android.annotation.Nullable;
import android.content.Context;
import android.provider.DeviceConfig;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* TextClassifier specific settings.
*
@ -41,8 +36,6 @@ import java.util.List;
*/
// TODO: Rename to TextClassifierSettings.
public final class TextClassificationConstants {
private static final String DELIMITER = ":";
/**
* Whether the smart linkify feature is enabled.
*/
@ -60,7 +53,6 @@ public final class TextClassificationConstants {
* Enable smart selection without a visible UI changes.
*/
private static final String MODEL_DARK_LAUNCH_ENABLED = "model_dark_launch_enabled";
/**
* Whether the smart selection feature is enabled.
*/
@ -74,89 +66,11 @@ public final class TextClassificationConstants {
*/
private static final String SMART_SELECT_ANIMATION_ENABLED =
"smart_select_animation_enabled";
/**
* Max length of text that suggestSelection can accept.
*/
@VisibleForTesting
static final String SUGGEST_SELECTION_MAX_RANGE_LENGTH =
"suggest_selection_max_range_length";
/**
* Max length of text that classifyText can accept.
*/
private static final String CLASSIFY_TEXT_MAX_RANGE_LENGTH = "classify_text_max_range_length";
/**
* Max length of text that generateLinks can accept.
*/
private static final String GENERATE_LINKS_MAX_TEXT_LENGTH = "generate_links_max_text_length";
/**
* Sampling rate for generateLinks logging.
*/
private static final String GENERATE_LINKS_LOG_SAMPLE_RATE =
"generate_links_log_sample_rate";
/**
* A colon(:) separated string that specifies the default entities types for
* generateLinks when hint is not given.
*/
@VisibleForTesting
static final String ENTITY_LIST_DEFAULT = "entity_list_default";
/**
* A colon(:) separated string that specifies the default entities types for
* generateLinks when the text is in a not editable UI widget.
*/
private static final String ENTITY_LIST_NOT_EDITABLE = "entity_list_not_editable";
/**
* A colon(:) separated string that specifies the default entities types for
* generateLinks when the text is in an editable UI widget.
*/
private static final String ENTITY_LIST_EDITABLE = "entity_list_editable";
/**
* A colon(:) separated string that specifies the default action types for
* suggestConversationActions when the suggestions are used in an app.
*/
private static final String IN_APP_CONVERSATION_ACTION_TYPES_DEFAULT =
"in_app_conversation_action_types_default";
/**
* A colon(:) separated string that specifies the default action types for
* suggestConversationActions when the suggestions are used in a notification.
*/
private static final String NOTIFICATION_CONVERSATION_ACTION_TYPES_DEFAULT =
"notification_conversation_action_types_default";
/**
* Threshold to accept a suggested language from LangID model.
*/
@VisibleForTesting
static final String LANG_ID_THRESHOLD_OVERRIDE = "lang_id_threshold_override";
/**
* Whether to enable {@link android.view.textclassifier.TemplateIntentFactory}.
*/
private static final String TEMPLATE_INTENT_FACTORY_ENABLED = "template_intent_factory_enabled";
/**
* Whether to enable "translate" action in classifyText.
*/
private static final String TRANSLATE_IN_CLASSIFICATION_ENABLED =
"translate_in_classification_enabled";
/**
* Whether to detect the languages of the text in request by using langId for the native
* model.
*/
private static final String DETECT_LANGUAGES_FROM_TEXT_ENABLED =
"detect_languages_from_text_enabled";
/**
* A colon(:) separated string that specifies the configuration to use when including
* surrounding context text in language detection queries.
* <p>
* Format= minimumTextSize<int>:penalizeRatio<float>:textScoreRatio<float>
* <p>
* e.g. 20:1.0:0.4
* <p>
* Accept all text lengths with minimumTextSize=0
* <p>
* Reject all text less than minimumTextSize with penalizeRatio=0
* @see {@code TextClassifierImpl#detectLanguages(String, int, int)} for reference.
*/
@VisibleForTesting
static final String LANG_ID_CONTEXT_SETTINGS = "lang_id_context_settings";
static final String GENERATE_LINKS_MAX_TEXT_LENGTH = "generate_links_max_text_length";
/**
* The TextClassifierService which would like to use. Example of setting the package:
* <pre>
@ -168,16 +82,6 @@ public final class TextClassificationConstants {
static final String TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE =
"textclassifier_service_package_override";
/**
* Whether to use the default system text classifier as the default text classifier
* implementation. The local text classifier is used if it is {@code false}.
*
* @see android.service.textclassifier.TextClassifierService#getDefaultTextClassifierImplementation(Context)
*/
// TODO: Once the system health experiment is done, remove this together with local TC.
private static final String USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL =
"use_default_system_text_classifier_as_default_impl";
private static final String DEFAULT_TEXT_CLASSIFIER_SERVICE_PACKAGE_OVERRIDE = null;
private static final boolean LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
@ -186,42 +90,7 @@ public final class TextClassificationConstants {
private static final boolean SMART_TEXT_SHARE_ENABLED_DEFAULT = true;
private static final boolean SMART_LINKIFY_ENABLED_DEFAULT = true;
private static final boolean SMART_SELECT_ANIMATION_ENABLED_DEFAULT = true;
private static final int SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000;
private static final int CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000;
private static final int GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT = 100 * 1000;
private static final int GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT = 100;
private static final List<String> ENTITY_LIST_DEFAULT_VALUE = Arrays.asList(
TextClassifier.TYPE_ADDRESS,
TextClassifier.TYPE_EMAIL,
TextClassifier.TYPE_PHONE,
TextClassifier.TYPE_URL,
TextClassifier.TYPE_DATE,
TextClassifier.TYPE_DATE_TIME,
TextClassifier.TYPE_FLIGHT_NUMBER);
private static final List<String> CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES = Arrays.asList(
ConversationAction.TYPE_TEXT_REPLY,
ConversationAction.TYPE_CREATE_REMINDER,
ConversationAction.TYPE_CALL_PHONE,
ConversationAction.TYPE_OPEN_URL,
ConversationAction.TYPE_SEND_EMAIL,
ConversationAction.TYPE_SEND_SMS,
ConversationAction.TYPE_TRACK_FLIGHT,
ConversationAction.TYPE_VIEW_CALENDAR,
ConversationAction.TYPE_VIEW_MAP,
ConversationAction.TYPE_ADD_CONTACT,
ConversationAction.TYPE_COPY);
/**
* < 0 : Not set. Use value from LangId model.
* 0 - 1: Override value in LangId model.
*
* @see EntityConfidence
*/
private static final float LANG_ID_THRESHOLD_OVERRIDE_DEFAULT = -1f;
private static final boolean TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT = true;
private static final boolean TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT = true;
private static final boolean DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT = true;
private static final float[] LANG_ID_CONTEXT_SETTINGS_DEFAULT = new float[]{20f, 1.0f, 0.4f};
private static final boolean USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL_DEFAULT = true;
@Nullable
public String getTextClassifierServicePackageOverride() {
@ -266,119 +135,20 @@ public final class TextClassificationConstants {
SMART_SELECT_ANIMATION_ENABLED, SMART_SELECT_ANIMATION_ENABLED_DEFAULT);
}
public int getSuggestSelectionMaxRangeLength() {
return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
SUGGEST_SELECTION_MAX_RANGE_LENGTH, SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT);
}
public int getClassifyTextMaxRangeLength() {
return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
CLASSIFY_TEXT_MAX_RANGE_LENGTH, CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT);
}
public int getGenerateLinksMaxTextLength() {
return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
GENERATE_LINKS_MAX_TEXT_LENGTH, GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT);
}
public int getGenerateLinksLogSampleRate() {
return DeviceConfig.getInt(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
GENERATE_LINKS_LOG_SAMPLE_RATE, GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT);
}
public List<String> getEntityListDefault() {
return getDeviceConfigStringList(ENTITY_LIST_DEFAULT, ENTITY_LIST_DEFAULT_VALUE);
}
public List<String> getEntityListNotEditable() {
return getDeviceConfigStringList(ENTITY_LIST_NOT_EDITABLE, ENTITY_LIST_DEFAULT_VALUE);
}
public List<String> getEntityListEditable() {
return getDeviceConfigStringList(ENTITY_LIST_EDITABLE, ENTITY_LIST_DEFAULT_VALUE);
}
public List<String> getInAppConversationActionTypes() {
return getDeviceConfigStringList(
IN_APP_CONVERSATION_ACTION_TYPES_DEFAULT,
CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES);
}
public List<String> getNotificationConversationActionTypes() {
return getDeviceConfigStringList(
NOTIFICATION_CONVERSATION_ACTION_TYPES_DEFAULT,
CONVERSATION_ACTIONS_TYPES_DEFAULT_VALUES);
}
public float getLangIdThresholdOverride() {
return DeviceConfig.getFloat(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
LANG_ID_THRESHOLD_OVERRIDE,
LANG_ID_THRESHOLD_OVERRIDE_DEFAULT);
}
public boolean isTemplateIntentFactoryEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TEMPLATE_INTENT_FACTORY_ENABLED,
TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT);
}
public boolean isTranslateInClassificationEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TRANSLATE_IN_CLASSIFICATION_ENABLED,
TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT);
}
public boolean isDetectLanguagesFromTextEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
DETECT_LANGUAGES_FROM_TEXT_ENABLED,
DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT);
}
public float[] getLangIdContextSettings() {
return getDeviceConfigFloatArray(
LANG_ID_CONTEXT_SETTINGS, LANG_ID_CONTEXT_SETTINGS_DEFAULT);
}
public boolean getUseDefaultTextClassifierAsDefaultImplementation() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL,
USE_DEFAULT_SYSTEM_TEXT_CLASSIFIER_AS_DEFAULT_IMPL_DEFAULT);
}
void dump(IndentingPrintWriter pw) {
pw.println("TextClassificationConstants:");
pw.increaseIndent();
pw.printPair("classify_text_max_range_length", getClassifyTextMaxRangeLength())
.println();
pw.printPair("detect_languages_from_text_enabled", isDetectLanguagesFromTextEnabled())
.println();
pw.printPair("entity_list_default", getEntityListDefault())
.println();
pw.printPair("entity_list_editable", getEntityListEditable())
.println();
pw.printPair("entity_list_not_editable", getEntityListNotEditable())
.println();
pw.printPair("generate_links_log_sample_rate", getGenerateLinksLogSampleRate())
.println();
pw.printPair("generate_links_max_text_length", getGenerateLinksMaxTextLength())
.println();
pw.printPair("in_app_conversation_action_types_default", getInAppConversationActionTypes())
.println();
pw.printPair("lang_id_context_settings", Arrays.toString(getLangIdContextSettings()))
.println();
pw.printPair("lang_id_threshold_override", getLangIdThresholdOverride())
.println();
pw.printPair("local_textclassifier_enabled", isLocalTextClassifierEnabled())
.println();
pw.printPair("model_dark_launch_enabled", isModelDarkLaunchEnabled())
.println();
pw.printPair("notification_conversation_action_types_default",
getNotificationConversationActionTypes()).println();
pw.printPair("smart_linkify_enabled", isSmartLinkifyEnabled())
.println();
pw.printPair("smart_select_animation_enabled", isSmartSelectionAnimationEnabled())
@ -387,57 +157,10 @@ public final class TextClassificationConstants {
.println();
pw.printPair("smart_text_share_enabled", isSmartTextShareEnabled())
.println();
pw.printPair("suggest_selection_max_range_length", getSuggestSelectionMaxRangeLength())
.println();
pw.printPair("system_textclassifier_enabled", isSystemTextClassifierEnabled())
.println();
pw.printPair("template_intent_factory_enabled", isTemplateIntentFactoryEnabled())
.println();
pw.printPair("translate_in_classification_enabled", isTranslateInClassificationEnabled())
.println();
pw.printPair("textclassifier_service_package_override",
getTextClassifierServicePackageOverride()).println();
pw.printPair("use_default_system_text_classifier_as_default_impl",
getUseDefaultTextClassifierAsDefaultImplementation()).println();
pw.decreaseIndent();
}
private static List<String> getDeviceConfigStringList(String key, List<String> defaultValue) {
return parse(
DeviceConfig.getString(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key, null),
defaultValue);
}
private static float[] getDeviceConfigFloatArray(String key, float[] defaultValue) {
return parse(
DeviceConfig.getString(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key, null),
defaultValue);
}
private static List<String> parse(@Nullable String listStr, List<String> defaultValue) {
if (listStr != null) {
return Collections.unmodifiableList(Arrays.asList(listStr.split(DELIMITER)));
}
return defaultValue;
}
private static float[] parse(@Nullable String arrayStr, float[] defaultValue) {
if (arrayStr != null) {
final String[] split = arrayStr.split(DELIMITER);
if (split.length != defaultValue.length) {
return defaultValue;
}
final float[] result = new float[split.length];
for (int i = 0; i < split.length; i++) {
try {
result[i] = Float.parseFloat(split[i]);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return result;
} else {
return defaultValue;
}
}
}

View File

@ -19,21 +19,15 @@ package android.view.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.ServiceManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.view.textclassifier.TextClassifier.TextClassifierType;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.Set;
/**
* Interface to the text classification service.
@ -41,7 +35,7 @@ import java.util.Set;
@SystemService(Context.TEXT_CLASSIFICATION_SERVICE)
public final class TextClassificationManager {
private static final String LOG_TAG = "TextClassificationManager";
private static final String LOG_TAG = TextClassifier.LOG_TAG;
private static final TextClassificationConstants sDefaultSettings =
new TextClassificationConstants();
@ -52,15 +46,11 @@ public final class TextClassificationManager {
classificationContext, getTextClassifier());
private final Context mContext;
private final SettingsObserver mSettingsObserver;
@GuardedBy("mLock")
@Nullable
private TextClassifier mCustomTextClassifier;
@GuardedBy("mLock")
@Nullable
private TextClassifier mLocalTextClassifier;
@GuardedBy("mLock")
private TextClassificationSessionFactory mSessionFactory;
@GuardedBy("mLock")
private TextClassificationConstants mSettings;
@ -69,7 +59,6 @@ public final class TextClassificationManager {
public TextClassificationManager(Context context) {
mContext = Objects.requireNonNull(context);
mSessionFactory = mDefaultSessionFactory;
mSettingsObserver = new SettingsObserver(this);
}
/**
@ -112,7 +101,7 @@ public final class TextClassificationManager {
*
* @see TextClassifier#LOCAL
* @see TextClassifier#SYSTEM
* @see TextClassifier#DEFAULT_SERVICE
* @see TextClassifier#DEFAULT_SYSTEM
* @hide
*/
@UnsupportedAppUsage
@ -189,28 +178,17 @@ public final class TextClassificationManager {
}
}
@Override
protected void finalize() throws Throwable {
try {
// Note that fields could be null if the constructor threw.
if (mSettingsObserver != null) {
DeviceConfig.removeOnPropertiesChangedListener(mSettingsObserver);
}
} finally {
super.finalize();
}
}
/** @hide */
private TextClassifier getSystemTextClassifier(@TextClassifierType int type) {
synchronized (mLock) {
if (getSettings().isSystemTextClassifierEnabled()) {
try {
Log.d(LOG_TAG, "Initializing SystemTextClassifier, type = " + type);
Log.d(LOG_TAG, "Initializing SystemTextClassifier, type = "
+ TextClassifier.typeToString(type));
return new SystemTextClassifier(
mContext,
getSettings(),
/* useDefault= */ type == TextClassifier.DEFAULT_SERVICE);
/* useDefault= */ type == TextClassifier.DEFAULT_SYSTEM);
} catch (ServiceManager.ServiceNotFoundException e) {
Log.e(LOG_TAG, "Could not initialize SystemTextClassifier", e);
}
@ -224,49 +202,13 @@ public final class TextClassificationManager {
*/
@NonNull
private TextClassifier getLocalTextClassifier() {
synchronized (mLock) {
if (mLocalTextClassifier == null) {
if (getSettings().isLocalTextClassifierEnabled()) {
mLocalTextClassifier =
new TextClassifierImpl(mContext, getSettings(), TextClassifier.NO_OP);
} else {
Log.d(LOG_TAG, "Local TextClassifier disabled");
mLocalTextClassifier = TextClassifier.NO_OP;
}
}
return mLocalTextClassifier;
}
}
/** @hide */
@VisibleForTesting
public void invalidateForTesting() {
invalidate();
}
private void invalidate() {
synchronized (mLock) {
mSettings = null;
invalidateTextClassifiers();
}
}
private void invalidateTextClassifiers() {
synchronized (mLock) {
mLocalTextClassifier = null;
}
}
Context getApplicationContext() {
return mContext.getApplicationContext() != null
? mContext.getApplicationContext()
: mContext;
Log.d(LOG_TAG, "Local text-classifier not supported. Returning a no-op text-classifier.");
return TextClassifier.NO_OP;
}
/** @hide **/
public void dump(IndentingPrintWriter pw) {
getLocalTextClassifier().dump(pw);
getSystemTextClassifier(TextClassifier.DEFAULT_SERVICE).dump(pw);
getSystemTextClassifier(TextClassifier.DEFAULT_SYSTEM).dump(pw);
getSystemTextClassifier(TextClassifier.SYSTEM).dump(pw);
getSettings().dump(pw);
}
@ -283,31 +225,4 @@ public final class TextClassificationManager {
return sDefaultSettings;
}
}
private static final class SettingsObserver implements
DeviceConfig.OnPropertiesChangedListener {
private final WeakReference<TextClassificationManager> mTcm;
SettingsObserver(TextClassificationManager tcm) {
mTcm = new WeakReference<>(tcm);
DeviceConfig.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
ActivityThread.currentApplication().getMainExecutor(),
this);
}
@Override
public void onPropertiesChanged(Properties properties) {
final TextClassificationManager tcm = mTcm.get();
if (tcm != null) {
final Set<String> keys = properties.getKeyset();
if (keys.contains(TextClassificationConstants.SYSTEM_TEXT_CLASSIFIER_ENABLED)
|| keys.contains(
TextClassificationConstants.LOCAL_TEXT_CLASSIFIER_ENABLED)) {
tcm.invalidateTextClassifiers();
}
}
}
}
}

View File

@ -61,19 +61,32 @@ import java.util.Set;
public interface TextClassifier {
/** @hide */
String DEFAULT_LOG_TAG = "androidtc";
String LOG_TAG = "androidtc";
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {LOCAL, SYSTEM, DEFAULT_SERVICE})
@IntDef(value = {LOCAL, SYSTEM, DEFAULT_SYSTEM})
@interface TextClassifierType {} // TODO: Expose as system APIs.
/** Specifies a TextClassifier that runs locally in the app's process. @hide */
int LOCAL = 0;
/** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */
int SYSTEM = 1;
/** Specifies the default TextClassifier that runs in the system process. @hide */
int DEFAULT_SERVICE = 2;
int DEFAULT_SYSTEM = 2;
/** @hide */
static String typeToString(@TextClassifierType int type) {
switch (type) {
case LOCAL:
return "Local";
case SYSTEM:
return "System";
case DEFAULT_SYSTEM:
return "Default system";
}
return "Unknown";
}
/** The TextClassifier failed to run. */
String TYPE_UNKNOWN = "";
@ -776,7 +789,7 @@ public interface TextClassifier {
static void checkMainThread() {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");
Log.w(LOG_TAG, "TextClassifier called on main thread");
}
}
}

View File

@ -1,187 +0,0 @@
/*
* 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.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_FIRST_ENTITY_TYPE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_SCORE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_SECOND_ENTITY_TYPE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_SESSION_ID;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_THIRD_ENTITY_TYPE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_WIDGET_TYPE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_WIDGET_VERSION;
import android.metrics.LogMaker;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.util.Objects;
/**
* Log {@link TextClassifierEvent} by using Tron, only support language detection and
* conversation actions.
*
* @hide
*/
public final class TextClassifierEventTronLogger {
private static final String TAG = "TCEventTronLogger";
private final MetricsLogger mMetricsLogger;
public TextClassifierEventTronLogger() {
this(new MetricsLogger());
}
@VisibleForTesting
public TextClassifierEventTronLogger(MetricsLogger metricsLogger) {
mMetricsLogger = Objects.requireNonNull(metricsLogger);
}
/** Emits a text classifier event to the logs. */
public void writeEvent(TextClassifierEvent event) {
Objects.requireNonNull(event);
int category = getCategory(event);
if (category == -1) {
Log.w(TAG, "Unknown category: " + event.getEventCategory());
return;
}
final LogMaker log = new LogMaker(category)
.setSubtype(getLogType(event))
.addTaggedData(FIELD_TEXT_CLASSIFIER_SESSION_ID, event.getResultId())
.addTaggedData(FIELD_TEXTCLASSIFIER_MODEL, getModelName(event));
if (event.getScores().length >= 1) {
log.addTaggedData(FIELD_TEXT_CLASSIFIER_SCORE, event.getScores()[0]);
}
String[] entityTypes = event.getEntityTypes();
// The old logger does not support a field of list type, and thus workaround by store them
// in three separate fields. This is not an issue with the new logger.
if (entityTypes.length >= 1) {
log.addTaggedData(FIELD_TEXT_CLASSIFIER_FIRST_ENTITY_TYPE, entityTypes[0]);
}
if (entityTypes.length >= 2) {
log.addTaggedData(FIELD_TEXT_CLASSIFIER_SECOND_ENTITY_TYPE, entityTypes[1]);
}
if (entityTypes.length >= 3) {
log.addTaggedData(FIELD_TEXT_CLASSIFIER_THIRD_ENTITY_TYPE, entityTypes[2]);
}
TextClassificationContext eventContext = event.getEventContext();
if (eventContext != null) {
log.addTaggedData(FIELD_TEXT_CLASSIFIER_WIDGET_TYPE, eventContext.getWidgetType());
log.addTaggedData(FIELD_TEXT_CLASSIFIER_WIDGET_VERSION,
eventContext.getWidgetVersion());
log.setPackageName(eventContext.getPackageName());
}
mMetricsLogger.write(log);
debugLog(log);
}
private static String getModelName(TextClassifierEvent event) {
if (event.getModelName() != null) {
return event.getModelName();
}
return SelectionSessionLogger.SignatureParser.getModelName(event.getResultId());
}
private static int getCategory(TextClassifierEvent event) {
switch (event.getEventCategory()) {
case TextClassifierEvent.CATEGORY_CONVERSATION_ACTIONS:
return MetricsEvent.CONVERSATION_ACTIONS;
case TextClassifierEvent.CATEGORY_LANGUAGE_DETECTION:
return MetricsEvent.LANGUAGE_DETECTION;
}
return -1;
}
private static int getLogType(TextClassifierEvent event) {
switch (event.getEventType()) {
case TextClassifierEvent.TYPE_SMART_ACTION:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
case TextClassifierEvent.TYPE_ACTIONS_SHOWN:
return MetricsEvent.ACTION_TEXT_CLASSIFIER_ACTIONS_SHOWN;
case TextClassifierEvent.TYPE_MANUAL_REPLY:
return MetricsEvent.ACTION_TEXT_CLASSIFIER_MANUAL_REPLY;
case TextClassifierEvent.TYPE_ACTIONS_GENERATED:
return MetricsEvent.ACTION_TEXT_CLASSIFIER_ACTIONS_GENERATED;
default:
return MetricsEvent.VIEW_UNKNOWN;
}
}
private String toCategoryName(int category) {
switch (category) {
case MetricsEvent.CONVERSATION_ACTIONS:
return "conversation_actions";
case MetricsEvent.LANGUAGE_DETECTION:
return "language_detection";
}
return "unknown";
}
private String toEventName(int logType) {
switch (logType) {
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
return "smart_share";
case MetricsEvent.ACTION_TEXT_CLASSIFIER_ACTIONS_SHOWN:
return "actions_shown";
case MetricsEvent.ACTION_TEXT_CLASSIFIER_MANUAL_REPLY:
return "manual_reply";
case MetricsEvent.ACTION_TEXT_CLASSIFIER_ACTIONS_GENERATED:
return "actions_generated";
}
return "unknown";
}
private void debugLog(LogMaker log) {
if (!Log.ENABLE_FULL_LOGGING) {
return;
}
final String id = String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_SESSION_ID));
final String categoryName = toCategoryName(log.getCategory());
final String eventName = toEventName(log.getSubtype());
final String widgetType =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_WIDGET_TYPE));
final String widgetVersion =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_WIDGET_VERSION));
final String model = String.valueOf(log.getTaggedData(FIELD_TEXTCLASSIFIER_MODEL));
final String firstEntityType =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_FIRST_ENTITY_TYPE));
final String secondEntityType =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_SECOND_ENTITY_TYPE));
final String thirdEntityType =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_THIRD_ENTITY_TYPE));
final String score =
String.valueOf(log.getTaggedData(FIELD_TEXT_CLASSIFIER_SCORE));
StringBuilder builder = new StringBuilder();
builder.append("writeEvent: ");
builder.append("id=").append(id);
builder.append(", category=").append(categoryName);
builder.append(", eventName=").append(eventName);
builder.append(", widgetType=").append(widgetType);
builder.append(", widgetVersion=").append(widgetVersion);
builder.append(", model=").append(model);
builder.append(", firstEntityType=").append(firstEntityType);
builder.append(", secondEntityType=").append(secondEntityType);
builder.append(", thirdEntityType=").append(thirdEntityType);
builder.append(", score=").append(score);
Log.v(TAG, builder.toString());
}
}

View File

@ -1,911 +0,0 @@
/*
* Copyright (C) 2017 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.annotation.WorkerThread;
import android.app.RemoteAction;
import android.content.Context;
import android.content.Intent;
import android.icu.util.ULocale;
import android.os.Bundle;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import android.view.textclassifier.ActionsModelParamsSupplier.ActionsModelParams;
import android.view.textclassifier.intent.ClassificationIntentFactory;
import android.view.textclassifier.intent.LabeledIntent;
import android.view.textclassifier.intent.LegacyClassificationIntentFactory;
import android.view.textclassifier.intent.TemplateClassificationIntentFactory;
import android.view.textclassifier.intent.TemplateIntentFactory;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.google.android.textclassifier.ActionsSuggestionsModel;
import com.google.android.textclassifier.AnnotatorModel;
import com.google.android.textclassifier.LangIdModel;
import com.google.android.textclassifier.LangIdModel.LanguageResult;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
/**
* Default implementation of the {@link TextClassifier} interface.
*
* <p>This class uses machine learning to recognize entities in text.
* Unless otherwise stated, methods of this class are blocking operations and should most
* likely not be called on the UI thread.
*
* @hide
*/
public final class TextClassifierImpl implements TextClassifier {
private static final String LOG_TAG = DEFAULT_LOG_TAG;
private static final boolean DEBUG = false;
private static final File FACTORY_MODEL_DIR = new File("/etc/textclassifier/");
// Annotator
private static final String ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX =
"textclassifier\\.(.*)\\.model";
private static final File ANNOTATOR_UPDATED_MODEL_FILE =
new File("/data/misc/textclassifier/textclassifier.model");
// LangID
private static final String LANG_ID_FACTORY_MODEL_FILENAME_REGEX = "lang_id.model";
private static final File UPDATED_LANG_ID_MODEL_FILE =
new File("/data/misc/textclassifier/lang_id.model");
// Actions
private static final String ACTIONS_FACTORY_MODEL_FILENAME_REGEX =
"actions_suggestions\\.(.*)\\.model";
private static final File UPDATED_ACTIONS_MODEL =
new File("/data/misc/textclassifier/actions_suggestions.model");
private final Context mContext;
private final TextClassifier mFallback;
private final GenerateLinksLogger mGenerateLinksLogger;
private final Object mLock = new Object();
@GuardedBy("mLock")
private ModelFileManager.ModelFile mAnnotatorModelInUse;
@GuardedBy("mLock")
private AnnotatorModel mAnnotatorImpl;
@GuardedBy("mLock")
private ModelFileManager.ModelFile mLangIdModelInUse;
@GuardedBy("mLock")
private LangIdModel mLangIdImpl;
@GuardedBy("mLock")
private ModelFileManager.ModelFile mActionModelInUse;
@GuardedBy("mLock")
private ActionsSuggestionsModel mActionsImpl;
private final SelectionSessionLogger mSessionLogger = new SelectionSessionLogger();
private final TextClassifierEventTronLogger mTextClassifierEventTronLogger =
new TextClassifierEventTronLogger();
private final TextClassificationConstants mSettings;
private final ModelFileManager mAnnotatorModelFileManager;
private final ModelFileManager mLangIdModelFileManager;
private final ModelFileManager mActionsModelFileManager;
private final ClassificationIntentFactory mClassificationIntentFactory;
private final TemplateIntentFactory mTemplateIntentFactory;
private final Supplier<ActionsModelParams> mActionsModelParamsSupplier;
public TextClassifierImpl(
Context context, TextClassificationConstants settings, TextClassifier fallback) {
mContext = Objects.requireNonNull(context);
mFallback = Objects.requireNonNull(fallback);
mSettings = Objects.requireNonNull(settings);
mGenerateLinksLogger = new GenerateLinksLogger(mSettings.getGenerateLinksLogSampleRate());
mAnnotatorModelFileManager = new ModelFileManager(
new ModelFileManager.ModelFileSupplierImpl(
FACTORY_MODEL_DIR,
ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX,
ANNOTATOR_UPDATED_MODEL_FILE,
AnnotatorModel::getVersion,
AnnotatorModel::getLocales));
mLangIdModelFileManager = new ModelFileManager(
new ModelFileManager.ModelFileSupplierImpl(
FACTORY_MODEL_DIR,
LANG_ID_FACTORY_MODEL_FILENAME_REGEX,
UPDATED_LANG_ID_MODEL_FILE,
LangIdModel::getVersion,
fd -> ModelFileManager.ModelFile.LANGUAGE_INDEPENDENT));
mActionsModelFileManager = new ModelFileManager(
new ModelFileManager.ModelFileSupplierImpl(
FACTORY_MODEL_DIR,
ACTIONS_FACTORY_MODEL_FILENAME_REGEX,
UPDATED_ACTIONS_MODEL,
ActionsSuggestionsModel::getVersion,
ActionsSuggestionsModel::getLocales));
mTemplateIntentFactory = new TemplateIntentFactory();
mClassificationIntentFactory = mSettings.isTemplateIntentFactoryEnabled()
? new TemplateClassificationIntentFactory(
mTemplateIntentFactory, new LegacyClassificationIntentFactory())
: new LegacyClassificationIntentFactory();
mActionsModelParamsSupplier = new ActionsModelParamsSupplier(mContext,
() -> {
synchronized (mLock) {
// Clear mActionsImpl here, so that we will create a new
// ActionsSuggestionsModel object with the new flag in the next request.
mActionsImpl = null;
mActionModelInUse = null;
}
});
}
public TextClassifierImpl(Context context, TextClassificationConstants settings) {
this(context, settings, TextClassifier.NO_OP);
}
/** @inheritDoc */
@Override
@WorkerThread
public TextSelection suggestSelection(TextSelection.Request request) {
Objects.requireNonNull(request);
Utils.checkMainThread();
try {
final int rangeLength = request.getEndIndex() - request.getStartIndex();
final String string = request.getText().toString();
if (string.length() > 0
&& rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) {
final String localesString = concatenateLocales(request.getDefaultLocales());
final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
final ZonedDateTime refTime = ZonedDateTime.now();
final AnnotatorModel annotatorImpl = getAnnotatorImpl(request.getDefaultLocales());
final int start;
final int end;
if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) {
start = request.getStartIndex();
end = request.getEndIndex();
} else {
final int[] startEnd = annotatorImpl.suggestSelection(
string, request.getStartIndex(), request.getEndIndex(),
new AnnotatorModel.SelectionOptions(localesString, detectLanguageTags));
start = startEnd[0];
end = startEnd[1];
}
if (start < end
&& start >= 0 && end <= string.length()
&& start <= request.getStartIndex() && end >= request.getEndIndex()) {
final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
final AnnotatorModel.ClassificationResult[] results =
annotatorImpl.classifyText(
string, start, end,
new AnnotatorModel.ClassificationOptions(
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString,
detectLanguageTags),
// 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());
}
return tsBuilder.setId(createId(
string, request.getStartIndex(), request.getEndIndex()))
.build();
} else {
// We can not trust the result. Log the issue and ignore the result.
Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
}
}
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG,
"Error suggesting selection for text. No changes to selection suggested.",
t);
}
// Getting here means something went wrong, return a NO_OP result.
return mFallback.suggestSelection(request);
}
/** @inheritDoc */
@Override
@WorkerThread
public TextClassification classifyText(TextClassification.Request request) {
Objects.requireNonNull(request);
Utils.checkMainThread();
try {
final int rangeLength = request.getEndIndex() - request.getStartIndex();
final String string = request.getText().toString();
if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) {
final String localesString = concatenateLocales(request.getDefaultLocales());
final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
final ZonedDateTime refTime = request.getReferenceTime() != null
? request.getReferenceTime() : ZonedDateTime.now();
final AnnotatorModel.ClassificationResult[] results =
getAnnotatorImpl(request.getDefaultLocales())
.classifyText(
string, request.getStartIndex(), request.getEndIndex(),
new AnnotatorModel.ClassificationOptions(
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString,
detectLanguageTags),
mContext,
getResourceLocalesString()
);
if (results.length > 0) {
return createClassificationResult(
results, string,
request.getStartIndex(), request.getEndIndex(), refTime.toInstant());
}
}
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error getting text classification info.", t);
}
// Getting here means something went wrong, return a NO_OP result.
return mFallback.classifyText(request);
}
/** @inheritDoc */
@Override
@WorkerThread
public TextLinks generateLinks(@NonNull TextLinks.Request request) {
Objects.requireNonNull(request);
Utils.checkMainThread();
if (!Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength())) {
return mFallback.generateLinks(request);
}
if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
return Utils.generateLegacyLinks(request);
}
final String textString = request.getText().toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
try {
final long startTimeMs = System.currentTimeMillis();
final ZonedDateTime refTime = ZonedDateTime.now();
final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
? request.getEntityConfig().resolveEntityListModifications(
getEntitiesForHints(request.getEntityConfig().getHints()))
: mSettings.getEntityListDefault();
final String localesString = concatenateLocales(request.getDefaultLocales());
final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
final AnnotatorModel annotatorImpl =
getAnnotatorImpl(request.getDefaultLocales());
final boolean isSerializedEntityDataEnabled =
ExtrasUtils.isSerializedEntityDataEnabled(request);
final AnnotatorModel.AnnotatedSpan[] annotations =
annotatorImpl.annotate(
textString,
new AnnotatorModel.AnnotationOptions(
refTime.toInstant().toEpochMilli(),
refTime.getZone().getId(),
localesString,
detectLanguageTags,
entitiesToIdentify,
AnnotatorModel.AnnotationUsecase.SMART.getValue(),
isSerializedEntityDataEnabled));
for (AnnotatorModel.AnnotatedSpan span : annotations) {
final AnnotatorModel.ClassificationResult[] results =
span.getClassification();
if (results.length == 0
|| !entitiesToIdentify.contains(results[0].getCollection())) {
continue;
}
final Map<String, Float> entityScores = new ArrayMap<>();
for (int i = 0; i < results.length; i++) {
entityScores.put(results[i].getCollection(), results[i].getScore());
}
Bundle extras = new Bundle();
if (isSerializedEntityDataEnabled) {
ExtrasUtils.putEntities(extras, results);
}
builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores, extras);
}
final TextLinks links = builder.build();
final long endTimeMs = System.currentTimeMillis();
final String callingPackageName = request.getCallingPackageName() == null
? mContext.getPackageName() // local (in process) TC.
: request.getCallingPackageName();
mGenerateLinksLogger.logGenerateLinks(
request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
return links;
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error getting links info.", t);
}
return mFallback.generateLinks(request);
}
/** @inheritDoc */
@Override
public int getMaxGenerateLinksTextLength() {
return mSettings.getGenerateLinksMaxTextLength();
}
private Collection<String> getEntitiesForHints(Collection<String> hints) {
final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
// Use the default if there is no hint, or conflicting ones.
final boolean useDefault = editable == notEditable;
if (useDefault) {
return mSettings.getEntityListDefault();
} else if (editable) {
return mSettings.getEntityListEditable();
} else { // notEditable
return mSettings.getEntityListNotEditable();
}
}
/** @inheritDoc */
@Override
public void onSelectionEvent(SelectionEvent event) {
mSessionLogger.writeEvent(event);
}
@Override
public void onTextClassifierEvent(TextClassifierEvent event) {
if (DEBUG) {
Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]");
}
try {
final SelectionEvent selEvent = event.toSelectionEvent();
if (selEvent != null) {
mSessionLogger.writeEvent(selEvent);
} else {
mTextClassifierEventTronLogger.writeEvent(event);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Error writing event", e);
}
}
/** @inheritDoc */
@Override
public TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
Objects.requireNonNull(request);
Utils.checkMainThread();
try {
final TextLanguage.Builder builder = new TextLanguage.Builder();
final LangIdModel.LanguageResult[] langResults =
getLangIdImpl().detectLanguages(request.getText().toString());
for (int i = 0; i < langResults.length; i++) {
builder.putLocale(
ULocale.forLanguageTag(langResults[i].getLanguage()),
langResults[i].getScore());
}
return builder.build();
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error detecting text language.", t);
}
return mFallback.detectLanguage(request);
}
@Override
public ConversationActions suggestConversationActions(ConversationActions.Request request) {
Objects.requireNonNull(request);
Utils.checkMainThread();
try {
ActionsSuggestionsModel actionsImpl = getActionsImpl();
if (actionsImpl == null) {
// Actions model is optional, fallback if it is not available.
return mFallback.suggestConversationActions(request);
}
ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
ActionsSuggestionsHelper.toNativeMessages(
request.getConversation(), this::detectLanguageTagsFromText);
if (nativeMessages.length == 0) {
return mFallback.suggestConversationActions(request);
}
ActionsSuggestionsModel.Conversation nativeConversation =
new ActionsSuggestionsModel.Conversation(nativeMessages);
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
actionsImpl.suggestActionsWithIntents(
nativeConversation,
null,
mContext,
getResourceLocalesString(),
getAnnotatorImpl(LocaleList.getDefault()));
return createConversationActionResult(request, nativeSuggestions);
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
}
return mFallback.suggestConversationActions(request);
}
/**
* Returns the {@link ConversationAction} result, with a non-null extras.
* <p>
* Whenever the RemoteAction is non-null, you can expect its corresponding intent
* with a non-null component name is in the extras.
*/
private ConversationActions createConversationActionResult(
ConversationActions.Request request,
ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
List<ConversationAction> conversationActions = new ArrayList<>();
for (ActionsSuggestionsModel.ActionSuggestion nativeSuggestion : nativeSuggestions) {
String actionType = nativeSuggestion.getActionType();
if (!expectedTypes.contains(actionType)) {
continue;
}
LabeledIntent.Result labeledIntentResult =
ActionsSuggestionsHelper.createLabeledIntentResult(
mContext,
mTemplateIntentFactory,
nativeSuggestion);
RemoteAction remoteAction = null;
Bundle extras = new Bundle();
if (labeledIntentResult != null) {
remoteAction = labeledIntentResult.remoteAction;
ExtrasUtils.putActionIntent(extras, labeledIntentResult.resolvedIntent);
}
ExtrasUtils.putSerializedEntityData(extras, nativeSuggestion.getSerializedEntityData());
ExtrasUtils.putEntitiesExtras(
extras,
TemplateIntentFactory.nameVariantsToBundle(nativeSuggestion.getEntityData()));
conversationActions.add(
new ConversationAction.Builder(actionType)
.setConfidenceScore(nativeSuggestion.getScore())
.setTextReply(nativeSuggestion.getResponseText())
.setAction(remoteAction)
.setExtras(extras)
.build());
}
conversationActions =
ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
if (request.getMaxSuggestions() >= 0
&& conversationActions.size() > request.getMaxSuggestions()) {
conversationActions = conversationActions.subList(0, request.getMaxSuggestions());
}
String resultId = ActionsSuggestionsHelper.createResultId(
mContext,
request.getConversation(),
mActionModelInUse.getVersion(),
mActionModelInUse.getSupportedLocales());
return new ConversationActions(conversationActions, resultId);
}
@Nullable
private String detectLanguageTagsFromText(CharSequence text) {
if (!mSettings.isDetectLanguagesFromTextEnabled()) {
return null;
}
final float threshold = getLangIdThreshold();
if (threshold < 0 || threshold > 1) {
Log.w(LOG_TAG,
"[detectLanguageTagsFromText] unexpected threshold is found: " + threshold);
return null;
}
TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
TextLanguage textLanguage = detectLanguage(request);
int localeHypothesisCount = textLanguage.getLocaleHypothesisCount();
List<String> languageTags = new ArrayList<>();
for (int i = 0; i < localeHypothesisCount; i++) {
ULocale locale = textLanguage.getLocale(i);
if (textLanguage.getConfidenceScore(locale) < threshold) {
break;
}
languageTags.add(locale.toLanguageTag());
}
if (languageTags.isEmpty()) {
return null;
}
return String.join(",", languageTags);
}
private Collection<String> resolveActionTypesFromRequest(ConversationActions.Request request) {
List<String> defaultActionTypes =
request.getHints().contains(ConversationActions.Request.HINT_FOR_NOTIFICATION)
? mSettings.getNotificationConversationActionTypes()
: mSettings.getInAppConversationActionTypes();
return request.getTypeConfig().resolveEntityListModifications(defaultActionTypes);
}
private AnnotatorModel getAnnotatorImpl(LocaleList localeList)
throws FileNotFoundException {
synchronized (mLock) {
localeList = localeList == null ? LocaleList.getDefault() : localeList;
final ModelFileManager.ModelFile bestModel =
mAnnotatorModelFileManager.findBestModelFile(localeList);
if (bestModel == null) {
throw new FileNotFoundException(
"No annotator model for " + localeList.toLanguageTags());
}
if (mAnnotatorImpl == null || !Objects.equals(mAnnotatorModelInUse, bestModel)) {
Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
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;
}
} finally {
maybeCloseAndLogError(pfd);
}
}
return mAnnotatorImpl;
}
}
private LangIdModel getLangIdImpl() throws FileNotFoundException {
synchronized (mLock) {
final ModelFileManager.ModelFile bestModel =
mLangIdModelFileManager.findBestModelFile(null);
if (bestModel == null) {
throw new FileNotFoundException("No LangID model is found");
}
if (mLangIdImpl == null || !Objects.equals(mLangIdModelInUse, bestModel)) {
Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
try {
if (pfd != null) {
mLangIdImpl = new LangIdModel(pfd.getFd());
mLangIdModelInUse = bestModel;
}
} finally {
maybeCloseAndLogError(pfd);
}
}
return mLangIdImpl;
}
}
@Nullable
private ActionsSuggestionsModel getActionsImpl() throws FileNotFoundException {
synchronized (mLock) {
// TODO: Use LangID to determine the locale we should use here?
final ModelFileManager.ModelFile bestModel =
mActionsModelFileManager.findBestModelFile(LocaleList.getDefault());
if (bestModel == null) {
return null;
}
if (mActionsImpl == null || !Objects.equals(mActionModelInUse, bestModel)) {
Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
try {
if (pfd == null) {
Log.d(LOG_TAG, "Failed to read the model file: " + bestModel.getPath());
return null;
}
ActionsModelParams params = mActionsModelParamsSupplier.get();
mActionsImpl = new ActionsSuggestionsModel(
pfd.getFd(), params.getSerializedPreconditions(bestModel));
mActionModelInUse = bestModel;
} finally {
maybeCloseAndLogError(pfd);
}
}
return mActionsImpl;
}
}
private String createId(String text, int start, int end) {
synchronized (mLock) {
return SelectionSessionLogger.createId(text, start, end, mContext,
mAnnotatorModelInUse.getVersion(),
mAnnotatorModelInUse.getSupportedLocales());
}
}
private static String concatenateLocales(@Nullable LocaleList locales) {
return (locales == null) ? "" : locales.toLanguageTags();
}
private TextClassification createClassificationResult(
AnnotatorModel.ClassificationResult[] classifications,
String text, int start, int end, @Nullable Instant referenceTime) {
final String classifiedText = text.substring(start, end);
final TextClassification.Builder builder = new TextClassification.Builder()
.setText(classifiedText);
final int typeCount = classifications.length;
AnnotatorModel.ClassificationResult highestScoringResult =
typeCount > 0 ? classifications[0] : null;
for (int i = 0; i < typeCount; i++) {
builder.setEntityType(classifications[i]);
if (classifications[i].getScore() > highestScoringResult.getScore()) {
highestScoringResult = classifications[i];
}
}
final Pair<Bundle, Bundle> languagesBundles = generateLanguageBundles(text, start, end);
final Bundle textLanguagesBundle = languagesBundles.first;
final Bundle foreignLanguageBundle = languagesBundles.second;
builder.setForeignLanguageExtra(foreignLanguageBundle);
boolean isPrimaryAction = true;
final List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
mContext,
classifiedText,
foreignLanguageBundle != null,
referenceTime,
highestScoringResult);
final LabeledIntent.TitleChooser titleChooser =
(labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
for (LabeledIntent labeledIntent : labeledIntents) {
final LabeledIntent.Result result =
labeledIntent.resolve(mContext, titleChooser, textLanguagesBundle);
if (result == null) {
continue;
}
final Intent intent = result.resolvedIntent;
final RemoteAction action = result.remoteAction;
if (isPrimaryAction) {
// For O backwards compatibility, the first RemoteAction is also written to the
// legacy API fields.
builder.setIcon(action.getIcon().loadDrawable(mContext));
builder.setLabel(action.getTitle().toString());
builder.setIntent(intent);
builder.setOnClickListener(TextClassification.createIntentOnClickListener(
TextClassification.createPendingIntent(
mContext, intent, labeledIntent.requestCode)));
isPrimaryAction = false;
}
builder.addAction(action, intent);
}
return builder.setId(createId(text, start, end)).build();
}
/**
* Returns a bundle pair with language detection information for extras.
* <p>
* Pair.first = textLanguagesBundle - A bundle containing information about all detected
* languages in the text. May be null if language detection fails or is disabled. This is
* typically expected to be added to a textClassifier generated remote action intent.
* See {@link ExtrasUtils#putTextLanguagesExtra(Bundle, Bundle)}.
* See {@link ExtrasUtils#getTopLanguage(Intent)}.
* <p>
* Pair.second = foreignLanguageBundle - A bundle with the language and confidence score if the
* system finds the text to be in a foreign language. Otherwise is null.
* See {@link TextClassification.Builder#setForeignLanguageExtra(Bundle)}.
*
* @param context the context of the text to detect languages for
* @param start the start index of the text
* @param end the end index of the text
*/
// TODO: Revisit this algorithm.
// TODO: Consider making this public API.
private Pair<Bundle, Bundle> generateLanguageBundles(String context, int start, int end) {
if (!mSettings.isTranslateInClassificationEnabled()) {
return null;
}
try {
final float threshold = getLangIdThreshold();
if (threshold < 0 || threshold > 1) {
Log.w(LOG_TAG,
"[detectForeignLanguage] unexpected threshold is found: " + threshold);
return Pair.create(null, null);
}
final EntityConfidence languageScores = detectLanguages(context, start, end);
if (languageScores.getEntities().isEmpty()) {
return Pair.create(null, null);
}
final Bundle textLanguagesBundle = new Bundle();
ExtrasUtils.putTopLanguageScores(textLanguagesBundle, languageScores);
final String language = languageScores.getEntities().get(0);
final float score = languageScores.getConfidenceScore(language);
if (score < threshold) {
return Pair.create(textLanguagesBundle, null);
}
Log.v(LOG_TAG, String.format(
Locale.US, "Language detected: <%s:%.2f>", language, score));
final Locale detected = new Locale(language);
final LocaleList deviceLocales = LocaleList.getDefault();
final int size = deviceLocales.size();
for (int i = 0; i < size; i++) {
if (deviceLocales.get(i).getLanguage().equals(detected.getLanguage())) {
return Pair.create(textLanguagesBundle, null);
}
}
final Bundle foreignLanguageBundle = ExtrasUtils.createForeignLanguageExtra(
detected.getLanguage(), score, getLangIdImpl().getVersion());
return Pair.create(textLanguagesBundle, foreignLanguageBundle);
} catch (Throwable t) {
Log.e(LOG_TAG, "Error generating language bundles.", t);
}
return Pair.create(null, null);
}
/**
* Detect the language of a piece of text by taking surrounding text into consideration.
*
* @param text text providing context for the text for which its language is to be detected
* @param start the start index of the text to detect its language
* @param end the end index of the text to detect its language
*/
// TODO: Revisit this algorithm.
private EntityConfidence detectLanguages(String text, int start, int end)
throws FileNotFoundException {
Preconditions.checkArgument(start >= 0);
Preconditions.checkArgument(end <= text.length());
Preconditions.checkArgument(start <= end);
final float[] langIdContextSettings = mSettings.getLangIdContextSettings();
// The minimum size of text to prefer for detection.
final int minimumTextSize = (int) langIdContextSettings[0];
// For reducing the score when text is less than the preferred size.
final float penalizeRatio = langIdContextSettings[1];
// Original detection score to surrounding text detection score ratios.
final float subjectTextScoreRatio = langIdContextSettings[2];
final float moreTextScoreRatio = 1f - subjectTextScoreRatio;
Log.v(LOG_TAG,
String.format(Locale.US, "LangIdContextSettings: "
+ "minimumTextSize=%d, penalizeRatio=%.2f, "
+ "subjectTextScoreRatio=%.2f, moreTextScoreRatio=%.2f",
minimumTextSize, penalizeRatio, subjectTextScoreRatio, moreTextScoreRatio));
if (end - start < minimumTextSize && penalizeRatio <= 0) {
return new EntityConfidence(Collections.emptyMap());
}
final String subject = text.substring(start, end);
final EntityConfidence scores = detectLanguages(subject);
if (subject.length() >= minimumTextSize
|| subject.length() == text.length()
|| subjectTextScoreRatio * penalizeRatio >= 1) {
return scores;
}
final EntityConfidence moreTextScores;
if (moreTextScoreRatio >= 0) {
// Attempt to grow the detection text to be at least minimumTextSize long.
final String moreText = Utils.getSubString(text, start, end, minimumTextSize);
moreTextScores = detectLanguages(moreText);
} else {
moreTextScores = new EntityConfidence(Collections.emptyMap());
}
// Combine the original detection scores with the those returned after including more text.
final Map<String, Float> newScores = new ArrayMap<>();
final Set<String> languages = new ArraySet<>();
languages.addAll(scores.getEntities());
languages.addAll(moreTextScores.getEntities());
for (String language : languages) {
final float score = (subjectTextScoreRatio * scores.getConfidenceScore(language)
+ moreTextScoreRatio * moreTextScores.getConfidenceScore(language))
* penalizeRatio;
newScores.put(language, score);
}
return new EntityConfidence(newScores);
}
/**
* Detect languages for the specified text.
*/
private EntityConfidence detectLanguages(String text) throws FileNotFoundException {
final LangIdModel langId = getLangIdImpl();
final LangIdModel.LanguageResult[] langResults = langId.detectLanguages(text);
final Map<String, Float> languagesMap = new ArrayMap<>();
for (LanguageResult langResult : langResults) {
languagesMap.put(langResult.getLanguage(), langResult.getScore());
}
return new EntityConfidence(languagesMap);
}
private float getLangIdThreshold() {
try {
return mSettings.getLangIdThresholdOverride() >= 0
? mSettings.getLangIdThresholdOverride()
: getLangIdImpl().getLangIdThreshold();
} catch (FileNotFoundException e) {
final float defaultThreshold = 0.5f;
Log.v(LOG_TAG, "Using default foreign language threshold: " + defaultThreshold);
return defaultThreshold;
}
}
@Override
public void dump(@NonNull IndentingPrintWriter printWriter) {
synchronized (mLock) {
printWriter.println("TextClassifierImpl:");
printWriter.increaseIndent();
printWriter.println("Annotator model file(s):");
printWriter.increaseIndent();
for (ModelFileManager.ModelFile modelFile :
mAnnotatorModelFileManager.listModelFiles()) {
printWriter.println(modelFile.toString());
}
printWriter.decreaseIndent();
printWriter.println("LangID model file(s):");
printWriter.increaseIndent();
for (ModelFileManager.ModelFile modelFile :
mLangIdModelFileManager.listModelFiles()) {
printWriter.println(modelFile.toString());
}
printWriter.decreaseIndent();
printWriter.println("Actions model file(s):");
printWriter.increaseIndent();
for (ModelFileManager.ModelFile modelFile :
mActionsModelFileManager.listModelFiles()) {
printWriter.println(modelFile.toString());
}
printWriter.decreaseIndent();
printWriter.printPair("mFallback", mFallback);
printWriter.decreaseIndent();
printWriter.println();
}
}
/**
* Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
*/
private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
if (fd == null) {
return;
}
try {
fd.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error closing file.", e);
}
}
/**
* Returns the locales string for the current resources configuration.
*/
private String getResourceLocalesString() {
try {
return mContext.getResources().getConfiguration().getLocales().toLanguageTags();
} catch (NullPointerException e) {
// NPE is unexpected. Erring on the side of caution.
return LocaleList.getDefault().toLanguageTags();
}
}
}

View File

@ -1,58 +0,0 @@
/*
* 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.intent;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import com.google.android.textclassifier.AnnotatorModel;
import java.time.Instant;
import java.util.List;
/**
* @hide
*/
public interface ClassificationIntentFactory {
/**
* Return a list of LabeledIntent from the classification result.
*/
List<LabeledIntent> create(
Context context,
String text,
boolean foreignText,
@Nullable Instant referenceTime,
@Nullable AnnotatorModel.ClassificationResult classification);
/**
* Inserts translate action to the list if it is a foreign text.
*/
static void insertTranslateAction(
List<LabeledIntent> actions, Context context, String text) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.translate),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.translate_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_TRANSLATE)
// TODO: Probably better to introduce a "translate" scheme instead of
// using EXTRA_TEXT.
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
}
}

View File

@ -1,219 +0,0 @@
/*
* 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.intent;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.textclassifier.ExtrasUtils;
import android.view.textclassifier.Log;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Objects;
/**
* Helper class to store the information from which RemoteActions are built.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class LabeledIntent {
private static final String TAG = "LabeledIntent";
public static final int DEFAULT_REQUEST_CODE = 0;
private static final TitleChooser DEFAULT_TITLE_CHOOSER =
(labeledIntent, resolveInfo) -> {
if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) {
return labeledIntent.titleWithEntity;
}
return labeledIntent.titleWithoutEntity;
};
@Nullable
public final String titleWithoutEntity;
@Nullable
public final String titleWithEntity;
public final String description;
@Nullable
public final String descriptionWithAppName;
// Do not update this intent.
public final Intent intent;
public final int requestCode;
/**
* Initializes a LabeledIntent.
*
* <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
* if distinguishing info (e.g. the classified text) is represented in intent extras only.
* In such circumstances, the request code should represent the distinguishing info
* (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
* unique. To be correct, the PendingIntent should be definitely unique but we try a
* best effort approach that avoids spamming the system with PendingIntents.
*/
// TODO: Fix the issue mentioned above so the behaviour is correct.
public LabeledIntent(
@Nullable String titleWithoutEntity,
@Nullable String titleWithEntity,
String description,
@Nullable String descriptionWithAppName,
Intent intent,
int requestCode) {
if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) {
throw new IllegalArgumentException(
"titleWithEntity and titleWithoutEntity should not be both null");
}
this.titleWithoutEntity = titleWithoutEntity;
this.titleWithEntity = titleWithEntity;
this.description = Objects.requireNonNull(description);
this.descriptionWithAppName = descriptionWithAppName;
this.intent = Objects.requireNonNull(intent);
this.requestCode = requestCode;
}
/**
* Return the resolved result.
*
* @param context the context to resolve the result's intent and action
* @param titleChooser for choosing an action title
* @param textLanguagesBundle containing language detection information
*/
@Nullable
public Result resolve(
Context context,
@Nullable TitleChooser titleChooser,
@Nullable Bundle textLanguagesBundle) {
final PackageManager pm = context.getPackageManager();
final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
if (resolveInfo == null || resolveInfo.activityInfo == null) {
Log.w(TAG, "resolveInfo or activityInfo is null");
return null;
}
final String packageName = resolveInfo.activityInfo.packageName;
final String className = resolveInfo.activityInfo.name;
if (packageName == null || className == null) {
Log.w(TAG, "packageName or className is null");
return null;
}
Intent resolvedIntent = new Intent(intent);
resolvedIntent.putExtra(
TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER,
getFromTextClassifierExtra(textLanguagesBundle));
boolean shouldShowIcon = false;
Icon icon = null;
if (!"android".equals(packageName)) {
// We only set the component name when the package name is not resolved to "android"
// to workaround a bug that explicit intent with component name == ResolverActivity
// can't be launched on keyguard.
resolvedIntent.setComponent(new ComponentName(packageName, className));
if (resolveInfo.activityInfo.getIconResource() != 0) {
icon = Icon.createWithResource(
packageName, resolveInfo.activityInfo.getIconResource());
shouldShowIcon = true;
}
}
if (icon == null) {
// RemoteAction requires that there be an icon.
icon = Icon.createWithResource(
"android", com.android.internal.R.drawable.ic_more_items);
}
final PendingIntent pendingIntent =
TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser;
CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
if (TextUtils.isEmpty(title)) {
Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
}
final RemoteAction action =
new RemoteAction(icon, title, resolveDescription(resolveInfo, pm), pendingIntent);
action.setShouldShowIcon(shouldShowIcon);
return new Result(resolvedIntent, action);
}
private String resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager) {
if (!TextUtils.isEmpty(descriptionWithAppName)) {
// Example string format of descriptionWithAppName: "Use %1$s to open map".
String applicationName = getApplicationName(resolveInfo, packageManager);
if (!TextUtils.isEmpty(applicationName)) {
return String.format(descriptionWithAppName, applicationName);
}
}
return description;
}
@Nullable
private String getApplicationName(
ResolveInfo resolveInfo, PackageManager packageManager) {
if (resolveInfo.activityInfo == null) {
return null;
}
if ("android".equals(resolveInfo.activityInfo.packageName)) {
return null;
}
if (resolveInfo.activityInfo.applicationInfo == null) {
return null;
}
return (String) packageManager.getApplicationLabel(
resolveInfo.activityInfo.applicationInfo);
}
private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) {
if (textLanguagesBundle != null) {
final Bundle bundle = new Bundle();
ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle);
return bundle;
} else {
return Bundle.EMPTY;
}
}
/**
* Data class that holds the result.
*/
public static final class Result {
public final Intent resolvedIntent;
public final RemoteAction remoteAction;
public Result(Intent resolvedIntent, RemoteAction remoteAction) {
this.resolvedIntent = Objects.requireNonNull(resolvedIntent);
this.remoteAction = Objects.requireNonNull(remoteAction);
}
}
/**
* An object to choose a title from resolved info. If {@code null} is returned,
* {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise.
*/
public interface TitleChooser {
/**
* Picks a title from a {@link LabeledIntent} by looking into resolved info.
* {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}.
*/
@Nullable
CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
}
}

View File

@ -1,280 +0,0 @@
/*
* 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.intent;
import static java.time.temporal.ChronoUnit.MILLIS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.SearchManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;
import android.provider.Browser;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.view.textclassifier.Log;
import android.view.textclassifier.TextClassifier;
import com.google.android.textclassifier.AnnotatorModel;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Creates intents based on the classification type.
* @hide
*/
// TODO: Consider to support {@code descriptionWithAppName}.
public final class LegacyClassificationIntentFactory implements ClassificationIntentFactory {
private static final String TAG = "LegacyClassificationIntentFactory";
private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
@NonNull
@Override
public List<LabeledIntent> create(Context context, String text, boolean foreignText,
@Nullable Instant referenceTime,
AnnotatorModel.ClassificationResult classification) {
final String type = classification != null
? classification.getCollection().trim().toLowerCase(Locale.ENGLISH)
: "";
text = text.trim();
final List<LabeledIntent> actions;
switch (type) {
case TextClassifier.TYPE_EMAIL:
actions = createForEmail(context, text);
break;
case TextClassifier.TYPE_PHONE:
actions = createForPhone(context, text);
break;
case TextClassifier.TYPE_ADDRESS:
actions = createForAddress(context, text);
break;
case TextClassifier.TYPE_URL:
actions = createForUrl(context, text);
break;
case TextClassifier.TYPE_DATE: // fall through
case TextClassifier.TYPE_DATE_TIME:
if (classification.getDatetimeResult() != null) {
final Instant parsedTime = Instant.ofEpochMilli(
classification.getDatetimeResult().getTimeMsUtc());
actions = createForDatetime(context, type, referenceTime, parsedTime);
} else {
actions = new ArrayList<>();
}
break;
case TextClassifier.TYPE_FLIGHT_NUMBER:
actions = createForFlight(context, text);
break;
case TextClassifier.TYPE_DICTIONARY:
actions = createForDictionary(context, text);
break;
default:
actions = new ArrayList<>();
break;
}
if (foreignText) {
ClassificationIntentFactory.insertTranslateAction(actions, context, text);
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForEmail(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.email),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.email_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("mailto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.add_contact_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.EMAIL, text),
text.hashCode()));
return actions;
}
@NonNull
private static List<LabeledIntent> createForPhone(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
final UserManager userManager = context.getSystemService(UserManager.class);
final Bundle userRestrictions = userManager != null
? userManager.getUserRestrictions() : new Bundle();
if (!userRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.dial),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.dial_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_DIAL).setData(
Uri.parse(String.format("tel:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.add_contact),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.add_contact_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_INSERT_OR_EDIT)
.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
.putExtra(ContactsContract.Intents.Insert.PHONE, text),
text.hashCode()));
if (!userRestrictions.getBoolean(UserManager.DISALLOW_SMS, false)) {
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.sms),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.sms_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("smsto:%s", text))),
LabeledIntent.DEFAULT_REQUEST_CODE));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForAddress(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
try {
final String encText = URLEncoder.encode(text, "UTF-8");
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.map),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.map_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(String.format("geo:0,0?q=%s", encText))),
LabeledIntent.DEFAULT_REQUEST_CODE));
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Could not encode address", e);
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForUrl(Context context, String text) {
if (Uri.parse(text).getScheme() == null) {
text = "http://" + text;
}
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.browse),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.browse_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_VIEW)
.setDataAndNormalize(Uri.parse(text))
.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()),
LabeledIntent.DEFAULT_REQUEST_CODE));
return actions;
}
@NonNull
private static List<LabeledIntent> createForDatetime(
Context context, String type, @Nullable Instant referenceTime,
Instant parsedTime) {
if (referenceTime == null) {
// If no reference time was given, use now.
referenceTime = Instant.now();
}
List<LabeledIntent> actions = new ArrayList<>();
actions.add(createCalendarViewIntent(context, parsedTime));
final long millisUntilEvent = referenceTime.until(parsedTime, MILLIS);
if (millisUntilEvent > MIN_EVENT_FUTURE_MILLIS) {
actions.add(createCalendarCreateEventIntent(context, parsedTime, type));
}
return actions;
}
@NonNull
private static List<LabeledIntent> createForFlight(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.view_flight),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.view_flight_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_WEB_SEARCH)
.putExtra(SearchManager.QUERY, text),
text.hashCode()));
return actions;
}
@NonNull
private static LabeledIntent createCalendarViewIntent(Context context, Instant parsedTime) {
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, parsedTime.toEpochMilli());
return new LabeledIntent(
context.getString(com.android.internal.R.string.view_calendar),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.view_calendar_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_VIEW).setData(builder.build()),
LabeledIntent.DEFAULT_REQUEST_CODE);
}
@NonNull
private static LabeledIntent createCalendarCreateEventIntent(
Context context, Instant parsedTime, @TextClassifier.EntityType String type) {
final boolean isAllDay = TextClassifier.TYPE_DATE.equals(type);
return new LabeledIntent(
context.getString(com.android.internal.R.string.add_calendar_event),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.add_calendar_event_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, isAllDay)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME,
parsedTime.toEpochMilli())
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME,
parsedTime.toEpochMilli() + DEFAULT_EVENT_DURATION),
parsedTime.hashCode());
}
@NonNull
private static List<LabeledIntent> createForDictionary(Context context, String text) {
final List<LabeledIntent> actions = new ArrayList<>();
actions.add(new LabeledIntent(
context.getString(com.android.internal.R.string.define),
/* titleWithEntity */ null,
context.getString(com.android.internal.R.string.define_desc),
/* descriptionWithAppName */ null,
new Intent(Intent.ACTION_DEFINE)
.putExtra(Intent.EXTRA_TEXT, text),
text.hashCode()));
return actions;
}
}

View File

@ -1,81 +0,0 @@
/*
* 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.intent;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.view.textclassifier.Log;
import android.view.textclassifier.TextClassifier;
import com.android.internal.annotations.VisibleForTesting;
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;
import java.util.Objects;
/**
* Creates intents based on {@link RemoteActionTemplate} objects for a ClassificationResult.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class TemplateClassificationIntentFactory implements ClassificationIntentFactory {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
private final TemplateIntentFactory mTemplateIntentFactory;
private final ClassificationIntentFactory mFallback;
public TemplateClassificationIntentFactory(TemplateIntentFactory templateIntentFactory,
ClassificationIntentFactory fallback) {
mTemplateIntentFactory = Objects.requireNonNull(templateIntentFactory);
mFallback = Objects.requireNonNull(fallback);
}
/**
* Returns a list of {@link LabeledIntent}
* that are constructed from the classification result.
*/
@NonNull
@Override
public List<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 (remoteActionTemplates == null) {
// RemoteActionTemplate is missing, fallback.
Log.w(TAG, "RemoteActionTemplate is missing, fallback to"
+ " LegacyClassificationIntentFactory.");
return mFallback.create(context, text, foreignText, referenceTime, classification);
}
final List<LabeledIntent> labeledIntents =
mTemplateIntentFactory.create(remoteActionTemplates);
if (foreignText) {
ClassificationIntentFactory.insertTranslateAction(labeledIntents, context, text.trim());
}
return labeledIntents;
}
}

View File

@ -1,156 +0,0 @@
/*
* 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.intent;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.textclassifier.Log;
import android.view.textclassifier.TextClassifier;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.textclassifier.NamedVariant;
import com.google.android.textclassifier.RemoteActionTemplate;
import java.util.ArrayList;
import java.util.List;
/**
* Creates intents based on {@link RemoteActionTemplate} objects.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class TemplateIntentFactory {
private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
/**
* Constructs and returns a list of {@link LabeledIntent} based on the given templates.
*/
@Nullable
public List<LabeledIntent> create(
@NonNull RemoteActionTemplate[] remoteActionTemplates) {
if (remoteActionTemplates.length == 0) {
return new ArrayList<>();
}
final List<LabeledIntent> labeledIntents = new ArrayList<>();
for (RemoteActionTemplate remoteActionTemplate : remoteActionTemplates) {
if (!isValidTemplate(remoteActionTemplate)) {
Log.w(TAG, "Invalid RemoteActionTemplate skipped.");
continue;
}
labeledIntents.add(
new LabeledIntent(
remoteActionTemplate.titleWithoutEntity,
remoteActionTemplate.titleWithEntity,
remoteActionTemplate.description,
remoteActionTemplate.descriptionWithAppName,
createIntent(remoteActionTemplate),
remoteActionTemplate.requestCode == null
? LabeledIntent.DEFAULT_REQUEST_CODE
: remoteActionTemplate.requestCode));
}
return labeledIntents;
}
private static boolean isValidTemplate(@Nullable RemoteActionTemplate remoteActionTemplate) {
if (remoteActionTemplate == null) {
Log.w(TAG, "Invalid RemoteActionTemplate: is null");
return false;
}
if (TextUtils.isEmpty(remoteActionTemplate.titleWithEntity)
&& TextUtils.isEmpty(remoteActionTemplate.titleWithoutEntity)) {
Log.w(TAG, "Invalid RemoteActionTemplate: title is null");
return false;
}
if (TextUtils.isEmpty(remoteActionTemplate.description)) {
Log.w(TAG, "Invalid RemoteActionTemplate: description is null");
return false;
}
if (!TextUtils.isEmpty(remoteActionTemplate.packageName)) {
Log.w(TAG, "Invalid RemoteActionTemplate: package name is set");
return false;
}
if (TextUtils.isEmpty(remoteActionTemplate.action)) {
Log.w(TAG, "Invalid RemoteActionTemplate: intent action not set");
return false;
}
return true;
}
private static Intent createIntent(RemoteActionTemplate remoteActionTemplate) {
final Intent intent = new Intent(remoteActionTemplate.action);
final Uri uri = TextUtils.isEmpty(remoteActionTemplate.data)
? null : Uri.parse(remoteActionTemplate.data).normalizeScheme();
final String type = TextUtils.isEmpty(remoteActionTemplate.type)
? null : Intent.normalizeMimeType(remoteActionTemplate.type);
intent.setDataAndType(uri, type);
intent.setFlags(remoteActionTemplate.flags == null ? 0 : remoteActionTemplate.flags);
if (remoteActionTemplate.category != null) {
for (String category : remoteActionTemplate.category) {
if (category != null) {
intent.addCategory(category);
}
}
}
intent.putExtras(nameVariantsToBundle(remoteActionTemplate.extras));
return intent;
}
/**
* Converts an array of {@link NamedVariant} to a Bundle and returns it.
*/
public static Bundle nameVariantsToBundle(@Nullable NamedVariant[] namedVariants) {
if (namedVariants == null) {
return Bundle.EMPTY;
}
Bundle bundle = new Bundle();
for (NamedVariant namedVariant : namedVariants) {
if (namedVariant == null) {
continue;
}
switch (namedVariant.getType()) {
case NamedVariant.TYPE_INT:
bundle.putInt(namedVariant.getName(), namedVariant.getInt());
break;
case NamedVariant.TYPE_LONG:
bundle.putLong(namedVariant.getName(), namedVariant.getLong());
break;
case NamedVariant.TYPE_FLOAT:
bundle.putFloat(namedVariant.getName(), namedVariant.getFloat());
break;
case NamedVariant.TYPE_DOUBLE:
bundle.putDouble(namedVariant.getName(), namedVariant.getDouble());
break;
case NamedVariant.TYPE_BOOL:
bundle.putBoolean(namedVariant.getName(), namedVariant.getBool());
break;
case NamedVariant.TYPE_STRING:
bundle.putString(namedVariant.getName(), namedVariant.getString());
break;
default:
Log.w(TAG,
"Unsupported type found in nameVariantsToBundle : "
+ namedVariant.getType());
}
}
return bundle;
}
}

View File

@ -1,599 +0,0 @@
/*
* Copyright (C) 2017 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.logging;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.metrics.LogMaker;
import android.os.Build;
import android.util.Log;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextSelection;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.UUID;
/**
* A selection event tracker.
* @hide
*/
//TODO: Do not allow any crashes from this class.
public final class SmartSelectionEventTracker {
private static final String LOG_TAG = "SmartSelectEventTracker";
private static final boolean DEBUG_LOG_ENABLED = true;
private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS;
private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX;
private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE;
private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION;
private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE;
private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START;
private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END;
private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START;
private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END;
private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID;
private static final String ZERO = "0";
private static final String TEXTVIEW = "textview";
private static final String EDITTEXT = "edittext";
private static final String UNSELECTABLE_TEXTVIEW = "nosel-textview";
private static final String WEBVIEW = "webview";
private static final String EDIT_WEBVIEW = "edit-webview";
private static final String CUSTOM_TEXTVIEW = "customview";
private static final String CUSTOM_EDITTEXT = "customedit";
private static final String CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
private static final String UNKNOWN = "unknown";
@Retention(RetentionPolicy.SOURCE)
@IntDef({WidgetType.UNSPECIFIED, WidgetType.TEXTVIEW, WidgetType.WEBVIEW,
WidgetType.EDITTEXT, WidgetType.EDIT_WEBVIEW})
public @interface WidgetType {
int UNSPECIFIED = 0;
int TEXTVIEW = 1;
int WEBVIEW = 2;
int EDITTEXT = 3;
int EDIT_WEBVIEW = 4;
int UNSELECTABLE_TEXTVIEW = 5;
int CUSTOM_TEXTVIEW = 6;
int CUSTOM_EDITTEXT = 7;
int CUSTOM_UNSELECTABLE_TEXTVIEW = 8;
}
private final MetricsLogger mMetricsLogger = new MetricsLogger();
private final int mWidgetType;
@Nullable private final String mWidgetVersion;
private final Context mContext;
@Nullable private String mSessionId;
private final int[] mSmartIndices = new int[2];
private final int[] mPrevIndices = new int[2];
private int mOrigStart;
private int mIndex;
private long mSessionStartTime;
private long mLastEventTime;
private boolean mSmartSelectionTriggered;
private String mModelName;
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) {
mWidgetType = widgetType;
mWidgetVersion = null;
mContext = Objects.requireNonNull(context);
}
public SmartSelectionEventTracker(
@NonNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion) {
mWidgetType = widgetType;
mWidgetVersion = widgetVersion;
mContext = Objects.requireNonNull(context);
}
/**
* Logs a selection event.
*
* @param event the selection event
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public void logEvent(@NonNull SelectionEvent event) {
Objects.requireNonNull(event);
if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null
&& DEBUG_LOG_ENABLED) {
Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
return;
}
final long now = System.currentTimeMillis();
switch (event.mEventType) {
case SelectionEvent.EventType.SELECTION_STARTED:
mSessionId = startNewSession();
Preconditions.checkArgument(event.mEnd == event.mStart + 1);
mOrigStart = event.mStart;
mSessionStartTime = now;
break;
case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through
case SelectionEvent.EventType.SMART_SELECTION_MULTI:
mSmartSelectionTriggered = true;
mModelName = getModelName(event);
mSmartIndices[0] = event.mStart;
mSmartIndices[1] = event.mEnd;
break;
case SelectionEvent.EventType.SELECTION_MODIFIED: // fall through
case SelectionEvent.EventType.AUTO_SELECTION:
if (mPrevIndices[0] == event.mStart && mPrevIndices[1] == event.mEnd) {
// Selection did not change. Ignore event.
return;
}
}
writeEvent(event, now);
if (event.isTerminal()) {
endSession();
}
}
private void writeEvent(SelectionEvent event, long now) {
final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime;
final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
.setType(getLogType(event))
.setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL)
.setPackageName(mContext.getPackageName())
.addTaggedData(START_EVENT_DELTA, now - mSessionStartTime)
.addTaggedData(PREV_EVENT_DELTA, prevEventDelta)
.addTaggedData(INDEX, mIndex)
.addTaggedData(WIDGET_TYPE, getWidgetTypeName())
.addTaggedData(WIDGET_VERSION, mWidgetVersion)
.addTaggedData(MODEL_NAME, mModelName)
.addTaggedData(ENTITY_TYPE, event.mEntityType)
.addTaggedData(SMART_START, getSmartRangeDelta(mSmartIndices[0]))
.addTaggedData(SMART_END, getSmartRangeDelta(mSmartIndices[1]))
.addTaggedData(EVENT_START, getRangeDelta(event.mStart))
.addTaggedData(EVENT_END, getRangeDelta(event.mEnd))
.addTaggedData(SESSION_ID, mSessionId);
mMetricsLogger.write(log);
debugLog(log);
mLastEventTime = now;
mPrevIndices[0] = event.mStart;
mPrevIndices[1] = event.mEnd;
mIndex++;
}
private String startNewSession() {
endSession();
mSessionId = createSessionId();
return mSessionId;
}
private void endSession() {
// Reset fields.
mOrigStart = 0;
mSmartIndices[0] = mSmartIndices[1] = 0;
mPrevIndices[0] = mPrevIndices[1] = 0;
mIndex = 0;
mSessionStartTime = 0;
mLastEventTime = 0;
mSmartSelectionTriggered = false;
mModelName = getModelName(null);
mSessionId = null;
}
private static int getLogType(SelectionEvent event) {
switch (event.mEventType) {
case SelectionEvent.ActionType.OVERTYPE:
return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE;
case SelectionEvent.ActionType.COPY:
return MetricsEvent.ACTION_TEXT_SELECTION_COPY;
case SelectionEvent.ActionType.PASTE:
return MetricsEvent.ACTION_TEXT_SELECTION_PASTE;
case SelectionEvent.ActionType.CUT:
return MetricsEvent.ACTION_TEXT_SELECTION_CUT;
case SelectionEvent.ActionType.SHARE:
return MetricsEvent.ACTION_TEXT_SELECTION_SHARE;
case SelectionEvent.ActionType.SMART_SHARE:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
case SelectionEvent.ActionType.DRAG:
return MetricsEvent.ACTION_TEXT_SELECTION_DRAG;
case SelectionEvent.ActionType.ABANDON:
return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON;
case SelectionEvent.ActionType.OTHER:
return MetricsEvent.ACTION_TEXT_SELECTION_OTHER;
case SelectionEvent.ActionType.SELECT_ALL:
return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL;
case SelectionEvent.ActionType.RESET:
return MetricsEvent.ACTION_TEXT_SELECTION_RESET;
case SelectionEvent.EventType.SELECTION_STARTED:
return MetricsEvent.ACTION_TEXT_SELECTION_START;
case SelectionEvent.EventType.SELECTION_MODIFIED:
return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY;
case SelectionEvent.EventType.SMART_SELECTION_SINGLE:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE;
case SelectionEvent.EventType.SMART_SELECTION_MULTI:
return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI;
case SelectionEvent.EventType.AUTO_SELECTION:
return MetricsEvent.ACTION_TEXT_SELECTION_AUTO;
default:
return MetricsEvent.VIEW_UNKNOWN;
}
}
private static String getLogTypeString(int logType) {
switch (logType) {
case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE:
return "OVERTYPE";
case MetricsEvent.ACTION_TEXT_SELECTION_COPY:
return "COPY";
case MetricsEvent.ACTION_TEXT_SELECTION_PASTE:
return "PASTE";
case MetricsEvent.ACTION_TEXT_SELECTION_CUT:
return "CUT";
case MetricsEvent.ACTION_TEXT_SELECTION_SHARE:
return "SHARE";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
return "SMART_SHARE";
case MetricsEvent.ACTION_TEXT_SELECTION_DRAG:
return "DRAG";
case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON:
return "ABANDON";
case MetricsEvent.ACTION_TEXT_SELECTION_OTHER:
return "OTHER";
case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL:
return "SELECT_ALL";
case MetricsEvent.ACTION_TEXT_SELECTION_RESET:
return "RESET";
case MetricsEvent.ACTION_TEXT_SELECTION_START:
return "SELECTION_STARTED";
case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY:
return "SELECTION_MODIFIED";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE:
return "SMART_SELECTION_SINGLE";
case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI:
return "SMART_SELECTION_MULTI";
case MetricsEvent.ACTION_TEXT_SELECTION_AUTO:
return "AUTO_SELECTION";
default:
return UNKNOWN;
}
}
private int getRangeDelta(int offset) {
return offset - mOrigStart;
}
private int getSmartRangeDelta(int offset) {
return mSmartSelectionTriggered ? getRangeDelta(offset) : 0;
}
private String getWidgetTypeName() {
switch (mWidgetType) {
case WidgetType.TEXTVIEW:
return TEXTVIEW;
case WidgetType.WEBVIEW:
return WEBVIEW;
case WidgetType.EDITTEXT:
return EDITTEXT;
case WidgetType.EDIT_WEBVIEW:
return EDIT_WEBVIEW;
case WidgetType.UNSELECTABLE_TEXTVIEW:
return UNSELECTABLE_TEXTVIEW;
case WidgetType.CUSTOM_TEXTVIEW:
return CUSTOM_TEXTVIEW;
case WidgetType.CUSTOM_EDITTEXT:
return CUSTOM_EDITTEXT;
case WidgetType.CUSTOM_UNSELECTABLE_TEXTVIEW:
return CUSTOM_UNSELECTABLE_TEXTVIEW;
default:
return UNKNOWN;
}
}
private String getModelName(@Nullable SelectionEvent event) {
return event == null
? SelectionEvent.NO_VERSION_TAG
: Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG);
}
private static String createSessionId() {
return UUID.randomUUID().toString();
}
private static void debugLog(LogMaker log) {
if (!DEBUG_LOG_ENABLED) return;
final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN);
final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), "");
final String widget = widgetVersion.isEmpty()
? widgetType : widgetType + "-" + widgetVersion;
final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO));
if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) {
String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), "");
sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1);
Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId));
}
final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN);
final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN);
final String type = getLogTypeString(log.getType());
final int smartStart = Integer.parseInt(
Objects.toString(log.getTaggedData(SMART_START), ZERO));
final int smartEnd = Integer.parseInt(
Objects.toString(log.getTaggedData(SMART_END), ZERO));
final int eventStart = Integer.parseInt(
Objects.toString(log.getTaggedData(EVENT_START), ZERO));
final int eventEnd = Integer.parseInt(
Objects.toString(log.getTaggedData(EVENT_END), ZERO));
Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model));
}
/**
* A selection event.
* Specify index parameters as word token indices.
*/
public static final class SelectionEvent {
/**
* Use this to specify an indeterminate positive index.
*/
public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE;
/**
* Use this to specify an indeterminate negative index.
*/
public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE;
private static final String NO_VERSION_TAG = "";
@Retention(RetentionPolicy.SOURCE)
@IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT,
ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON,
ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET})
public @interface ActionType {
/** User typed over the selection. */
int OVERTYPE = 100;
/** User copied the selection. */
int COPY = 101;
/** User pasted over the selection. */
int PASTE = 102;
/** User cut the selection. */
int CUT = 103;
/** User shared the selection. */
int SHARE = 104;
/** User clicked the textAssist menu item. */
int SMART_SHARE = 105;
/** User dragged+dropped the selection. */
int DRAG = 106;
/** User abandoned the selection. */
int ABANDON = 107;
/** User performed an action on the selection. */
int OTHER = 108;
/* Non-terminal actions. */
/** User activated Select All */
int SELECT_ALL = 200;
/** User reset the smart selection. */
int RESET = 201;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT,
ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON,
ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET,
EventType.SELECTION_STARTED, EventType.SELECTION_MODIFIED,
EventType.SMART_SELECTION_SINGLE, EventType.SMART_SELECTION_MULTI,
EventType.AUTO_SELECTION})
private @interface EventType {
/** User started a new selection. */
int SELECTION_STARTED = 1;
/** User modified an existing selection. */
int SELECTION_MODIFIED = 2;
/** Smart selection triggered for a single token (word). */
int SMART_SELECTION_SINGLE = 3;
/** Smart selection triggered spanning multiple tokens (words). */
int SMART_SELECTION_MULTI = 4;
/** Something else other than User or the default TextClassifier triggered a selection. */
int AUTO_SELECTION = 5;
}
private final int mStart;
private final int mEnd;
private @EventType int mEventType;
private final @TextClassifier.EntityType String mEntityType;
private final String mVersionTag;
private SelectionEvent(
int start, int end, int eventType,
@TextClassifier.EntityType String entityType, String versionTag) {
Preconditions.checkArgument(end >= start, "end cannot be less than start");
mStart = start;
mEnd = end;
mEventType = eventType;
mEntityType = Objects.requireNonNull(entityType);
mVersionTag = Objects.requireNonNull(versionTag);
}
/**
* Creates a "selection started" event.
*
* @param start the word index of the selected word
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionStarted(int start) {
return new SelectionEvent(
start, start + 1, EventType.SELECTION_STARTED,
TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
}
/**
* Creates a "selection modified" event.
* Use when the user modifies the selection.
*
* @param start the start word (inclusive) index of the selection
* @param end the end word (exclusive) index of the selection
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionModified(int start, int end) {
return new SelectionEvent(
start, end, EventType.SELECTION_MODIFIED,
TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
}
/**
* Creates a "selection modified" event.
* Use when the user modifies the selection and the selection's entity type is known.
*
* @param start the start word (inclusive) index of the selection
* @param end the end word (exclusive) index of the selection
* @param classification the TextClassification object returned by the TextClassifier that
* classified the selected text
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionModified(
int start, int end, @NonNull TextClassification classification) {
final String entityType = classification.getEntityCount() > 0
? classification.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
final String versionTag = getVersionInfo(classification.getId());
return new SelectionEvent(
start, end, EventType.SELECTION_MODIFIED, entityType, versionTag);
}
/**
* Creates a "selection modified" event.
* Use when a TextClassifier modifies the selection.
*
* @param start the start word (inclusive) index of the selection
* @param end the end word (exclusive) index of the selection
* @param selection the TextSelection object returned by the TextClassifier for the
* specified selection
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionModified(
int start, int end, @NonNull TextSelection selection) {
final boolean smartSelection = getSourceClassifier(selection.getId())
.equals(TextClassifier.DEFAULT_LOG_TAG);
final int eventType;
if (smartSelection) {
eventType = end - start > 1
? EventType.SMART_SELECTION_MULTI
: EventType.SMART_SELECTION_SINGLE;
} else {
eventType = EventType.AUTO_SELECTION;
}
final String entityType = selection.getEntityCount() > 0
? selection.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
final String versionTag = getVersionInfo(selection.getId());
return new SelectionEvent(start, end, eventType, entityType, versionTag);
}
/**
* Creates an event specifying an action taken on a selection.
* Use when the user clicks on an action to act on the selected text.
*
* @param start the start word (inclusive) index of the selection
* @param end the end word (exclusive) index of the selection
* @param actionType the action that was performed on the selection
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionAction(
int start, int end, @ActionType int actionType) {
return new SelectionEvent(
start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
}
/**
* Creates an event specifying an action taken on a selection.
* Use when the user clicks on an action to act on the selected text and the selection's
* entity type is known.
*
* @param start the start word (inclusive) index of the selection
* @param end the end word (exclusive) index of the selection
* @param actionType the action that was performed on the selection
* @param classification the TextClassification object returned by the TextClassifier that
* classified the selected text
*/
@UnsupportedAppUsage(trackingBug = 136637107, maxTargetSdk = Build.VERSION_CODES.Q,
publicAlternatives = "See {@link android.view.textclassifier.TextClassifier}.")
public static SelectionEvent selectionAction(
int start, int end, @ActionType int actionType,
@NonNull TextClassification classification) {
final String entityType = classification.getEntityCount() > 0
? classification.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
final String versionTag = getVersionInfo(classification.getId());
return new SelectionEvent(start, end, actionType, entityType, versionTag);
}
@VisibleForTesting
public static String getVersionInfo(String signature) {
final int start = signature.indexOf("|") + 1;
final int end = signature.indexOf("|", start);
if (start >= 1 && end >= start) {
return signature.substring(start, end);
}
return "";
}
private static String getSourceClassifier(String signature) {
final int end = signature.indexOf("|");
if (end >= 0) {
return signature.substring(0, end);
}
return "";
}
private boolean isTerminal() {
switch (mEventType) {
case ActionType.OVERTYPE: // fall through
case ActionType.COPY: // fall through
case ActionType.PASTE: // fall through
case ActionType.CUT: // fall through
case ActionType.SHARE: // fall through
case ActionType.SMART_SHARE: // fall through
case ActionType.DRAG: // fall through
case ActionType.ABANDON: // fall through
case ActionType.OTHER: // fall through
return true;
default:
return false;
}
}
}
}

View File

@ -39,7 +39,6 @@ import android.view.ActionMode;
import android.view.textclassifier.ExtrasUtils;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.SelectionEvent.InvocationMethod;
import android.view.textclassifier.SelectionSessionLogger;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationContext;
@ -705,7 +704,7 @@ public final class SelectionActionModeHelper {
SelectionMetricsLogger(TextView textView) {
Objects.requireNonNull(textView);
mEditTextLogger = textView.isTextEditable();
mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
mTokenIterator = BreakIterator.getWordInstance(textView.getTextLocale());
}
public void logSelectionStarted(

View File

@ -1,95 +0,0 @@
/*
* 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 androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.Collections;
import java.util.Locale;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ActionsModelParamsSupplierTest {
@Test
public void getSerializedPreconditions_validActionsModelParams() {
ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
new File("/model/file"),
200 /* version */,
Collections.singletonList(Locale.forLanguageTag("en")),
"en",
false);
byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
ActionsModelParamsSupplier.ActionsModelParams params =
new ActionsModelParamsSupplier.ActionsModelParams(
200 /* version */,
"en",
serializedPreconditions);
byte[] actual = params.getSerializedPreconditions(modelFile);
assertThat(actual).isEqualTo(serializedPreconditions);
}
@Test
public void getSerializedPreconditions_invalidVersion() {
ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
new File("/model/file"),
201 /* version */,
Collections.singletonList(Locale.forLanguageTag("en")),
"en",
false);
byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
ActionsModelParamsSupplier.ActionsModelParams params =
new ActionsModelParamsSupplier.ActionsModelParams(
200 /* version */,
"en",
serializedPreconditions);
byte[] actual = params.getSerializedPreconditions(modelFile);
assertThat(actual).isNull();
}
@Test
public void getSerializedPreconditions_invalidLocales() {
final String LANGUAGE_TAG = "zh";
ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
new File("/model/file"),
200 /* version */,
Collections.singletonList(Locale.forLanguageTag(LANGUAGE_TAG)),
LANGUAGE_TAG,
false);
byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
ActionsModelParamsSupplier.ActionsModelParams params =
new ActionsModelParamsSupplier.ActionsModelParams(
200 /* version */,
"en",
serializedPreconditions);
byte[] actual = params.getSerializedPreconditions(modelFile);
assertThat(actual).isNull();
}
}

View File

@ -1,331 +0,0 @@
/*
* 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 android.view.textclassifier.ConversationActions.Message.PERSON_USER_OTHERS;
import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_SELF;
import static com.google.common.truth.Truth.assertThat;
import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.view.textclassifier.intent.LabeledIntent;
import android.view.textclassifier.intent.TemplateIntentFactory;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.textclassifier.ActionsSuggestionsModel;
import com.google.android.textclassifier.RemoteActionTemplate;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ActionsSuggestionsHelperTest {
private static final String LOCALE_TAG = Locale.US.toLanguageTag();
private static final Function<CharSequence, String> LANGUAGE_DETECTOR =
charSequence -> LOCALE_TAG;
@Test
public void testToNativeMessages_emptyInput() {
ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
ActionsSuggestionsHelper.toNativeMessages(
Collections.emptyList(), LANGUAGE_DETECTOR);
assertThat(conversationMessages).isEmpty();
}
@Test
public void testToNativeMessages_noTextMessages() {
ConversationActions.Message messageWithoutText =
new ConversationActions.Message.Builder(PERSON_USER_OTHERS).build();
ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
ActionsSuggestionsHelper.toNativeMessages(
Collections.singletonList(messageWithoutText), LANGUAGE_DETECTOR);
assertThat(conversationMessages).isEmpty();
}
@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(userB)
.setText("first")
.build();
ConversationActions.Message secondMessage =
new ConversationActions.Message.Builder(userA)
.setText("second")
.build();
ConversationActions.Message thirdMessage =
new ConversationActions.Message.Builder(PERSON_USER_SELF)
.setText("third")
.build();
ConversationActions.Message fourthMessage =
new ConversationActions.Message.Builder(userA)
.setText("fourth")
.build();
ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
ActionsSuggestionsHelper.toNativeMessages(
Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage),
LANGUAGE_DETECTOR);
assertThat(conversationMessages).hasLength(4);
assertNativeMessage(conversationMessages[0], firstMessage.getText(), 2, 0);
assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 0, 0);
assertNativeMessage(conversationMessages[3], fourthMessage.getText(), 1, 0);
}
@Test
public void testToNativeMessages_referenceTime() {
ConversationActions.Message firstMessage =
new ConversationActions.Message.Builder(PERSON_USER_OTHERS)
.setText("first")
.setReferenceTime(createZonedDateTimeFromMsUtc(1000))
.build();
ConversationActions.Message secondMessage =
new ConversationActions.Message.Builder(PERSON_USER_OTHERS)
.setText("second")
.build();
ConversationActions.Message thirdMessage =
new ConversationActions.Message.Builder(PERSON_USER_OTHERS)
.setText("third")
.setReferenceTime(createZonedDateTimeFromMsUtc(2000))
.build();
ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
ActionsSuggestionsHelper.toNativeMessages(
Arrays.asList(firstMessage, secondMessage, thirdMessage),
LANGUAGE_DETECTOR);
assertThat(conversationMessages).hasLength(3);
assertNativeMessage(conversationMessages[0], firstMessage.getText(), 1, 1000);
assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 1, 2000);
}
@Test
public void testDeduplicateActions() {
Bundle phoneExtras = new Bundle();
Intent phoneIntent = new Intent();
phoneIntent.setComponent(new ComponentName("phone", "intent"));
ExtrasUtils.putActionIntent(phoneExtras, phoneIntent);
Bundle anotherPhoneExtras = new Bundle();
Intent anotherPhoneIntent = new Intent();
anotherPhoneIntent.setComponent(new ComponentName("phone", "another.intent"));
ExtrasUtils.putActionIntent(anotherPhoneExtras, anotherPhoneIntent);
Bundle urlExtras = new Bundle();
Intent urlIntent = new Intent();
urlIntent.setComponent(new ComponentName("url", "intent"));
ExtrasUtils.putActionIntent(urlExtras, urlIntent);
PendingIntent pendingIntent = PendingIntent.getActivity(
InstrumentationRegistry.getTargetContext(),
0,
phoneIntent,
0);
Icon icon = Icon.createWithData(new byte[0], 0, 0);
ConversationAction action =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(icon, "label", "1", pendingIntent))
.setExtras(phoneExtras)
.build();
ConversationAction actionWithSameLabel =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(
icon, "label", "2", pendingIntent))
.setExtras(phoneExtras)
.build();
ConversationAction actionWithSamePackageButDifferentClass =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(
icon, "label", "3", pendingIntent))
.setExtras(anotherPhoneExtras)
.build();
ConversationAction actionWithDifferentLabel =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(
icon, "another_label", "4", pendingIntent))
.setExtras(phoneExtras)
.build();
ConversationAction actionWithDifferentPackage =
new ConversationAction.Builder(ConversationAction.TYPE_OPEN_URL)
.setAction(new RemoteAction(icon, "label", "5", pendingIntent))
.setExtras(urlExtras)
.build();
ConversationAction actionWithoutRemoteAction =
new ConversationAction.Builder(ConversationAction.TYPE_CREATE_REMINDER)
.build();
List<ConversationAction> conversationActions =
ActionsSuggestionsHelper.removeActionsWithDuplicates(
Arrays.asList(action, actionWithSameLabel,
actionWithSamePackageButDifferentClass, actionWithDifferentLabel,
actionWithDifferentPackage, actionWithoutRemoteAction));
assertThat(conversationActions).hasSize(3);
assertThat(conversationActions.get(0).getAction().getContentDescription()).isEqualTo("4");
assertThat(conversationActions.get(1).getAction().getContentDescription()).isEqualTo("5");
assertThat(conversationActions.get(2).getAction()).isNull();
}
@Test
public void testDeduplicateActions_nullComponent() {
Bundle phoneExtras = new Bundle();
Intent phoneIntent = new Intent(Intent.ACTION_DIAL);
ExtrasUtils.putActionIntent(phoneExtras, phoneIntent);
PendingIntent pendingIntent = PendingIntent.getActivity(
InstrumentationRegistry.getTargetContext(),
0,
phoneIntent,
0);
Icon icon = Icon.createWithData(new byte[0], 0, 0);
ConversationAction action =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(icon, "label", "1", pendingIntent))
.setExtras(phoneExtras)
.build();
ConversationAction actionWithSameLabel =
new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
.setAction(new RemoteAction(
icon, "label", "2", pendingIntent))
.setExtras(phoneExtras)
.build();
List<ConversationAction> conversationActions =
ActionsSuggestionsHelper.removeActionsWithDuplicates(
Arrays.asList(action, actionWithSameLabel));
assertThat(conversationActions).isEmpty();
}
@Test
public void createLabeledIntentResult_null() {
ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
new ActionsSuggestionsModel.ActionSuggestion(
"text",
ConversationAction.TYPE_OPEN_URL,
1.0f,
null,
null,
null
);
LabeledIntent.Result labeledIntentResult =
ActionsSuggestionsHelper.createLabeledIntentResult(
InstrumentationRegistry.getTargetContext(),
new TemplateIntentFactory(),
nativeSuggestion);
assertThat(labeledIntentResult).isNull();
}
@Test
public void createLabeledIntentResult_emptyList() {
ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
new ActionsSuggestionsModel.ActionSuggestion(
"text",
ConversationAction.TYPE_OPEN_URL,
1.0f,
null,
null,
new RemoteActionTemplate[0]
);
LabeledIntent.Result labeledIntentResult =
ActionsSuggestionsHelper.createLabeledIntentResult(
InstrumentationRegistry.getTargetContext(),
new TemplateIntentFactory(),
nativeSuggestion);
assertThat(labeledIntentResult).isNull();
}
@Test
public void createLabeledIntentResult() {
ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
new ActionsSuggestionsModel.ActionSuggestion(
"text",
ConversationAction.TYPE_OPEN_URL,
1.0f,
null,
null,
new RemoteActionTemplate[]{
new RemoteActionTemplate(
"title",
null,
"description",
null,
Intent.ACTION_VIEW,
Uri.parse("http://www.android.com").toString(),
null,
0,
null,
null,
null,
0)});
LabeledIntent.Result labeledIntentResult =
ActionsSuggestionsHelper.createLabeledIntentResult(
InstrumentationRegistry.getTargetContext(),
new TemplateIntentFactory(),
nativeSuggestion);
assertThat(labeledIntentResult.remoteAction.getTitle()).isEqualTo("title");
assertThat(labeledIntentResult.resolvedIntent.getAction()).isEqualTo(Intent.ACTION_VIEW);
}
private ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneId.of("UTC"));
}
private static void assertNativeMessage(
ActionsSuggestionsModel.ConversationMessage nativeMessage,
CharSequence text,
int userId,
long referenceTimeInMsUtc) {
assertThat(nativeMessage.getText()).isEqualTo(text.toString());
assertThat(nativeMessage.getUserId()).isEqualTo(userId);
assertThat(nativeMessage.getDetectedTextLanguageTags()).isEqualTo(LOCALE_TAG);
assertThat(nativeMessage.getReferenceTimeMsUtc()).isEqualTo(referenceTimeInMsUtc);
}
}

View File

@ -1,350 +0,0 @@
/*
* 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 static org.mockito.Mockito.when;
import android.os.LocaleList;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ModelFileManagerTest {
private static final Locale DEFAULT_LOCALE = Locale.forLanguageTag("en-US");
@Mock
private Supplier<List<ModelFileManager.ModelFile>> mModelFileSupplier;
private ModelFileManager.ModelFileSupplierImpl mModelFileSupplierImpl;
private ModelFileManager mModelFileManager;
private File mRootTestDir;
private File mFactoryModelDir;
private File mUpdatedModelFile;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mModelFileManager = new ModelFileManager(mModelFileSupplier);
mRootTestDir = InstrumentationRegistry.getContext().getCacheDir();
mFactoryModelDir = new File(mRootTestDir, "factory");
mUpdatedModelFile = new File(mRootTestDir, "updated.model");
mModelFileSupplierImpl =
new ModelFileManager.ModelFileSupplierImpl(
mFactoryModelDir,
"test\\d.model",
mUpdatedModelFile,
fd -> 1,
fd -> ModelFileManager.ModelFile.LANGUAGE_INDEPENDENT
);
mRootTestDir.mkdirs();
mFactoryModelDir.mkdirs();
Locale.setDefault(DEFAULT_LOCALE);
}
@After
public void removeTestDir() {
recursiveDelete(mRootTestDir);
}
@Test
public void get() {
ModelFileManager.ModelFile modelFile =
new ModelFileManager.ModelFile(
new File("/path/a"), 1, Collections.emptyList(), "", true);
when(mModelFileSupplier.get()).thenReturn(Collections.singletonList(modelFile));
List<ModelFileManager.ModelFile> modelFiles = mModelFileManager.listModelFiles();
assertThat(modelFiles).hasSize(1);
assertThat(modelFiles.get(0)).isEqualTo(modelFile);
}
@Test
public void findBestModel_versionCode() {
ModelFileManager.ModelFile olderModelFile =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.emptyList(), "", true);
ModelFileManager.ModelFile newerModelFile =
new ModelFileManager.ModelFile(
new File("/path/b"), 2,
Collections.emptyList(), "", true);
when(mModelFileSupplier.get())
.thenReturn(Arrays.asList(olderModelFile, newerModelFile));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(LocaleList.getEmptyLocaleList());
assertThat(bestModelFile).isEqualTo(newerModelFile);
}
@Test
public void findBestModel_languageDependentModelIsPreferred() {
Locale locale = Locale.forLanguageTag("ja");
ModelFileManager.ModelFile languageIndependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.emptyList(), "", true);
ModelFileManager.ModelFile languageDependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.singletonList(locale), locale.toLanguageTag(), false);
when(mModelFileSupplier.get())
.thenReturn(
Arrays.asList(languageIndependentModelFile, languageDependentModelFile));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(
LocaleList.forLanguageTags(locale.toLanguageTag()));
assertThat(bestModelFile).isEqualTo(languageDependentModelFile);
}
@Test
public void findBestModel_noMatchedLanguageModel() {
Locale locale = Locale.forLanguageTag("ja");
ModelFileManager.ModelFile languageIndependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.emptyList(), "", true);
ModelFileManager.ModelFile languageDependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.singletonList(locale), locale.toLanguageTag(), false);
when(mModelFileSupplier.get())
.thenReturn(
Arrays.asList(languageIndependentModelFile, languageDependentModelFile));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(
LocaleList.forLanguageTags("zh-hk"));
assertThat(bestModelFile).isEqualTo(languageIndependentModelFile);
}
@Test
public void findBestModel_noMatchedLanguageModel_defaultLocaleModelExists() {
ModelFileManager.ModelFile languageIndependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.emptyList(), "", true);
ModelFileManager.ModelFile languageDependentModelFile =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.singletonList(
DEFAULT_LOCALE), DEFAULT_LOCALE.toLanguageTag(), false);
when(mModelFileSupplier.get())
.thenReturn(
Arrays.asList(languageIndependentModelFile, languageDependentModelFile));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(
LocaleList.forLanguageTags("zh-hk"));
assertThat(bestModelFile).isEqualTo(languageIndependentModelFile);
}
@Test
public void findBestModel_languageIsMoreImportantThanVersion() {
ModelFileManager.ModelFile matchButOlderModel =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("fr")), "fr", false);
ModelFileManager.ModelFile mismatchButNewerModel =
new ModelFileManager.ModelFile(
new File("/path/b"), 2,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
when(mModelFileSupplier.get())
.thenReturn(
Arrays.asList(matchButOlderModel, mismatchButNewerModel));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(
LocaleList.forLanguageTags("fr"));
assertThat(bestModelFile).isEqualTo(matchButOlderModel);
}
@Test
public void findBestModel_languageIsMoreImportantThanVersion_bestModelComesFirst() {
ModelFileManager.ModelFile matchLocaleModel =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
ModelFileManager.ModelFile languageIndependentModel =
new ModelFileManager.ModelFile(
new File("/path/a"), 2,
Collections.emptyList(), "", true);
when(mModelFileSupplier.get())
.thenReturn(
Arrays.asList(matchLocaleModel, languageIndependentModel));
ModelFileManager.ModelFile bestModelFile =
mModelFileManager.findBestModelFile(
LocaleList.forLanguageTags("ja"));
assertThat(bestModelFile).isEqualTo(matchLocaleModel);
}
@Test
public void modelFileEquals() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
ModelFileManager.ModelFile modelB =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
assertThat(modelA).isEqualTo(modelB);
}
@Test
public void modelFile_different() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
ModelFileManager.ModelFile modelB =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
assertThat(modelA).isNotEqualTo(modelB);
}
@Test
public void modelFile_getPath() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
assertThat(modelA.getPath()).isEqualTo("/path/a");
}
@Test
public void modelFile_getName() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
assertThat(modelA.getName()).isEqualTo("a");
}
@Test
public void modelFile_isPreferredTo_languageDependentIsBetter() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 1,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
ModelFileManager.ModelFile modelB =
new ModelFileManager.ModelFile(
new File("/path/b"), 2,
Collections.emptyList(), "", true);
assertThat(modelA.isPreferredTo(modelB)).isTrue();
}
@Test
public void modelFile_isPreferredTo_version() {
ModelFileManager.ModelFile modelA =
new ModelFileManager.ModelFile(
new File("/path/a"), 2,
Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
ModelFileManager.ModelFile modelB =
new ModelFileManager.ModelFile(
new File("/path/b"), 1,
Collections.emptyList(), "", false);
assertThat(modelA.isPreferredTo(modelB)).isTrue();
}
@Test
public void testFileSupplierImpl_updatedFileOnly() throws IOException {
mUpdatedModelFile.createNewFile();
File model1 = new File(mFactoryModelDir, "test1.model");
model1.createNewFile();
File model2 = new File(mFactoryModelDir, "test2.model");
model2.createNewFile();
new File(mFactoryModelDir, "not_match_regex.model").createNewFile();
List<ModelFileManager.ModelFile> modelFiles = mModelFileSupplierImpl.get();
List<String> modelFilePaths =
modelFiles
.stream()
.map(modelFile -> modelFile.getPath())
.collect(Collectors.toList());
assertThat(modelFiles).hasSize(3);
assertThat(modelFilePaths).containsExactly(
mUpdatedModelFile.getAbsolutePath(),
model1.getAbsolutePath(),
model2.getAbsolutePath());
}
@Test
public void testFileSupplierImpl_empty() {
mFactoryModelDir.delete();
List<ModelFileManager.ModelFile> modelFiles = mModelFileSupplierImpl.get();
assertThat(modelFiles).hasSize(0);
}
private static void recursiveDelete(File f) {
if (f.isDirectory()) {
for (File innerFile : f.listFiles()) {
recursiveDelete(innerFile);
}
}
f.delete();
}
}

View File

@ -16,7 +16,6 @@
package android.view.textclassifier;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.provider.DeviceConfig;
@ -24,8 +23,6 @@ import android.provider.DeviceConfig;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.common.primitives.Floats;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -57,17 +54,17 @@ public class TextClassificationConstantsTest {
public void testLoadFromDeviceConfig_IntValue() throws Exception {
// Saves config original value.
final String originalValue = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TextClassificationConstants.SUGGEST_SELECTION_MAX_RANGE_LENGTH);
TextClassificationConstants.GENERATE_LINKS_MAX_TEXT_LENGTH);
final TextClassificationConstants constants = new TextClassificationConstants();
try {
// Sets and checks different value.
setDeviceConfig(TextClassificationConstants.SUGGEST_SELECTION_MAX_RANGE_LENGTH, "8");
assertWithMessage(TextClassificationConstants.SUGGEST_SELECTION_MAX_RANGE_LENGTH)
.that(constants.getSuggestSelectionMaxRangeLength()).isEqualTo(8);
setDeviceConfig(TextClassificationConstants.GENERATE_LINKS_MAX_TEXT_LENGTH, "8");
assertWithMessage(TextClassificationConstants.GENERATE_LINKS_MAX_TEXT_LENGTH)
.that(constants.getGenerateLinksMaxTextLength()).isEqualTo(8);
} finally {
// Restores config original value.
setDeviceConfig(TextClassificationConstants.SUGGEST_SELECTION_MAX_RANGE_LENGTH,
setDeviceConfig(TextClassificationConstants.GENERATE_LINKS_MAX_TEXT_LENGTH,
originalValue);
}
}
@ -94,61 +91,6 @@ public class TextClassificationConstantsTest {
}
}
@Test
public void testLoadFromDeviceConfig_FloatValue() throws Exception {
// Saves config original value.
final String originalValue = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TextClassificationConstants.LANG_ID_THRESHOLD_OVERRIDE);
final TextClassificationConstants constants = new TextClassificationConstants();
try {
// Sets and checks different value.
setDeviceConfig(TextClassificationConstants.LANG_ID_THRESHOLD_OVERRIDE, "2");
assertWithMessage(TextClassificationConstants.LANG_ID_THRESHOLD_OVERRIDE)
.that(constants.getLangIdThresholdOverride()).isWithin(EPSILON).of(2f);
} finally {
// Restores config original value.
setDeviceConfig(TextClassificationConstants.LANG_ID_THRESHOLD_OVERRIDE, originalValue);
}
}
@Test
public void testLoadFromDeviceConfig_StringList() throws Exception {
// Saves config original value.
final String originalValue = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TextClassificationConstants.ENTITY_LIST_DEFAULT);
final TextClassificationConstants constants = new TextClassificationConstants();
try {
// Sets and checks different value.
setDeviceConfig(TextClassificationConstants.ENTITY_LIST_DEFAULT, "email:url");
assertWithMessage(TextClassificationConstants.ENTITY_LIST_DEFAULT)
.that(constants.getEntityListDefault())
.containsExactly("email", "url");
} finally {
// Restores config original value.
setDeviceConfig(TextClassificationConstants.ENTITY_LIST_DEFAULT, originalValue);
}
}
@Test
public void testLoadFromDeviceConfig_FloatList() throws Exception {
// Saves config original value.
final String originalValue = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
TextClassificationConstants.LANG_ID_CONTEXT_SETTINGS);
final TextClassificationConstants constants = new TextClassificationConstants();
try {
// Sets and checks different value.
setDeviceConfig(TextClassificationConstants.LANG_ID_CONTEXT_SETTINGS, "30:0.5:0.3");
assertThat(Floats.asList(constants.getLangIdContextSettings())).containsExactly(30f,
0.5f, 0.3f).inOrder();
} finally {
// Restores config original value.
setDeviceConfig(TextClassificationConstants.LANG_ID_CONTEXT_SETTINGS, originalValue);
}
}
private void setDeviceConfig(String key, String value) {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_TEXTCLASSIFIER, key,
value, /* makeDefault */ false);

View File

@ -16,19 +16,15 @@
package android.view.textclassifier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import android.content.Context;
import android.content.Intent;
import android.os.LocaleList;
import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
@ -38,14 +34,12 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class TextClassificationManagerTest {
private static final LocaleList LOCALES = LocaleList.forLanguageTags("en-US");
private Context mContext;
private TextClassificationManager mTcm;
@Before
public void setup() {
mContext = InstrumentationRegistry.getTargetContext();
mContext = ApplicationProvider.getApplicationContext();
mTcm = mContext.getSystemService(TextClassificationManager.class);
}
@ -53,45 +47,17 @@ public class TextClassificationManagerTest {
public void testSetTextClassifier() {
TextClassifier classifier = mock(TextClassifier.class);
mTcm.setTextClassifier(classifier);
assertEquals(classifier, mTcm.getTextClassifier());
assertThat(mTcm.getTextClassifier()).isEqualTo(classifier);
}
@Test
public void testGetLocalTextClassifier() {
assertTrue(mTcm.getTextClassifier(TextClassifier.LOCAL) instanceof TextClassifierImpl);
assertThat(mTcm.getTextClassifier(TextClassifier.LOCAL)).isSameAs(TextClassifier.NO_OP);
}
@Test
public void testGetSystemTextClassifier() {
assertTrue(mTcm.getTextClassifier(TextClassifier.SYSTEM) instanceof SystemTextClassifier);
}
@Test
public void testCannotResolveIntent() {
Context fakeContext = new FakeContextBuilder()
.setAllIntentComponent(FakeContextBuilder.DEFAULT_COMPONENT)
.setIntentComponent(Intent.ACTION_INSERT_OR_EDIT, null)
.build();
TextClassifier fallback = TextClassifier.NO_OP;
TextClassifier classifier = new TextClassifierImpl(
fakeContext, new TextClassificationConstants(), fallback);
String text = "Contact me at +12122537077";
String classifiedText = "+12122537077";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification result = classifier.classifyText(request);
TextClassification fallbackResult = fallback.classifyText(request);
// classifier should not totally fail in which case it returns a fallback result.
// It should skip the failing intent and return a result for non-failing intents.
assertFalse(result.getActions().isEmpty());
assertNotSame(result, fallbackResult);
assertThat(mTcm.getTextClassifier(TextClassifier.SYSTEM))
.isInstanceOf(SystemTextClassifier.class);
}
}

View File

@ -1,719 +0,0 @@
/*
* 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 org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import android.app.RemoteAction;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
import android.service.textclassifier.TextClassifierService;
import android.text.Spannable;
import android.text.SpannableString;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.google.common.truth.Truth;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Testing {@link TextClassifierTest} APIs on local and system textclassifier.
* <p>
* Tests are skipped if such a textclassifier does not exist.
*/
@SmallTest
@RunWith(Parameterized.class)
public class TextClassifierTest {
private static final String LOCAL = "local";
private static final String SESSION = "session";
private static final String DEFAULT = "default";
// TODO: Add SYSTEM, which tests TextClassifier.SYSTEM.
@Parameterized.Parameters(name = "{0}")
public static Iterable<Object> textClassifierTypes() {
return Arrays.asList(LOCAL, SESSION, DEFAULT);
}
@Parameterized.Parameter
public String mTextClassifierType;
private static final TextClassificationConstants TC_CONSTANTS =
new TextClassificationConstants();
private static final LocaleList LOCALES = LocaleList.forLanguageTags("en-US");
private static final String NO_TYPE = null;
private Context mContext;
private TextClassificationManager mTcm;
private TextClassifier mClassifier;
@Before
public void setup() {
mContext = InstrumentationRegistry.getTargetContext();
mTcm = mContext.getSystemService(TextClassificationManager.class);
if (mTextClassifierType.equals(LOCAL)) {
mClassifier = mTcm.getTextClassifier(TextClassifier.LOCAL);
} else if (mTextClassifierType.equals(SESSION)) {
mClassifier = mTcm.createTextClassificationSession(
new TextClassificationContext.Builder(
"android",
TextClassifier.WIDGET_TYPE_NOTIFICATION)
.build(),
mTcm.getTextClassifier(TextClassifier.LOCAL));
} else {
mClassifier = TextClassifierService.getDefaultTextClassifierImplementation(mContext);
}
}
@Test
public void testSuggestSelection() {
if (isTextClassifierDisabled()) return;
String text = "Contact me at droid@android.com";
String selected = "droid";
String suggested = "droid@android.com";
int startIndex = text.indexOf(selected);
int endIndex = startIndex + selected.length();
int smartStartIndex = text.indexOf(suggested);
int smartEndIndex = smartStartIndex + suggested.length();
TextSelection.Request request = new TextSelection.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextSelection selection = mClassifier.suggestSelection(request);
assertThat(selection,
isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_EMAIL));
}
@Test
public void testSuggestSelection_url() {
if (isTextClassifierDisabled()) return;
String text = "Visit http://www.android.com for more information";
String selected = "http";
String suggested = "http://www.android.com";
int startIndex = text.indexOf(selected);
int endIndex = startIndex + selected.length();
int smartStartIndex = text.indexOf(suggested);
int smartEndIndex = smartStartIndex + suggested.length();
TextSelection.Request request = new TextSelection.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextSelection selection = mClassifier.suggestSelection(request);
assertThat(selection,
isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_URL));
}
@Test
public void testSmartSelection_withEmoji() {
if (isTextClassifierDisabled()) return;
String text = "\uD83D\uDE02 Hello.";
String selected = "Hello";
int startIndex = text.indexOf(selected);
int endIndex = startIndex + selected.length();
TextSelection.Request request = new TextSelection.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextSelection selection = mClassifier.suggestSelection(request);
assertThat(selection,
isTextSelection(startIndex, endIndex, NO_TYPE));
}
@Test
public void testClassifyText() {
if (isTextClassifierDisabled()) return;
String text = "Contact me at droid@android.com";
String classifiedText = "droid@android.com";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_EMAIL));
}
@Test
public void testClassifyText_url() {
if (isTextClassifierDisabled()) return;
String text = "Visit www.android.com for more information";
String classifiedText = "www.android.com";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL));
assertThat(classification, containsIntentWithAction(Intent.ACTION_VIEW));
}
@Test
public void testClassifyText_address() {
if (isTextClassifierDisabled()) return;
String text = "Brandschenkestrasse 110, Zürich, Switzerland";
TextClassification.Request request = new TextClassification.Request.Builder(
text, 0, text.length())
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification, isTextClassification(text, TextClassifier.TYPE_ADDRESS));
}
@Test
public void testClassifyText_url_inCaps() {
if (isTextClassifierDisabled()) return;
String text = "Visit HTTP://ANDROID.COM for more information";
String classifiedText = "HTTP://ANDROID.COM";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL));
assertThat(classification, containsIntentWithAction(Intent.ACTION_VIEW));
}
@Test
public void testClassifyText_date() {
if (isTextClassifierDisabled()) return;
String text = "Let's meet on January 9, 2018.";
String classifiedText = "January 9, 2018";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_DATE));
Bundle extras = classification.getExtras();
List<Bundle> entities = ExtrasUtils.getEntities(extras);
Truth.assertThat(entities).hasSize(1);
Bundle entity = entities.get(0);
Truth.assertThat(ExtrasUtils.getEntityType(entity)).isEqualTo(TextClassifier.TYPE_DATE);
}
@Test
public void testClassifyText_datetime() {
if (isTextClassifierDisabled()) return;
String text = "Let's meet 2018/01/01 10:30:20.";
String classifiedText = "2018/01/01 10:30:20";
int startIndex = text.indexOf(classifiedText);
int endIndex = startIndex + classifiedText.length();
TextClassification.Request request = new TextClassification.Request.Builder(
text, startIndex, endIndex)
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = mClassifier.classifyText(request);
assertThat(classification,
isTextClassification(classifiedText, TextClassifier.TYPE_DATE_TIME));
}
@Test
public void testClassifyText_foreignText() {
LocaleList originalLocales = LocaleList.getDefault();
LocaleList.setDefault(LocaleList.forLanguageTags("en"));
String japaneseText = "これは日本語のテキストです";
Context context = new FakeContextBuilder()
.setIntentComponent(Intent.ACTION_TRANSLATE, FakeContextBuilder.DEFAULT_COMPONENT)
.build();
TextClassifier classifier = new TextClassifierImpl(context, TC_CONSTANTS);
TextClassification.Request request = new TextClassification.Request.Builder(
japaneseText, 0, japaneseText.length())
.setDefaultLocales(LOCALES)
.build();
TextClassification classification = classifier.classifyText(request);
RemoteAction translateAction = classification.getActions().get(0);
assertEquals(1, classification.getActions().size());
assertEquals(
context.getString(com.android.internal.R.string.translate),
translateAction.getTitle());
assertEquals(translateAction, ExtrasUtils.findTranslateAction(classification));
Intent intent = ExtrasUtils.getActionsIntents(classification).get(0);
assertEquals(Intent.ACTION_TRANSLATE, intent.getAction());
Bundle foreignLanguageInfo = ExtrasUtils.getForeignLanguageExtra(classification);
assertEquals("ja", ExtrasUtils.getEntityType(foreignLanguageInfo));
assertTrue(ExtrasUtils.getScore(foreignLanguageInfo) >= 0);
assertTrue(ExtrasUtils.getScore(foreignLanguageInfo) <= 1);
assertTrue(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER));
assertEquals("ja", ExtrasUtils.getTopLanguage(intent).getLanguage());
LocaleList.setDefault(originalLocales);
}
@Test
public void testGenerateLinks_phone() {
if (isTextClassifierDisabled()) return;
String text = "The number is +12122537077. See you tonight!";
TextLinks.Request request = new TextLinks.Request.Builder(text).build();
assertThat(mClassifier.generateLinks(request),
isTextLinksContaining(text, "+12122537077", TextClassifier.TYPE_PHONE));
}
@Test
public void testGenerateLinks_exclude() {
if (isTextClassifierDisabled()) return;
String text = "You want apple@banana.com. See you tonight!";
List<String> hints = Collections.EMPTY_LIST;
List<String> included = Collections.EMPTY_LIST;
List<String> excluded = Arrays.asList(TextClassifier.TYPE_EMAIL);
TextLinks.Request request = new TextLinks.Request.Builder(text)
.setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded))
.setDefaultLocales(LOCALES)
.build();
assertThat(mClassifier.generateLinks(request),
not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL)));
}
@Test
public void testGenerateLinks_explicit_address() {
if (isTextClassifierDisabled()) return;
String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!";
List<String> explicit = Arrays.asList(TextClassifier.TYPE_ADDRESS);
TextLinks.Request request = new TextLinks.Request.Builder(text)
.setEntityConfig(TextClassifier.EntityConfig.createWithExplicitEntityList(explicit))
.setDefaultLocales(LOCALES)
.build();
assertThat(mClassifier.generateLinks(request),
isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA",
TextClassifier.TYPE_ADDRESS));
}
@Test
public void testGenerateLinks_exclude_override() {
if (isTextClassifierDisabled()) return;
String text = "You want apple@banana.com. See you tonight!";
List<String> hints = Collections.EMPTY_LIST;
List<String> included = Arrays.asList(TextClassifier.TYPE_EMAIL);
List<String> excluded = Arrays.asList(TextClassifier.TYPE_EMAIL);
TextLinks.Request request = new TextLinks.Request.Builder(text)
.setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded))
.setDefaultLocales(LOCALES)
.build();
assertThat(mClassifier.generateLinks(request),
not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL)));
}
@Test
public void testGenerateLinks_maxLength() {
if (isTextClassifierDisabled()) return;
char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength()];
Arrays.fill(manySpaces, ' ');
TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build();
TextLinks links = mClassifier.generateLinks(request);
assertTrue(links.getLinks().isEmpty());
}
@Test
public void testApplyLinks_unsupportedCharacter() {
if (isTextClassifierDisabled()) return;
Spannable url = new SpannableString("\u202Emoc.diordna.com");
TextLinks.Request request = new TextLinks.Request.Builder(url).build();
assertEquals(
TextLinks.STATUS_UNSUPPORTED_CHARACTER,
mClassifier.generateLinks(request).apply(url, 0, null));
}
@Test
public void testGenerateLinks_tooLong() {
if (isTextClassifierDisabled()) return;
char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength() + 1];
Arrays.fill(manySpaces, ' ');
TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build();
TextLinks links = mClassifier.generateLinks(request);
assertTrue(links.getLinks().isEmpty());
}
@Test
public void testGenerateLinks_entityData() {
if (isTextClassifierDisabled()) return;
String text = "The number is +12122537077.";
Bundle extras = new Bundle();
ExtrasUtils.putIsSerializedEntityDataEnabled(extras, true);
TextLinks.Request request = new TextLinks.Request.Builder(text).setExtras(extras).build();
TextLinks textLinks = mClassifier.generateLinks(request);
Truth.assertThat(textLinks.getLinks()).hasSize(1);
TextLinks.TextLink textLink = textLinks.getLinks().iterator().next();
List<Bundle> entities = ExtrasUtils.getEntities(textLink.getExtras());
Truth.assertThat(entities).hasSize(1);
Bundle entity = entities.get(0);
Truth.assertThat(ExtrasUtils.getEntityType(entity)).isEqualTo(TextClassifier.TYPE_PHONE);
}
@Test
public void testGenerateLinks_entityData_disabled() {
if (isTextClassifierDisabled()) return;
String text = "The number is +12122537077.";
TextLinks.Request request = new TextLinks.Request.Builder(text).build();
TextLinks textLinks = mClassifier.generateLinks(request);
Truth.assertThat(textLinks.getLinks()).hasSize(1);
TextLinks.TextLink textLink = textLinks.getLinks().iterator().next();
List<Bundle> entities = ExtrasUtils.getEntities(textLink.getExtras());
Truth.assertThat(entities).isNull();
}
@Test
public void testDetectLanguage() {
if (isTextClassifierDisabled()) return;
String text = "This is English text";
TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
TextLanguage textLanguage = mClassifier.detectLanguage(request);
assertThat(textLanguage, isTextLanguage("en"));
}
@Test
public void testDetectLanguage_japanese() {
if (isTextClassifierDisabled()) return;
String text = "これは日本語のテキストです";
TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
TextLanguage textLanguage = mClassifier.detectLanguage(request);
assertThat(textLanguage, isTextLanguage("ja"));
}
@Ignore // Doesn't work without a language-based model.
@Test
public void testSuggestConversationActions_textReplyOnly_maxOne() {
if (isTextClassifierDisabled()) return;
ConversationActions.Message message =
new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText("Where are you?")
.build();
TextClassifier.EntityConfig typeConfig =
new TextClassifier.EntityConfig.Builder().includeTypesFromTextClassifier(false)
.setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_TEXT_REPLY))
.build();
ConversationActions.Request request =
new ConversationActions.Request.Builder(Collections.singletonList(message))
.setMaxSuggestions(1)
.setTypeConfig(typeConfig)
.build();
ConversationActions conversationActions = mClassifier.suggestConversationActions(request);
Truth.assertThat(conversationActions.getConversationActions()).hasSize(1);
ConversationAction conversationAction = conversationActions.getConversationActions().get(0);
Truth.assertThat(conversationAction.getType()).isEqualTo(
ConversationAction.TYPE_TEXT_REPLY);
Truth.assertThat(conversationAction.getTextReply()).isNotNull();
}
@Ignore // Doesn't work without a language-based model.
@Test
public void testSuggestConversationActions_textReplyOnly_noMax() {
if (isTextClassifierDisabled()) return;
ConversationActions.Message message =
new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText("Where are you?")
.build();
TextClassifier.EntityConfig typeConfig =
new TextClassifier.EntityConfig.Builder().includeTypesFromTextClassifier(false)
.setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_TEXT_REPLY))
.build();
ConversationActions.Request request =
new ConversationActions.Request.Builder(Collections.singletonList(message))
.setTypeConfig(typeConfig)
.build();
ConversationActions conversationActions = mClassifier.suggestConversationActions(request);
assertTrue(conversationActions.getConversationActions().size() > 1);
for (ConversationAction conversationAction :
conversationActions.getConversationActions()) {
assertThat(conversationAction,
isConversationAction(ConversationAction.TYPE_TEXT_REPLY));
}
}
@Test
public void testSuggestConversationActions_openUrl() {
if (isTextClassifierDisabled()) return;
ConversationActions.Message message =
new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText("Check this out: https://www.android.com")
.build();
TextClassifier.EntityConfig typeConfig =
new TextClassifier.EntityConfig.Builder().includeTypesFromTextClassifier(false)
.setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_OPEN_URL))
.build();
ConversationActions.Request request =
new ConversationActions.Request.Builder(Collections.singletonList(message))
.setMaxSuggestions(1)
.setTypeConfig(typeConfig)
.build();
ConversationActions conversationActions = mClassifier.suggestConversationActions(request);
Truth.assertThat(conversationActions.getConversationActions()).hasSize(1);
ConversationAction conversationAction = conversationActions.getConversationActions().get(0);
Truth.assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_OPEN_URL);
Intent actionIntent = ExtrasUtils.getActionIntent(conversationAction.getExtras());
Truth.assertThat(actionIntent.getAction()).isEqualTo(Intent.ACTION_VIEW);
Truth.assertThat(actionIntent.getData()).isEqualTo(Uri.parse("https://www.android.com"));
}
@Ignore // Doesn't work without a language-based model.
@Test
public void testSuggestConversationActions_copy() {
if (isTextClassifierDisabled()) return;
ConversationActions.Message message =
new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText("Authentication code: 12345")
.build();
TextClassifier.EntityConfig typeConfig =
new TextClassifier.EntityConfig.Builder().includeTypesFromTextClassifier(false)
.setIncludedTypes(
Collections.singletonList(ConversationAction.TYPE_COPY))
.build();
ConversationActions.Request request =
new ConversationActions.Request.Builder(Collections.singletonList(message))
.setMaxSuggestions(1)
.setTypeConfig(typeConfig)
.build();
ConversationActions conversationActions = mClassifier.suggestConversationActions(request);
Truth.assertThat(conversationActions.getConversationActions()).hasSize(1);
ConversationAction conversationAction = conversationActions.getConversationActions().get(0);
Truth.assertThat(conversationAction.getType()).isEqualTo(ConversationAction.TYPE_COPY);
Truth.assertThat(conversationAction.getTextReply()).isAnyOf(null, "");
Truth.assertThat(conversationAction.getAction()).isNull();
String code = ExtrasUtils.getCopyText(conversationAction.getExtras());
Truth.assertThat(code).isEqualTo("12345");
Truth.assertThat(
ExtrasUtils.getSerializedEntityData(conversationAction.getExtras())).isNotEmpty();
}
@Test
public void testSuggestConversationActions_deduplicate() {
Context context = new FakeContextBuilder()
.setIntentComponent(Intent.ACTION_SENDTO, FakeContextBuilder.DEFAULT_COMPONENT)
.build();
ConversationActions.Message message =
new ConversationActions.Message.Builder(
ConversationActions.Message.PERSON_USER_OTHERS)
.setText("a@android.com b@android.com")
.build();
ConversationActions.Request request =
new ConversationActions.Request.Builder(Collections.singletonList(message))
.setMaxSuggestions(3)
.build();
TextClassifier classifier = new TextClassifierImpl(context, TC_CONSTANTS);
ConversationActions conversationActions = classifier.suggestConversationActions(request);
Truth.assertThat(conversationActions.getConversationActions()).isEmpty();
}
private boolean isTextClassifierDisabled() {
return mClassifier == null || mClassifier == TextClassifier.NO_OP;
}
private static Matcher<TextSelection> isTextSelection(
final int startIndex, final int endIndex, final String type) {
return new BaseMatcher<TextSelection>() {
@Override
public boolean matches(Object o) {
if (o instanceof TextSelection) {
TextSelection selection = (TextSelection) o;
return startIndex == selection.getSelectionStartIndex()
&& endIndex == selection.getSelectionEndIndex()
&& typeMatches(selection, type);
}
return false;
}
private boolean typeMatches(TextSelection selection, String type) {
return type == null
|| (selection.getEntityCount() > 0
&& type.trim().equalsIgnoreCase(selection.getEntity(0)));
}
@Override
public void describeTo(Description description) {
description.appendValue(
String.format("%d, %d, %s", startIndex, endIndex, type));
}
};
}
private static Matcher<TextLinks> isTextLinksContaining(
final String text, final String substring, final String type) {
return new BaseMatcher<TextLinks>() {
@Override
public void describeTo(Description description) {
description.appendText("text=").appendValue(text)
.appendText(", substring=").appendValue(substring)
.appendText(", type=").appendValue(type);
}
@Override
public boolean matches(Object o) {
if (o instanceof TextLinks) {
for (TextLinks.TextLink link : ((TextLinks) o).getLinks()) {
if (text.subSequence(link.getStart(), link.getEnd()).equals(substring)) {
return type.equals(link.getEntity(0));
}
}
}
return false;
}
};
}
private static Matcher<TextClassification> isTextClassification(
final String text, final String type) {
return new BaseMatcher<TextClassification>() {
@Override
public boolean matches(Object o) {
if (o instanceof TextClassification) {
TextClassification result = (TextClassification) o;
return text.equals(result.getText())
&& result.getEntityCount() > 0
&& type.equals(result.getEntity(0));
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("text=").appendValue(text)
.appendText(", type=").appendValue(type);
}
};
}
private static Matcher<TextClassification> containsIntentWithAction(final String action) {
return new BaseMatcher<TextClassification>() {
@Override
public boolean matches(Object o) {
if (o instanceof TextClassification) {
TextClassification result = (TextClassification) o;
return ExtrasUtils.findAction(result, action) != null;
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("intent action=").appendValue(action);
}
};
}
private static Matcher<TextLanguage> isTextLanguage(final String languageTag) {
return new BaseMatcher<TextLanguage>() {
@Override
public boolean matches(Object o) {
if (o instanceof TextLanguage) {
TextLanguage result = (TextLanguage) o;
return result.getLocaleHypothesisCount() > 0
&& languageTag.equals(result.getLocale(0).toLanguageTag());
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("locale=").appendValue(languageTag);
}
};
}
private static Matcher<ConversationAction> isConversationAction(String actionType) {
return new BaseMatcher<ConversationAction>() {
@Override
public boolean matches(Object o) {
if (!(o instanceof ConversationAction)) {
return false;
}
ConversationAction conversationAction =
(ConversationAction) o;
if (!actionType.equals(conversationAction.getType())) {
return false;
}
if (ConversationAction.TYPE_TEXT_REPLY.equals(actionType)) {
if (conversationAction.getTextReply() == null) {
return false;
}
}
if (conversationAction.getConfidenceScore() < 0
|| conversationAction.getConfidenceScore() > 1) {
return false;
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("actionType=").appendValue(actionType);
}
};
}
}

View File

@ -1,200 +0,0 @@
/*
* 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.intent;
import static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.assertThrows;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.textclassifier.FakeContextBuilder;
import android.view.textclassifier.TextClassifier;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class LabeledIntentTest {
private static final String TITLE_WITHOUT_ENTITY = "Map";
private static final String TITLE_WITH_ENTITY = "Map NW14D1";
private static final String DESCRIPTION = "Check the map";
private static final String DESCRIPTION_WITH_APP_NAME = "Use %1$s to open map";
private static final Intent INTENT =
new Intent(Intent.ACTION_VIEW).setDataAndNormalize(Uri.parse("http://www.android.com"));
private static final int REQUEST_CODE = 42;
private static final Bundle TEXT_LANGUAGES_BUNDLE = Bundle.EMPTY;
private static final String APP_LABEL = "fake";
private Context mContext;
@Before
public void setup() {
final ComponentName component = FakeContextBuilder.DEFAULT_COMPONENT;
mContext = new FakeContextBuilder()
.setIntentComponent(Intent.ACTION_VIEW, component)
.setAppLabel(component.getPackageName(), APP_LABEL)
.build();
}
@Test
public void resolve_preferTitleWithEntity() {
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
null,
INTENT,
REQUEST_CODE
);
LabeledIntent.Result result = labeledIntent.resolve(
mContext, /*titleChooser*/ null, TEXT_LANGUAGES_BUNDLE);
assertThat(result).isNotNull();
assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITH_ENTITY);
assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
Intent intent = result.resolvedIntent;
assertThat(intent.getAction()).isEqualTo(intent.getAction());
assertThat(intent.getComponent()).isNotNull();
assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
}
@Test
public void resolve_useAvailableTitle() {
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
null,
INTENT,
REQUEST_CODE
);
LabeledIntent.Result result = labeledIntent.resolve(
mContext, /*titleChooser*/ null, TEXT_LANGUAGES_BUNDLE);
assertThat(result).isNotNull();
assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
Intent intent = result.resolvedIntent;
assertThat(intent.getAction()).isEqualTo(intent.getAction());
assertThat(intent.getComponent()).isNotNull();
}
@Test
public void resolve_titleChooser() {
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
null,
INTENT,
REQUEST_CODE
);
LabeledIntent.Result result = labeledIntent.resolve(
mContext, (labeledIntent1, resolveInfo) -> "chooser", TEXT_LANGUAGES_BUNDLE);
assertThat(result).isNotNull();
assertThat(result.remoteAction.getTitle()).isEqualTo("chooser");
assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
Intent intent = result.resolvedIntent;
assertThat(intent.getAction()).isEqualTo(intent.getAction());
assertThat(intent.getComponent()).isNotNull();
}
@Test
public void resolve_titleChooserReturnsNull() {
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
null,
INTENT,
REQUEST_CODE
);
LabeledIntent.Result result = labeledIntent.resolve(
mContext, (labeledIntent1, resolveInfo) -> null, TEXT_LANGUAGES_BUNDLE);
assertThat(result).isNotNull();
assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
assertThat(result.remoteAction.getContentDescription()).isEqualTo(DESCRIPTION);
Intent intent = result.resolvedIntent;
assertThat(intent.getAction()).isEqualTo(intent.getAction());
assertThat(intent.getComponent()).isNotNull();
}
@Test
public void resolve_missingTitle() {
assertThrows(
IllegalArgumentException.class,
() ->
new LabeledIntent(
null,
null,
DESCRIPTION,
null,
INTENT,
REQUEST_CODE
));
}
@Test
public void resolve_noIntentHandler() {
// See setup(). mContext can only resolve Intent.ACTION_VIEW.
Intent unresolvableIntent = new Intent(Intent.ACTION_TRANSLATE);
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
null,
unresolvableIntent,
REQUEST_CODE);
LabeledIntent.Result result = labeledIntent.resolve(mContext, null, null);
assertThat(result).isNull();
}
@Test
public void resolve_descriptionWithAppName() {
LabeledIntent labeledIntent = new LabeledIntent(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
INTENT,
REQUEST_CODE
);
LabeledIntent.Result result = labeledIntent.resolve(
mContext, /*titleChooser*/ null, TEXT_LANGUAGES_BUNDLE);
assertThat(result).isNotNull();
assertThat(result.remoteAction.getContentDescription()).isEqualTo("Use fake to open map");
}
}

View File

@ -1,122 +0,0 @@
/*
* 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.intent;
import static com.google.common.truth.Truth.assertThat;
import android.content.Intent;
import android.view.textclassifier.TextClassifier;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.textclassifier.AnnotatorModel;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LegacyIntentClassificationFactoryTest {
private static final String TEXT = "text";
private LegacyClassificationIntentFactory mLegacyIntentClassificationFactory;
@Before
public void setup() {
mLegacyIntentClassificationFactory = new LegacyClassificationIntentFactory();
}
@Test
public void create_typeDictionary() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_DICTIONARY,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
0L,
0L,
0d);
List<LabeledIntent> intents = mLegacyIntentClassificationFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ false,
null,
classificationResult);
assertThat(intents).hasSize(1);
LabeledIntent labeledIntent = intents.get(0);
Intent intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(Intent.ACTION_DEFINE);
assertThat(intent.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(TEXT);
}
@Test
public void create_translateAndDictionary() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_DICTIONARY,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
0L,
0L,
0d);
List<LabeledIntent> intents = mLegacyIntentClassificationFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ true,
null,
classificationResult);
assertThat(intents).hasSize(2);
assertThat(intents.get(0).intent.getAction()).isEqualTo(Intent.ACTION_DEFINE);
assertThat(intents.get(1).intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
}
}

View File

@ -1,243 +0,0 @@
/*
* 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.intent;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.Intent;
import android.view.textclassifier.TextClassifier;
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_WITHOUT_ENTITY = "Map";
private static final String DESCRIPTION = "Opens in Maps";
private static final String DESCRIPTION_WITH_APP_NAME = "Use %1$s to open Map";
private static final String ACTION = Intent.ACTION_VIEW;
@Mock
private ClassificationIntentFactory mFallback;
private TemplateClassificationIntentFactory mTemplateClassificationIntentFactory;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mTemplateClassificationIntentFactory = new TemplateClassificationIntentFactory(
new TemplateIntentFactory(),
mFallback);
}
@Test
public void create_foreignText() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
createRemoteActionTemplates(),
0L,
0L,
0d);
List<LabeledIntent> intents =
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ true,
null,
classificationResult);
assertThat(intents).hasSize(2);
LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
Intent intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(ACTION);
labeledIntent = intents.get(1);
intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
}
@Test
public void create_notForeignText() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
createRemoteActionTemplates(),
0L,
0L,
0d);
List<LabeledIntent> intents =
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ false,
null,
classificationResult);
assertThat(intents).hasSize(1);
LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
Intent intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(ACTION);
}
@Test
public void create_nullTemplate() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
0L,
0L,
0d);
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ false,
null,
classificationResult);
verify(mFallback).create(
same(InstrumentationRegistry.getContext()), eq(TEXT), eq(false), eq(null),
same(classificationResult));
}
@Test
public void create_emptyResult() {
AnnotatorModel.ClassificationResult classificationResult =
new AnnotatorModel.ClassificationResult(
TextClassifier.TYPE_ADDRESS,
1.0f,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
new RemoteActionTemplate[0],
0L,
0L,
0d);
mTemplateClassificationIntentFactory.create(
InstrumentationRegistry.getContext(),
TEXT,
/* foreignText */ false,
null,
classificationResult);
verify(mFallback, never()).create(
any(Context.class), eq(TEXT), eq(false), eq(null),
any(AnnotatorModel.ClassificationResult.class));
}
private static RemoteActionTemplate[] createRemoteActionTemplates() {
return new RemoteActionTemplate[]{
new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
ACTION,
null,
null,
null,
null,
null,
null,
null
)
};
}
}

View File

@ -1,270 +0,0 @@
/*
* 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.intent;
import static com.google.common.truth.Truth.assertThat;
import android.content.Intent;
import android.net.Uri;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
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.MockitoAnnotations;
import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TemplateIntentFactoryTest {
private static final String TEXT = "text";
private static final String TITLE_WITHOUT_ENTITY = "Map";
private static final String TITLE_WITH_ENTITY = "Map NW14D1";
private static final String DESCRIPTION = "Check the map";
private static final String DESCRIPTION_WITH_APP_NAME = "Use %1$s to open map";
private static final String ACTION = Intent.ACTION_VIEW;
private static final String DATA = Uri.parse("http://www.android.com").toString();
private static final String TYPE = "text/html";
private static final Integer FLAG = Intent.FLAG_ACTIVITY_NEW_TASK;
private static final String[] CATEGORY =
new String[]{Intent.CATEGORY_DEFAULT, Intent.CATEGORY_APP_BROWSER};
private static final String PACKAGE_NAME = "pkg.name";
private static final String KEY_ONE = "key1";
private static final String VALUE_ONE = "value1";
private static final String KEY_TWO = "key2";
private static final int VALUE_TWO = 42;
private static final NamedVariant[] NAMED_VARIANTS = new NamedVariant[]{
new NamedVariant(KEY_ONE, VALUE_ONE),
new NamedVariant(KEY_TWO, VALUE_TWO)
};
private static final Integer REQUEST_CODE = 10;
private TemplateIntentFactory mTemplateIntentFactory;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mTemplateIntentFactory = new TemplateIntentFactory();
}
@Test
public void create_full() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
ACTION,
DATA,
TYPE,
FLAG,
CATEGORY,
/* packageName */ null,
NAMED_VARIANTS,
REQUEST_CODE
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
assertThat(intents).hasSize(1);
LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
assertThat(labeledIntent.titleWithEntity).isEqualTo(TITLE_WITH_ENTITY);
assertThat(labeledIntent.description).isEqualTo(DESCRIPTION);
assertThat(labeledIntent.descriptionWithAppName).isEqualTo(DESCRIPTION_WITH_APP_NAME);
assertThat(labeledIntent.requestCode).isEqualTo(REQUEST_CODE);
Intent intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(ACTION);
assertThat(intent.getData().toString()).isEqualTo(DATA);
assertThat(intent.getType()).isEqualTo(TYPE);
assertThat(intent.getFlags()).isEqualTo(FLAG);
assertThat(intent.getCategories()).containsExactly((Object[]) CATEGORY);
assertThat(intent.getPackage()).isNull();
assertThat(intent.getStringExtra(KEY_ONE)).isEqualTo(VALUE_ONE);
assertThat(intent.getIntExtra(KEY_TWO, 0)).isEqualTo(VALUE_TWO);
}
@Test
public void normalizesScheme() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
ACTION,
"HTTp://www.android.com",
TYPE,
FLAG,
CATEGORY,
/* packageName */ null,
NAMED_VARIANTS,
REQUEST_CODE
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
String data = intents.get(0).intent.getData().toString();
assertThat(data).isEqualTo("http://www.android.com");
}
@Test
public void create_minimal() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
null,
DESCRIPTION,
null,
ACTION,
null,
null,
null,
null,
null,
null,
null
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[]{remoteActionTemplate});
assertThat(intents).hasSize(1);
LabeledIntent labeledIntent = intents.get(0);
assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
assertThat(labeledIntent.titleWithEntity).isNull();
assertThat(labeledIntent.description).isEqualTo(DESCRIPTION);
assertThat(labeledIntent.requestCode).isEqualTo(
LabeledIntent.DEFAULT_REQUEST_CODE);
Intent intent = labeledIntent.intent;
assertThat(intent.getAction()).isEqualTo(ACTION);
assertThat(intent.getData()).isNull();
assertThat(intent.getType()).isNull();
assertThat(intent.getFlags()).isEqualTo(0);
assertThat(intent.getCategories()).isNull();
assertThat(intent.getPackage()).isNull();
}
@Test
public void invalidTemplate_nullTemplate() {
RemoteActionTemplate remoteActionTemplate = null;
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).isEmpty();
}
@Test
public void invalidTemplate_nonEmptyPackageName() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
ACTION,
DATA,
TYPE,
FLAG,
CATEGORY,
PACKAGE_NAME,
NAMED_VARIANTS,
REQUEST_CODE
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).isEmpty();
}
@Test
public void invalidTemplate_emptyTitle() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
null,
null,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
ACTION,
null,
null,
null,
null,
null,
null,
null
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).isEmpty();
}
@Test
public void invalidTemplate_emptyDescription() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
null,
null,
ACTION,
null,
null,
null,
null,
null,
null,
null
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).isEmpty();
}
@Test
public void invalidTemplate_emptyIntentAction() {
RemoteActionTemplate remoteActionTemplate = new RemoteActionTemplate(
TITLE_WITHOUT_ENTITY,
TITLE_WITH_ENTITY,
DESCRIPTION,
DESCRIPTION_WITH_APP_NAME,
null,
null,
null,
null,
null,
null,
null,
null
);
List<LabeledIntent> intents =
mTemplateIntentFactory.create(new RemoteActionTemplate[] {remoteActionTemplate});
assertThat(intents).isEmpty();
}
}

View File

@ -1,116 +0,0 @@
/*
* Copyright (C) 2017 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.logging;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import android.metrics.LogMaker;
import android.util.ArrayMap;
import android.view.textclassifier.GenerateLinksLogger;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class GenerateLinksLoggerTest {
private static final String PACKAGE_NAME = "packageName";
private static final String ZERO = "0";
private static final int LATENCY_MS = 123;
@Test
public void testLogGenerateLinks() {
final String phoneText = "+12122537077";
final String addressText = "1600 Amphitheater Parkway, Mountain View, CA";
final String testText = "The number is " + phoneText + ", the address is " + addressText;
final int phoneOffset = testText.indexOf(phoneText);
final int addressOffset = testText.indexOf(addressText);
final Map<String, Float> phoneEntityScores = new ArrayMap<>();
phoneEntityScores.put(TextClassifier.TYPE_PHONE, 0.9f);
phoneEntityScores.put(TextClassifier.TYPE_OTHER, 0.1f);
final Map<String, Float> addressEntityScores = new ArrayMap<>();
addressEntityScores.put(TextClassifier.TYPE_ADDRESS, 1f);
TextLinks links = new TextLinks.Builder(testText)
.addLink(phoneOffset, phoneOffset + phoneText.length(), phoneEntityScores)
.addLink(addressOffset, addressOffset + addressText.length(), addressEntityScores)
.build();
// Set up mock.
MetricsLogger metricsLogger = mock(MetricsLogger.class);
ArgumentCaptor<LogMaker> logMakerCapture = ArgumentCaptor.forClass(LogMaker.class);
doNothing().when(metricsLogger).write(logMakerCapture.capture());
// Generate the log.
GenerateLinksLogger logger = new GenerateLinksLogger(1 /* sampleRate */, metricsLogger);
logger.logGenerateLinks(testText, links, PACKAGE_NAME, LATENCY_MS);
// Validate.
List<LogMaker> logs = logMakerCapture.getAllValues();
assertEquals(3, logs.size());
assertHasLog(logs, "" /* entityType */, 2, phoneText.length() + addressText.length(),
testText.length());
assertHasLog(logs, TextClassifier.TYPE_ADDRESS, 1, addressText.length(),
testText.length());
assertHasLog(logs, TextClassifier.TYPE_PHONE, 1, phoneText.length(),
testText.length());
}
private void assertHasLog(List<LogMaker> logs, String entityType, int numLinks,
int linkTextLength, int textLength) {
for (LogMaker log : logs) {
if (!entityType.equals(getEntityType(log))) {
continue;
}
assertEquals(PACKAGE_NAME, log.getPackageName());
assertNotNull(Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID)));
assertEquals(numLinks, getIntValue(log, MetricsEvent.FIELD_LINKIFY_NUM_LINKS));
assertEquals(linkTextLength, getIntValue(log, MetricsEvent.FIELD_LINKIFY_LINK_LENGTH));
assertEquals(textLength, getIntValue(log, MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH));
assertEquals(LATENCY_MS, getIntValue(log, MetricsEvent.FIELD_LINKIFY_LATENCY));
return;
}
fail("No log for entity type \"" + entityType + "\"");
}
private static String getEntityType(LogMaker log) {
return Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "");
}
private static int getIntValue(LogMaker log, int eventField) {
return Integer.parseInt(Objects.toString(log.getTaggedData(eventField), ZERO));
}
}

View File

@ -1,43 +0,0 @@
/*
* 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.logging;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.google.common.truth.Truth;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class SmartSelectionEventTrackerTest {
@Test
public void getVersionInfo_valid() {
String signature = "a|702|b";
String versionInfo = SmartSelectionEventTracker.SelectionEvent.getVersionInfo(signature);
Truth.assertThat(versionInfo).isEqualTo("702");
}
@Test
public void getVersionInfo_invalid() {
String signature = "|702";
String versionInfo = SmartSelectionEventTracker.SelectionEvent.getVersionInfo(signature);
Truth.assertThat(versionInfo).isEmpty();
}
}

View File

@ -1,107 +0,0 @@
/*
* 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.logging;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.CONVERSATION_ACTIONS;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_EVENT_TIME;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_FIRST_ENTITY_TYPE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_SCORE;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_TEXT_CLASSIFIER_WIDGET_TYPE;
import static com.google.common.truth.Truth.assertThat;
import android.metrics.LogMaker;
import android.view.textclassifier.ConversationAction;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassifierEvent;
import android.view.textclassifier.TextClassifierEventTronLogger;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.MetricsLogger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TextClassifierEventTronLoggerTest {
private static final String WIDGET_TYPE = "notification";
private static final String PACKAGE_NAME = "pkg";
@Mock
private MetricsLogger mMetricsLogger;
private TextClassifierEventTronLogger mTextClassifierEventTronLogger;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mTextClassifierEventTronLogger = new TextClassifierEventTronLogger(mMetricsLogger);
}
@Test
public void testWriteEvent() {
TextClassificationContext textClassificationContext =
new TextClassificationContext.Builder(PACKAGE_NAME, WIDGET_TYPE)
.build();
TextClassifierEvent.ConversationActionsEvent textClassifierEvent =
new TextClassifierEvent.ConversationActionsEvent.Builder(
TextClassifierEvent.TYPE_SMART_ACTION)
.setEntityTypes(ConversationAction.TYPE_CALL_PHONE)
.setScores(0.5f)
.setEventContext(textClassificationContext)
.build();
mTextClassifierEventTronLogger.writeEvent(textClassifierEvent);
ArgumentCaptor<LogMaker> captor = ArgumentCaptor.forClass(LogMaker.class);
Mockito.verify(mMetricsLogger).write(captor.capture());
LogMaker logMaker = captor.getValue();
assertThat(logMaker.getCategory()).isEqualTo(CONVERSATION_ACTIONS);
assertThat(logMaker.getSubtype()).isEqualTo(ACTION_TEXT_SELECTION_SMART_SHARE);
assertThat(logMaker.getTaggedData(FIELD_TEXT_CLASSIFIER_FIRST_ENTITY_TYPE))
.isEqualTo(ConversationAction.TYPE_CALL_PHONE);
assertThat((float) logMaker.getTaggedData(FIELD_TEXT_CLASSIFIER_SCORE))
.isWithin(0.00001f).of(0.5f);
// Never write event time.
assertThat(logMaker.getTaggedData(FIELD_TEXT_CLASSIFIER_EVENT_TIME)).isNull();
assertThat(logMaker.getPackageName()).isEqualTo(PACKAGE_NAME);
assertThat(logMaker.getTaggedData(FIELD_TEXT_CLASSIFIER_WIDGET_TYPE))
.isEqualTo(WIDGET_TYPE);
}
@Test
public void testWriteEvent_unsupportedCategory() {
TextClassifierEvent.TextSelectionEvent textClassifierEvent =
new TextClassifierEvent.TextSelectionEvent.Builder(
TextClassifierEvent.TYPE_SMART_ACTION)
.build();
mTextClassifierEventTronLogger.writeEvent(textClassifierEvent);
Mockito.verify(mMetricsLogger, Mockito.never()).write(Mockito.any(LogMaker.class));
}
}