Remove local text classifier and related tests. am: 293bdf360a
am: e94d4b04bc
Change-Id: I7ca571e879d61b0c76d0a2424fcfb91b06be5d74
This commit is contained in:
@ -790,10 +790,6 @@ java_library {
|
||||
"libphonenumber-platform",
|
||||
"tagsoup",
|
||||
"rappor",
|
||||
"libtextclassifier-java",
|
||||
],
|
||||
required: [
|
||||
"libtextclassifier",
|
||||
],
|
||||
dxflags: ["--core-library"],
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 **/
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user