diff options
38 files changed, 61 insertions, 7015 deletions
diff --git a/Android.bp b/Android.bp index 8adf48dc49b6..da5d62429035 100644 --- a/Android.bp +++ b/Android.bp @@ -791,10 +791,6 @@ java_library { "libphonenumber-platform", "tagsoup", "rappor", - "libtextclassifier-java", - ], - required: [ - "libtextclassifier", ], dxflags: ["--core-library"], } diff --git a/apct-tests/perftests/textclassifier/src/android/view/textclassifier/TextClassificationManagerPerfTest.java b/apct-tests/perftests/textclassifier/src/android/view/textclassifier/TextClassificationManagerPerfTest.java index f61ea8549236..46250d74a4e3 100644 --- a/apct-tests/perftests/textclassifier/src/android/view/textclassifier/TextClassificationManagerPerfTest.java +++ b/apct-tests/perftests/textclassifier/src/android/view/textclassifier/TextClassificationManagerPerfTest.java @@ -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(); } } diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java index 3ff6f549e337..9dfbc285c381 100644 --- a/core/java/android/service/textclassifier/TextClassifierService.java +++ b/core/java/android/service/textclassifier/TextClassifierService.java @@ -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,27 +404,19 @@ public abstract class TextClassifierService extends Service { */ @NonNull public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) { - final TextClassificationManager tcm = - context.getSystemService(TextClassificationManager.class); - if (tcm == null) { + final String defaultTextClassifierPackageName = + context.getPackageManager().getDefaultTextClassifierPackageName(); + if (TextUtils.isEmpty(defaultTextClassifierPackageName)) { return TextClassifier.NO_OP; } - TextClassificationConstants settings = new TextClassificationConstants(); - if (settings.getUseDefaultTextClassifierAsDefaultImplementation()) { - final String defaultTextClassifierPackageName = - context.getPackageManager().getDefaultTextClassifierPackageName(); - if (TextUtils.isEmpty(defaultTextClassifierPackageName)) { - return TextClassifier.NO_OP; - } - if (defaultTextClassifierPackageName.equals(context.getPackageName())) { - throw new RuntimeException( - "The default text classifier itself should not call the" - + "getDefaultTextClassifierImplementation() method."); - } - return tcm.getTextClassifier(TextClassifier.DEFAULT_SERVICE); - } else { - return tcm.getTextClassifier(TextClassifier.LOCAL); + if (defaultTextClassifierPackageName.equals(context.getPackageName())) { + throw new RuntimeException( + "The default text classifier itself should not call the" + + "getDefaultTextClassifierImplementation() method."); } + final TextClassificationManager tcm = + context.getSystemService(TextClassificationManager.class); + return tcm.getTextClassifier(TextClassifier.DEFAULT_SYSTEM); } /** @hide **/ diff --git a/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java b/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java deleted file mode 100644 index 31645672f049..000000000000 --- a/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java +++ /dev/null @@ -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(); - } - } - } -} diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java deleted file mode 100644 index 3ed48f655d47..000000000000 --- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java +++ /dev/null @@ -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()); - } -} diff --git a/core/java/android/view/textclassifier/ExtrasUtils.java b/core/java/android/view/textclassifier/ExtrasUtils.java index 11e0e2ca072c..9e2b6427eaea 100644 --- a/core/java/android/view/textclassifier/ExtrasUtils.java +++ b/core/java/android/view/textclassifier/ExtrasUtils.java @@ -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) { @@ -93,72 +54,6 @@ public final class ExtrasUtils { } /** - * @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. */ @Nullable @@ -167,48 +62,6 @@ public final class ExtrasUtils { } /** - * 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. */ @Nullable @@ -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); - } -} +}
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/GenerateLinksLogger.java b/core/java/android/view/textclassifier/GenerateLinksLogger.java deleted file mode 100644 index 17ec73ad395f..000000000000 --- a/core/java/android/view/textclassifier/GenerateLinksLogger.java +++ /dev/null @@ -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(); - } - } -} diff --git a/core/java/android/view/textclassifier/Log.java b/core/java/android/view/textclassifier/Log.java index 03ed4967e7b6..98ee09ce29c4 100644 --- a/core/java/android/view/textclassifier/Log.java +++ b/core/java/android/view/textclassifier/Log.java @@ -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() { } diff --git a/core/java/android/view/textclassifier/ModelFileManager.java b/core/java/android/view/textclassifier/ModelFileManager.java deleted file mode 100644 index 0a4ff5d559ab..000000000000 --- a/core/java/android/view/textclassifier/ModelFileManager.java +++ /dev/null @@ -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()); - } - } -} diff --git a/core/java/android/view/textclassifier/SelectionEvent.java b/core/java/android/view/textclassifier/SelectionEvent.java index 9a5454472076..6f9556b6a96e 100644 --- a/core/java/android/view/textclassifier/SelectionEvent.java +++ b/core/java/android/view/textclassifier/SelectionEvent.java @@ -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; } /** diff --git a/core/java/android/view/textclassifier/SelectionSessionLogger.java b/core/java/android/view/textclassifier/SelectionSessionLogger.java index ae9f65ba6129..e7d896eb3f23 100644 --- a/core/java/android/view/textclassifier/SelectionSessionLogger.java +++ b/core/java/android/view/textclassifier/SelectionSessionLogger.java @@ -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; - } } } diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java index 86ef4e103990..8eac1c1520c0 100644 --- a/core/java/android/view/textclassifier/SystemTextClassifier.java +++ b/core/java/android/view/textclassifier/SystemTextClassifier.java @@ -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; diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index 8b9d12916595..3aed32adea1c 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -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); } } diff --git a/core/java/android/view/textclassifier/TextClassificationConstants.java b/core/java/android/view/textclassifier/TextClassificationConstants.java index 3d5ac58e7704..adb6fea91799 100644 --- a/core/java/android/view/textclassifier/TextClassificationConstants.java +++ b/core/java/android/view/textclassifier/TextClassificationConstants.java @@ -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. */ @@ -75,88 +67,10 @@ 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; - } - } }
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java index dfbec9b67027..fa4f7d6d32da 100644 --- a/core/java/android/view/textclassifier/TextClassificationManager.java +++ b/core/java/android/view/textclassifier/TextClassificationManager.java @@ -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(); - } - } - } - } } diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 2cc226d5a601..6d5077a99c62 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -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"); } } } diff --git a/core/java/android/view/textclassifier/TextClassifierEventTronLogger.java b/core/java/android/view/textclassifier/TextClassifierEventTronLogger.java deleted file mode 100644 index 8162699a74c6..000000000000 --- a/core/java/android/view/textclassifier/TextClassifierEventTronLogger.java +++ /dev/null @@ -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()); - } -} diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java deleted file mode 100644 index d7149ee05b57..000000000000 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ /dev/null @@ -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(); - } - } -} diff --git a/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java b/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java deleted file mode 100644 index 22e374f2b38f..000000000000 --- a/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java +++ /dev/null @@ -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())); - } -} diff --git a/core/java/android/view/textclassifier/intent/LabeledIntent.java b/core/java/android/view/textclassifier/intent/LabeledIntent.java deleted file mode 100644 index cbd9d1a522f6..000000000000 --- a/core/java/android/view/textclassifier/intent/LabeledIntent.java +++ /dev/null @@ -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); - } -} diff --git a/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java b/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java deleted file mode 100644 index 8d60ad850afa..000000000000 --- a/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java +++ /dev/null @@ -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; - } -} diff --git a/core/java/android/view/textclassifier/intent/TemplateClassificationIntentFactory.java b/core/java/android/view/textclassifier/intent/TemplateClassificationIntentFactory.java deleted file mode 100644 index aef4bd6c7351..000000000000 --- a/core/java/android/view/textclassifier/intent/TemplateClassificationIntentFactory.java +++ /dev/null @@ -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; - } -} diff --git a/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java b/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java deleted file mode 100644 index 7a3956996972..000000000000 --- a/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java +++ /dev/null @@ -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; - } -} diff --git a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java deleted file mode 100644 index 28cb80d0c74c..000000000000 --- a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ /dev/null @@ -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; - } - } - } -} diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 4ef3f61b5280..45943f512c22 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -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( diff --git a/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java b/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java deleted file mode 100644 index 87449979704d..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java +++ /dev/null @@ -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(); - } - -} diff --git a/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java b/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java deleted file mode 100644 index ec7e83f1f243..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/ActionsSuggestionsHelperTest.java +++ /dev/null @@ -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); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java deleted file mode 100644 index 79e1406f6ae6..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java +++ /dev/null @@ -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(); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java index 82fa73f76207..2f21b7f0c75a 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java @@ -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); diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java index 1ca46491b363..628252d8ca6c 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java @@ -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); } } diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java deleted file mode 100644 index 372a4787cf1a..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java +++ /dev/null @@ -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); - } - }; - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java deleted file mode 100644 index 3ad26f5d5108..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java +++ /dev/null @@ -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"); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java deleted file mode 100644 index 8891d3fd2dca..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java +++ /dev/null @@ -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); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java deleted file mode 100644 index bcea5fea6a13..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java +++ /dev/null @@ -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 - ) - }; - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java deleted file mode 100644 index a33c35811276..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java +++ /dev/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(); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/logging/GenerateLinksLoggerTest.java b/core/tests/coretests/src/android/view/textclassifier/logging/GenerateLinksLoggerTest.java deleted file mode 100644 index 5e8e5823eefa..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/logging/GenerateLinksLoggerTest.java +++ /dev/null @@ -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)); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/logging/SmartSelectionEventTrackerTest.java b/core/tests/coretests/src/android/view/textclassifier/logging/SmartSelectionEventTrackerTest.java deleted file mode 100644 index 321a7f21c307..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/logging/SmartSelectionEventTrackerTest.java +++ /dev/null @@ -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(); - } -} diff --git a/core/tests/coretests/src/android/view/textclassifier/logging/TextClassifierEventTronLoggerTest.java b/core/tests/coretests/src/android/view/textclassifier/logging/TextClassifierEventTronLoggerTest.java deleted file mode 100644 index 2c540e560f6b..000000000000 --- a/core/tests/coretests/src/android/view/textclassifier/logging/TextClassifierEventTronLoggerTest.java +++ /dev/null @@ -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)); - } -} |