summaryrefslogtreecommitdiff
path: root/android/view/textclassifier
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-11-17 16:38:15 -0500
committerJustin Klaassen <justinklaassen@google.com>2017-11-17 16:38:15 -0500
commit6a65f2da209bff03cb0eb6da309710ac6ee5026d (patch)
tree48e2090e716d4178378cb0599fc5d9cffbcf3f63 /android/view/textclassifier
parent46c77c203439b3b37c99d09e326df4b1fe08c10b (diff)
downloadandroid-28-6a65f2da209bff03cb0eb6da309710ac6ee5026d.tar.gz
Import Android SDK Platform P [4456821]
/google/data/ro/projects/android/fetch_artifact \ --bid 4456821 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4456821.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: I2d206b200d7952f899a5d1647ab532638cc8dd43
Diffstat (limited to 'android/view/textclassifier')
-rw-r--r--android/view/textclassifier/TextClassification.java28
-rw-r--r--android/view/textclassifier/TextClassifier.java84
-rw-r--r--android/view/textclassifier/TextClassifierImpl.java275
-rw-r--r--android/view/textclassifier/TextLinks.java252
-rw-r--r--android/view/textclassifier/TextSelection.java53
-rw-r--r--android/view/textclassifier/logging/SmartSelectionEventTracker.java179
6 files changed, 527 insertions, 344 deletions
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index 26d2141e..2779aa2d 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -23,6 +23,7 @@ import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
+import android.os.LocaleList;
import android.view.View.OnClickListener;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -438,4 +439,31 @@ public final class TextClassification {
mLogType, mVersionInfo);
}
}
+
+ /**
+ * TextClassification optional input parameters.
+ */
+ public static final class Options {
+
+ private LocaleList mDefaultLocales;
+
+ /**
+ * @param defaultLocales ordered list of locale preferences that may be used to disambiguate
+ * the provided text. If no locale preferences exist, set this to null or an empty
+ * locale list.
+ */
+ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+ }
}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index 46dbd0e3..aeb84897 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -56,23 +56,7 @@ public interface TextClassifier {
* No-op TextClassifier.
* This may be used to turn off TextClassifier features.
*/
- TextClassifier NO_OP = new TextClassifier() {
-
- @Override
- public TextSelection suggestSelection(
- CharSequence text,
- int selectionStartIndex,
- int selectionEndIndex,
- LocaleList defaultLocales) {
- return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
- }
-
- @Override
- public TextClassification classifyText(
- CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) {
- return TextClassification.EMPTY;
- }
- };
+ TextClassifier NO_OP = new TextClassifier() {};
/**
* Returns suggested text selection start and end indices, recognized entity types, and their
@@ -82,21 +66,34 @@ public interface TextClassifier {
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
* @param selectionEndIndex end index of the selected part of text
- * @param defaultLocales ordered list of locale preferences that can be used to disambiguate
- * the provided text. If no locale preferences exist, set this to null or an empty locale
- * list in which case the classifier will decide whether to use no locale information, use
- * a default locale, or use the system default.
+ * @param options optional input parameters
*
* @throws IllegalArgumentException if text is null; selectionStartIndex is negative;
* selectionEndIndex is greater than text.length() or not greater than selectionStartIndex
*/
@WorkerThread
@NonNull
- TextSelection suggestSelection(
+ default TextSelection suggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options) {
+ return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
+ }
+
+ /**
+ * @see #suggestSelection(CharSequence, int, int, TextSelection.Options)
+ */
+ // TODO: Consider deprecating (b/68846316)
+ @WorkerThread
+ @NonNull
+ default TextSelection suggestSelection(
@NonNull CharSequence text,
@IntRange(from = 0) int selectionStartIndex,
@IntRange(from = 0) int selectionEndIndex,
- @Nullable LocaleList defaultLocales);
+ @Nullable LocaleList defaultLocales) {
+ return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
+ }
/**
* Classifies the specified text and returns a {@link TextClassification} object that can be
@@ -106,41 +103,48 @@ public interface TextClassifier {
* by the sub sequence starting at startIndex and ending at endIndex)
* @param startIndex start index of the text to classify
* @param endIndex end index of the text to classify
- * @param defaultLocales ordered list of locale preferences that can be used to disambiguate
- * the provided text. If no locale preferences exist, set this to null or an empty locale
- * list in which case the classifier will decide whether to use no locale information, use
- * a default locale, or use the system default.
+ * @param options optional input parameters
*
* @throws IllegalArgumentException if text is null; startIndex is negative;
* endIndex is greater than text.length() or not greater than startIndex
*/
@WorkerThread
@NonNull
- TextClassification classifyText(
+ default TextClassification classifyText(
@NonNull CharSequence text,
@IntRange(from = 0) int startIndex,
@IntRange(from = 0) int endIndex,
- @Nullable LocaleList defaultLocales);
+ @Nullable TextClassification.Options options) {
+ return TextClassification.EMPTY;
+ }
/**
- * Returns a {@link LinksInfo} that may be applied to the text to annotate it with links
+ * @see #classifyText(CharSequence, int, int, TextClassification.Options)
+ */
+ // TODO: Consider deprecating (b/68846316)
+ @WorkerThread
+ @NonNull
+ default TextClassification classifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable LocaleList defaultLocales) {
+ return TextClassification.EMPTY;
+ }
+
+ /**
+ * Returns a {@link TextLinks} that may be applied to the text to annotate it with links
* information.
*
* @param text the text to generate annotations for
- * @param linkMask See {@link android.text.util.Linkify} for a list of linkMasks that may be
- * specified. Subclasses of this interface may specify additional linkMasks
- * @param defaultLocales ordered list of locale preferences that can be used to disambiguate
- * the provided text. If no locale preferences exist, set this to null or an empty locale
- * list in which case the classifier will decide whether to use no locale information, use
- * a default locale, or use the system default.
+ * @param options configuration for link generation. If null, defaults will be used.
*
* @throws IllegalArgumentException if text is null
- * @hide
*/
@WorkerThread
- default LinksInfo getLinks(
- @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales) {
- return LinksInfo.NO_OP;
+ default TextLinks generateLinks(
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+ return new TextLinks.Builder(text.toString()).build();
}
/**
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index 1c07be4b..2ad6e02c 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -30,13 +30,8 @@ import android.os.ParcelFileDescriptor;
import android.provider.Browser;
import android.provider.ContactsContract;
import android.provider.Settings;
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.text.method.WordIterator;
-import android.text.style.ClickableSpan;
import android.text.util.Linkify;
import android.util.Patterns;
-import android.view.View;
import android.widget.TextViewMetrics;
import com.android.internal.annotations.GuardedBy;
@@ -46,13 +41,8 @@ import com.android.internal.util.Preconditions;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.text.BreakIterator;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -100,16 +90,24 @@ final class TextClassifierImpl implements TextClassifier {
@Override
public TextSelection suggestSelection(
@NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
- @Nullable LocaleList defaultLocales) {
+ @Nullable TextSelection.Options options) {
validateInput(text, selectionStartIndex, selectionEndIndex);
try {
if (text.length() > 0) {
- final SmartSelection smartSelection = getSmartSelection(defaultLocales);
+ final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
+ final SmartSelection smartSelection = getSmartSelection(locales);
final String string = text.toString();
- final int[] startEnd = smartSelection.suggest(
- string, selectionStartIndex, selectionEndIndex);
- final int start = startEnd[0];
- final int end = startEnd[1];
+ final int start;
+ final int end;
+ if (getSettings().isDarkLaunch() && !options.isDarkLaunchAllowed()) {
+ start = selectionStartIndex;
+ end = selectionEndIndex;
+ } else {
+ final int[] startEnd = smartSelection.suggest(
+ string, selectionStartIndex, selectionEndIndex);
+ start = startEnd[0];
+ end = startEnd[1];
+ }
if (start <= end
&& start >= 0 && end <= string.length()
&& start <= selectionStartIndex && end >= selectionEndIndex) {
@@ -139,18 +137,27 @@ final class TextClassifierImpl implements TextClassifier {
}
// Getting here means something went wrong, return a NO_OP result.
return TextClassifier.NO_OP.suggestSelection(
- text, selectionStartIndex, selectionEndIndex, defaultLocales);
+ text, selectionStartIndex, selectionEndIndex, options);
+ }
+
+ @Override
+ public TextSelection suggestSelection(
+ @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ @Nullable LocaleList defaultLocales) {
+ return suggestSelection(text, selectionStartIndex, selectionEndIndex,
+ new TextSelection.Options().setDefaultLocales(defaultLocales));
}
@Override
public TextClassification classifyText(
@NonNull CharSequence text, int startIndex, int endIndex,
- @Nullable LocaleList defaultLocales) {
+ @Nullable TextClassification.Options options) {
validateInput(text, startIndex, endIndex);
try {
if (text.length() > 0) {
final String string = text.toString();
- SmartSelection.ClassificationResult[] results = getSmartSelection(defaultLocales)
+ final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
+ final SmartSelection.ClassificationResult[] results = getSmartSelection(locales)
.classifyText(string, startIndex, endIndex,
getHintFlags(string, startIndex, endIndex));
if (results.length > 0) {
@@ -165,23 +172,41 @@ final class TextClassifierImpl implements TextClassifier {
Log.e(LOG_TAG, "Error getting text classification info.", t);
}
// Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.classifyText(
- text, startIndex, endIndex, defaultLocales);
+ return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options);
}
@Override
- public LinksInfo getLinks(
- @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales) {
- Preconditions.checkArgument(text != null);
+ public TextClassification classifyText(
+ @NonNull CharSequence text, int startIndex, int endIndex,
+ @Nullable LocaleList defaultLocales) {
+ return classifyText(text, startIndex, endIndex,
+ new TextClassification.Options().setDefaultLocales(defaultLocales));
+ }
+
+ @Override
+ public TextLinks generateLinks(
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+ Preconditions.checkNotNull(text);
+ final String textString = text.toString();
+ final TextLinks.Builder builder = new TextLinks.Builder(textString);
try {
- return LinksInfoFactory.create(
- mContext, getSmartSelection(defaultLocales), text.toString(), linkMask);
+ LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
+ final SmartSelection smartSelection = getSmartSelection(defaultLocales);
+ final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString);
+ for (SmartSelection.AnnotatedSpan span : annotations) {
+ final Map<String, Float> entityScores = new HashMap<>();
+ final SmartSelection.ClassificationResult[] results = span.getClassification();
+ for (int i = 0; i < results.length; i++) {
+ entityScores.put(results[i].mCollection, results[i].mScore);
+ }
+ builder.addLink(new TextLinks.TextLink(
+ textString, span.getStartIndex(), span.getEndIndex(), entityScores));
+ }
} catch (Throwable t) {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error getting links info.", t);
}
- // Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales);
+ return builder.build();
}
@Override
@@ -210,7 +235,9 @@ final class TextClassifierImpl implements TextClassifier {
if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
destroySmartSelectionIfExistsLocked();
final ParcelFileDescriptor fd = getFdLocked(locale);
- mSmartSelection = new SmartSelection(fd.getFd());
+ final int modelFd = fd.getFd();
+ mVersion = SmartSelection.getVersion(modelFd);
+ mSmartSelection = new SmartSelection(modelFd);
closeAndLogError(fd);
mLocale = locale;
}
@@ -231,18 +258,26 @@ final class TextClassifierImpl implements TextClassifier {
@GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
ParcelFileDescriptor updateFd;
+ int updateVersion = -1;
try {
updateFd = ParcelFileDescriptor.open(
new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
+ if (updateFd != null) {
+ updateVersion = SmartSelection.getVersion(updateFd.getFd());
+ }
} catch (FileNotFoundException e) {
updateFd = null;
}
ParcelFileDescriptor factoryFd;
+ int factoryVersion = -1;
try {
final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale);
if (factoryModelFilePath != null) {
factoryFd = ParcelFileDescriptor.open(
new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
+ if (factoryFd != null) {
+ factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
+ }
} else {
factoryFd = null;
}
@@ -278,15 +313,11 @@ final class TextClassifierImpl implements TextClassifier {
return factoryFd;
}
- final int updateVersion = SmartSelection.getVersion(updateFdInt);
- final int factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
if (updateVersion > factoryVersion) {
closeAndLogError(factoryFd);
- mVersion = updateVersion;
return updateFd;
} else {
closeAndLogError(updateFd);
- mVersion = factoryVersion;
return factoryFd;
}
}
@@ -466,180 +497,6 @@ final class TextClassifierImpl implements TextClassifier {
}
/**
- * Detects and creates links for specified text.
- */
- private static final class LinksInfoFactory {
-
- private LinksInfoFactory() {}
-
- public static LinksInfo create(
- Context context, SmartSelection smartSelection, String text, int linkMask) {
- final WordIterator wordIterator = new WordIterator();
- wordIterator.setCharSequence(text, 0, text.length());
- final List<SpanSpec> spans = new ArrayList<>();
- int start = 0;
- int end;
- while ((end = wordIterator.nextBoundary(start)) != BreakIterator.DONE) {
- final String token = text.substring(start, end);
- if (TextUtils.isEmpty(token)) {
- continue;
- }
-
- final int[] selection = smartSelection.suggest(text, start, end);
- final int selectionStart = selection[0];
- final int selectionEnd = selection[1];
- if (selectionStart >= 0 && selectionEnd <= text.length()
- && selectionStart <= selectionEnd) {
- final SmartSelection.ClassificationResult[] results =
- smartSelection.classifyText(
- text, selectionStart, selectionEnd,
- getHintFlags(text, selectionStart, selectionEnd));
- if (results.length > 0) {
- final String type = getHighestScoringType(results);
- if (matches(type, linkMask)) {
- // For links without disambiguation, we simply use the default intent.
- final List<Intent> intents = IntentFactory.create(
- context, type, text.substring(selectionStart, selectionEnd));
- if (!intents.isEmpty() && hasActivityHandler(context, intents.get(0))) {
- final ClickableSpan span = createSpan(context, intents.get(0));
- spans.add(new SpanSpec(selectionStart, selectionEnd, span));
- }
- }
- }
- }
- start = end;
- }
- return new LinksInfoImpl(text, avoidOverlaps(spans, text));
- }
-
- /**
- * Returns true if the classification type matches the specified linkMask.
- */
- private static boolean matches(String type, int linkMask) {
- type = type.trim().toLowerCase(Locale.ENGLISH);
- if ((linkMask & Linkify.PHONE_NUMBERS) != 0
- && TextClassifier.TYPE_PHONE.equals(type)) {
- return true;
- }
- if ((linkMask & Linkify.EMAIL_ADDRESSES) != 0
- && TextClassifier.TYPE_EMAIL.equals(type)) {
- return true;
- }
- if ((linkMask & Linkify.MAP_ADDRESSES) != 0
- && TextClassifier.TYPE_ADDRESS.equals(type)) {
- return true;
- }
- if ((linkMask & Linkify.WEB_URLS) != 0
- && TextClassifier.TYPE_URL.equals(type)) {
- return true;
- }
- return false;
- }
-
- /**
- * Trim the number of spans so that no two spans overlap.
- *
- * This algorithm first ensures that there is only one span per start index, then it
- * makes sure that no two spans overlap.
- */
- private static List<SpanSpec> avoidOverlaps(List<SpanSpec> spans, String text) {
- Collections.sort(spans, Comparator.comparingInt(span -> span.mStart));
- // Group spans by start index. Take the longest span.
- final Map<Integer, SpanSpec> reps = new LinkedHashMap<>(); // order matters.
- final int size = spans.size();
- for (int i = 0; i < size; i++) {
- final SpanSpec span = spans.get(i);
- final LinksInfoFactory.SpanSpec rep = reps.get(span.mStart);
- if (rep == null || rep.mEnd < span.mEnd) {
- reps.put(span.mStart, span);
- }
- }
- // Avoid span intersections. Take the longer span.
- final LinkedList<SpanSpec> result = new LinkedList<>();
- for (SpanSpec rep : reps.values()) {
- if (result.isEmpty()) {
- result.add(rep);
- continue;
- }
-
- final SpanSpec last = result.getLast();
- if (rep.mStart < last.mEnd) {
- // Spans intersect. Use the one with characters.
- if ((rep.mEnd - rep.mStart) > (last.mEnd - last.mStart)) {
- result.set(result.size() - 1, rep);
- }
- } else {
- result.add(rep);
- }
- }
- return result;
- }
-
- private static ClickableSpan createSpan(final Context context, final Intent intent) {
- return new ClickableSpan() {
- // TODO: Style this span.
- @Override
- public void onClick(View widget) {
- context.startActivity(intent);
- }
- };
- }
-
- private static boolean hasActivityHandler(Context context, Intent intent) {
- if (intent == null) {
- return false;
- }
- final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
- return resolveInfo != null && resolveInfo.activityInfo != null;
- }
-
- /**
- * Implementation of LinksInfo that adds ClickableSpans to the specified text.
- */
- private static final class LinksInfoImpl implements LinksInfo {
-
- private final CharSequence mOriginalText;
- private final List<SpanSpec> mSpans;
-
- LinksInfoImpl(CharSequence originalText, List<SpanSpec> spans) {
- mOriginalText = originalText;
- mSpans = spans;
- }
-
- @Override
- public boolean apply(@NonNull CharSequence text) {
- Preconditions.checkArgument(text != null);
- if (text instanceof Spannable && mOriginalText.toString().equals(text.toString())) {
- Spannable spannable = (Spannable) text;
- final int size = mSpans.size();
- for (int i = 0; i < size; i++) {
- final SpanSpec span = mSpans.get(i);
- spannable.setSpan(span.mSpan, span.mStart, span.mEnd, 0);
- }
- return true;
- }
- return false;
- }
- }
-
- /**
- * Span plus its start and end index.
- */
- private static final class SpanSpec {
-
- private final int mStart;
- private final int mEnd;
- private final ClickableSpan mSpan;
-
- SpanSpec(int start, int end, ClickableSpan span) {
- mStart = start;
- mEnd = end;
- mSpan = span;
- }
- }
- }
-
- /**
* Creates intents based on the classification type.
*/
private static final class IntentFactory {
@@ -656,8 +513,8 @@ final class TextClassifierImpl implements TextClassifier {
intents.add(new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse(String.format("mailto:%s", text))));
intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT)
- .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
- .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
+ .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE)
+ .putExtra(ContactsContract.Intents.Insert.EMAIL, text));
break;
case TextClassifier.TYPE_PHONE:
intents.add(new Intent(Intent.ACTION_DIAL)
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
new file mode 100644
index 00000000..f3cc827f
--- /dev/null
+++ b/android/view/textclassifier/TextLinks.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 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.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.LocaleList;
+import android.text.SpannableString;
+import android.text.style.ClickableSpan;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * A collection of links, representing subsequences of text and the entity types (phone number,
+ * address, url, etc) they may be.
+ */
+public final class TextLinks {
+ private final String mFullText;
+ private final List<TextLink> mLinks;
+
+ private TextLinks(String fullText, Collection<TextLink> links) {
+ mFullText = fullText;
+ mLinks = Collections.unmodifiableList(new ArrayList<>(links));
+ }
+
+ /**
+ * Returns an unmodifiable Collection of the links.
+ */
+ public Collection<TextLink> getLinks() {
+ return mLinks;
+ }
+
+ /**
+ * Annotates the given text with the generated links. It will fail if the provided text doesn't
+ * match the original text used to crete the TextLinks.
+ *
+ * @param text the text to apply the links to. Must match the original text.
+ * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null.
+ *
+ * @return Success or failure.
+ */
+ public boolean apply(
+ @NonNull SpannableString text,
+ @Nullable Function<TextLink, ClickableSpan> spanFactory) {
+ Preconditions.checkNotNull(text);
+ if (!mFullText.equals(text.toString())) {
+ return false;
+ }
+
+ if (spanFactory == null) {
+ spanFactory = DEFAULT_SPAN_FACTORY;
+ }
+ for (TextLink link : mLinks) {
+ final ClickableSpan span = spanFactory.apply(link);
+ if (span != null) {
+ text.setSpan(span, link.getStart(), link.getEnd(), 0);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A link, identifying a substring of text and possible entity types for it.
+ */
+ public static final class TextLink {
+ private final EntityConfidence<String> mEntityScores;
+ private final String mOriginalText;
+ private final int mStart;
+ private final int mEnd;
+
+ /**
+ * Create a new TextLink.
+ *
+ * @throws IllegalArgumentException if entityScores is null or empty.
+ */
+ public TextLink(String originalText, int start, int end, Map<String, Float> entityScores) {
+ Preconditions.checkNotNull(originalText);
+ Preconditions.checkNotNull(entityScores);
+ Preconditions.checkArgument(!entityScores.isEmpty());
+ Preconditions.checkArgument(start <= end);
+ mOriginalText = originalText;
+ mStart = start;
+ mEnd = end;
+ mEntityScores = new EntityConfidence<>();
+
+ for (Map.Entry<String, Float> entry : entityScores.entrySet()) {
+ mEntityScores.setEntityType(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Returns the start index of this link in the original text.
+ *
+ * @return the start index.
+ */
+ public int getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the end index of this link in the original text.
+ *
+ * @return the end index.
+ */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the number of entity types that have confidence scores.
+ *
+ * @return the entity count.
+ */
+ public int getEntityCount() {
+ return mEntityScores.getEntities().size();
+ }
+
+ /**
+ * Returns the entity type at a given index. Entity types are sorted by confidence.
+ *
+ * @return the entity type at the provided index.
+ */
+ @NonNull public @TextClassifier.EntityType String getEntity(int index) {
+ return mEntityScores.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for a particular entity type.
+ *
+ * @param entityType the entity type.
+ */
+ public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore(
+ @TextClassifier.EntityType String entityType) {
+ return mEntityScores.getConfidenceScore(entityType);
+ }
+ }
+
+ /**
+ * Optional input parameters for generating TextLinks.
+ */
+ public static final class Options {
+ private final LocaleList mLocaleList;
+
+ private Options(LocaleList localeList) {
+ this.mLocaleList = localeList;
+ }
+
+ /**
+ * Builder to construct Options.
+ */
+ public static final class Builder {
+ private LocaleList mLocaleList;
+
+ /**
+ * Sets the LocaleList to use.
+ *
+ * @return this Builder.
+ */
+ public Builder setLocaleList(@Nullable LocaleList localeList) {
+ this.mLocaleList = localeList;
+ return this;
+ }
+
+ /**
+ * Builds the Options object.
+ */
+ public Options build() {
+ return new Options(mLocaleList);
+ }
+ }
+ public @Nullable LocaleList getDefaultLocales() {
+ return mLocaleList;
+ }
+ };
+
+ /**
+ * A function to create spans from TextLinks.
+ *
+ * Applies only to TextViews.
+ * We can hide this until we are convinced we want it to be part of the public API.
+ *
+ * @hide
+ */
+ public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY =
+ new Function<TextLink, ClickableSpan>() {
+ @Override
+ public ClickableSpan apply(TextLink textLink) {
+ // TODO: Implement.
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+ };
+
+ /**
+ * A builder to construct a TextLinks instance.
+ */
+ public static final class Builder {
+ private final String mFullText;
+ private final Collection<TextLink> mLinks;
+
+ /**
+ * Create a new TextLinks.Builder.
+ *
+ * @param fullText The full text that links will be added to.
+ */
+ public Builder(@NonNull String fullText) {
+ mFullText = Preconditions.checkNotNull(fullText);
+ mLinks = new ArrayList<>();
+ }
+
+ /**
+ * Adds a TextLink.
+ *
+ * @return this instance.
+ */
+ public Builder addLink(TextLink link) {
+ Preconditions.checkNotNull(link);
+ mLinks.add(link);
+ return this;
+ }
+
+ /**
+ * Constructs a TextLinks instance.
+ *
+ * @return the constructed TextLinks.
+ */
+ public TextLinks build() {
+ return new TextLinks(mFullText, mLinks);
+ }
+ }
+}
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 11ebe835..0a67954a 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -19,6 +19,8 @@ package android.view.textclassifier;
import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.LocaleList;
import android.view.textclassifier.TextClassifier.EntityType;
import com.android.internal.util.Preconditions;
@@ -181,4 +183,55 @@ public final class TextSelection {
mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo);
}
}
+
+ /**
+ * TextSelection optional input parameters.
+ */
+ public static final class Options {
+
+ private LocaleList mDefaultLocales;
+ private boolean mDarkLaunchAllowed;
+
+ /**
+ * @param defaultLocales ordered list of locale preferences that may be used to disambiguate
+ * the provided text. If no locale preferences exist, set this to null or an empty
+ * locale list.
+ */
+ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ /**
+ * @param allowed whether or not the TextClassifier should return selection suggestions
+ * when "dark launched". When a TextClassifier is dark launched, it can suggest
+ * selection changes that should not be used to actually change the user's selection.
+ * Instead, the suggested selection is logged, compared with the user's selection
+ * interaction, and used to generate quality metrics for the TextClassifier.
+ *
+ * @hide
+ */
+ public void setDarkLaunchAllowed(boolean allowed) {
+ mDarkLaunchAllowed = allowed;
+ }
+
+ /**
+ * Returns true if the TextClassifier should return selection suggestions when
+ * "dark launched". Otherwise, returns false.
+ *
+ * @hide
+ */
+ public boolean isDarkLaunchAllowed() {
+ return mDarkLaunchAllowed;
+ }
+ }
}
diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java
index 83af19bb..2833564f 100644
--- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java
+++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java
@@ -48,31 +48,45 @@ public final class SmartSelectionEventTracker {
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 VERSION_TAG = MetricsEvent.FIELD_SELECTION_VERSION_TAG;
- private static final int SMART_INDICES = MetricsEvent.FIELD_SELECTION_SMART_RANGE;
- private static final int EVENT_INDICES = MetricsEvent.FIELD_SELECTION_RANGE;
+ 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 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;
@@ -83,10 +97,18 @@ public final class SmartSelectionEventTracker {
private long mSessionStartTime;
private long mLastEventTime;
private boolean mSmartSelectionTriggered;
- private String mVersionTag;
+ private String mModelName;
public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) {
mWidgetType = widgetType;
+ mWidgetVersion = null;
+ mContext = Preconditions.checkNotNull(context);
+ }
+
+ public SmartSelectionEventTracker(
+ @NonNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion) {
+ mWidgetType = widgetType;
+ mWidgetVersion = widgetVersion;
mContext = Preconditions.checkNotNull(context);
}
@@ -115,7 +137,7 @@ public final class SmartSelectionEventTracker {
case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through
case SelectionEvent.EventType.SMART_SELECTION_MULTI:
mSmartSelectionTriggered = true;
- mVersionTag = getVersionTag(event);
+ mModelName = getModelName(event);
mSmartIndices[0] = event.mStart;
mSmartIndices[1] = event.mEnd;
break;
@@ -137,14 +159,19 @@ public final class SmartSelectionEventTracker {
final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime;
final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
.setType(getLogType(event))
- .setSubtype(getLogSubType(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(VERSION_TAG, mVersionTag)
- .addTaggedData(SMART_INDICES, getSmartDelta())
- .addTaggedData(EVENT_INDICES, getEventDelta(event))
+ .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);
@@ -169,7 +196,7 @@ public final class SmartSelectionEventTracker {
mSessionStartTime = 0;
mLastEventTime = 0;
mSmartSelectionTriggered = false;
- mVersionTag = getVersionTag(null);
+ mModelName = getModelName(null);
mSessionId = null;
}
@@ -251,113 +278,75 @@ public final class SmartSelectionEventTracker {
}
}
- private static int getLogSubType(SelectionEvent event) {
- switch (event.mEntityType) {
- case TextClassifier.TYPE_OTHER:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER;
- case TextClassifier.TYPE_EMAIL:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL;
- case TextClassifier.TYPE_PHONE:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE;
- case TextClassifier.TYPE_ADDRESS:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS;
- case TextClassifier.TYPE_URL:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_URL;
- default:
- return MetricsEvent.TEXT_CLASSIFIER_TYPE_UNKNOWN;
- }
- }
-
- private static String getLogSubTypeString(int logSubType) {
- switch (logSubType) {
- case MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER:
- return TextClassifier.TYPE_OTHER;
- case MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL:
- return TextClassifier.TYPE_EMAIL;
- case MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE:
- return TextClassifier.TYPE_PHONE;
- case MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS:
- return TextClassifier.TYPE_ADDRESS;
- case MetricsEvent.TEXT_CLASSIFIER_TYPE_URL:
- return TextClassifier.TYPE_URL;
- default:
- return TextClassifier.TYPE_UNKNOWN;
- }
- }
-
- private int getSmartDelta() {
- if (mSmartSelectionTriggered) {
- return (clamp(mSmartIndices[0] - mOrigStart) << 16)
- | (clamp(mSmartIndices[1] - mOrigStart) & 0xffff);
- }
- // If the smart selection model was not run, return invalid selection indices [0,0]. This
- // allows us to tell from the terminal event alone whether the model was run.
- return 0;
+ private int getRangeDelta(int offset) {
+ return offset - mOrigStart;
}
- private int getEventDelta(SelectionEvent event) {
- return (clamp(event.mStart - mOrigStart) << 16)
- | (clamp(event.mEnd - mOrigStart) & 0xffff);
+ private int getSmartRangeDelta(int offset) {
+ return mSmartSelectionTriggered ? getRangeDelta(offset) : 0;
}
- private String getVersionTag(@Nullable SelectionEvent event) {
- final String widgetType;
+ private String getWidgetTypeName() {
switch (mWidgetType) {
case WidgetType.TEXTVIEW:
- widgetType = TEXTVIEW;
- break;
+ return TEXTVIEW;
case WidgetType.WEBVIEW:
- widgetType = WEBVIEW;
- break;
+ return WEBVIEW;
case WidgetType.EDITTEXT:
- widgetType = EDITTEXT;
- break;
+ return EDITTEXT;
case WidgetType.EDIT_WEBVIEW:
- widgetType = EDIT_WEBVIEW;
- break;
+ 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:
- widgetType = UNKNOWN;
+ return UNKNOWN;
}
- final String version = event == null
+ }
+
+ private String getModelName(@Nullable SelectionEvent event) {
+ return event == null
? SelectionEvent.NO_VERSION_TAG
: Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG);
- return String.format("%s/%s", widgetType, version);
}
private static String createSessionId() {
return UUID.randomUUID().toString();
}
- private static int clamp(int val) {
- return Math.max(Math.min(val, Short.MAX_VALUE), Short.MIN_VALUE);
- }
-
private static void debugLog(LogMaker log) {
if (!DEBUG_LOG_ENABLED) return;
- final String tag = Objects.toString(log.getTaggedData(VERSION_TAG), "tag");
+ 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)", tag, sessionId));
+ 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 smartIndices = Integer.parseInt(
- Objects.toString(log.getTaggedData(SMART_INDICES), ZERO));
- final int smartStart = (short) ((smartIndices & 0xffff0000) >> 16);
- final int smartEnd = (short) (smartIndices & 0xffff);
-
- final int eventIndices = Integer.parseInt(
- Objects.toString(log.getTaggedData(EVENT_INDICES), ZERO));
- final int eventStart = (short) ((eventIndices & 0xffff0000) >> 16);
- final int eventEnd = (short) (eventIndices & 0xffff);
-
- Log.d(LOG_TAG, String.format("%2d: %s/%s, context=%d,%d - old=%d,%d (%s)",
- index, type, subType, eventStart, eventEnd, smartStart, smartEnd, tag));
+ 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));
}
/**
@@ -369,12 +358,12 @@ public final class SmartSelectionEventTracker {
/**
* Use this to specify an indeterminate positive index.
*/
- public static final int OUT_OF_BOUNDS = Short.MAX_VALUE;
+ 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 = Short.MIN_VALUE;
+ public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE;
private static final String NO_VERSION_TAG = "";