diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-11-17 16:38:15 -0500 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-11-17 16:38:15 -0500 |
commit | 6a65f2da209bff03cb0eb6da309710ac6ee5026d (patch) | |
tree | 48e2090e716d4178378cb0599fc5d9cffbcf3f63 /android/view/textclassifier | |
parent | 46c77c203439b3b37c99d09e326df4b1fe08c10b (diff) | |
download | android-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.java | 28 | ||||
-rw-r--r-- | android/view/textclassifier/TextClassifier.java | 84 | ||||
-rw-r--r-- | android/view/textclassifier/TextClassifierImpl.java | 275 | ||||
-rw-r--r-- | android/view/textclassifier/TextLinks.java | 252 | ||||
-rw-r--r-- | android/view/textclassifier/TextSelection.java | 53 | ||||
-rw-r--r-- | android/view/textclassifier/logging/SmartSelectionEventTracker.java | 179 |
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 = ""; |