Merge "Fix smart_linkify_enabled flag." into pi-dev

This commit is contained in:
TreeHugger Robot
2018-03-22 22:56:58 +00:00
committed by Android (Google) Code Review
9 changed files with 284 additions and 21 deletions

View File

@@ -650,7 +650,8 @@ public class Linkify {
final CharSequence truncatedText = text.subSequence(
0, Math.min(text.length(), classifier.getMaxGenerateLinksTextLength()));
final Supplier<TextLinks> supplier = () -> classifier.generateLinks(truncatedText, options);
final Supplier<TextLinks> supplier = () ->
classifier.generateLinks(truncatedText, options.setLegacyFallback(true));
final Consumer<TextLinks> consumer = links -> {
if (links.getLinks().isEmpty()) {
if (callback != null) {

View File

@@ -30,6 +30,8 @@ import android.service.textclassifier.ITextLinksCallback;
import android.service.textclassifier.ITextSelectionCallback;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.internal.util.Preconditions;
import java.util.concurrent.CountDownLatch;
@@ -37,8 +39,10 @@ import java.util.concurrent.TimeUnit;
/**
* Proxy to the system's default TextClassifier.
* @hide
*/
final class SystemTextClassifier implements TextClassifier {
@VisibleForTesting(visibility = Visibility.PACKAGE)
public final class SystemTextClassifier implements TextClassifier {
private static final String LOG_TAG = "SystemTextClassifier";
@@ -53,13 +57,13 @@ final class SystemTextClassifier implements TextClassifier {
@GuardedBy("mLoggerLock")
private Logger mLogger;
SystemTextClassifier(Context context, TextClassificationConstants settings)
public SystemTextClassifier(Context context, TextClassificationConstants settings)
throws ServiceManager.ServiceNotFoundException {
mManagerService = ITextClassifierService.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE));
mSettings = Preconditions.checkNotNull(settings);
mFallback = new TextClassifierImpl(context, settings);
mPackageName = context.getPackageName();
mPackageName = Preconditions.checkNotNull(context.getPackageName());
}
/**
@@ -124,8 +128,9 @@ final class SystemTextClassifier implements TextClassifier {
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
Utils.validate(text, false /* allowInMainThread */);
if (!mSettings.isSmartLinkifyEnabled()) {
return TextClassifier.NO_OP.generateLinks(text, options);
final boolean legacyFallback = options != null && options.isLegacyFallback();
if (!mSettings.isSmartLinkifyEnabled() && legacyFallback) {
return Utils.generateLegacyLinks(text, options);
}
try {

View File

@@ -344,6 +344,25 @@ public final class TextClassification implements Parcelable {
}
}
/**
* Triggers the specified intent.
*
* @throws IllegalArgumentException if context or intent is null
* @hide
*/
public static void fireIntent(@NonNull final Context context, @NonNull final Intent intent) {
switch (getIntentType(intent, context)) {
case IntentType.ACTIVITY:
context.startActivity(intent);
return;
case IntentType.SERVICE:
context.startService(intent);
return;
default:
return;
}
}
@IntentType
private static int getIntentType(@NonNull Intent intent, @NonNull Context context) {
Preconditions.checkArgument(context != null);

View File

@@ -26,6 +26,12 @@ import android.os.LocaleList;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.text.util.Linkify.LinkifyMask;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -37,6 +43,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Interface for providing text classification related features.
@@ -511,6 +518,65 @@ public interface TextClassifier {
Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()");
}
/**
* Generates links using legacy {@link Linkify}.
*/
public static TextLinks generateLegacyLinks(
@NonNull CharSequence text, @NonNull TextLinks.Options options) {
final String string = Preconditions.checkNotNull(text).toString();
final TextLinks.Builder links = new TextLinks.Builder(string);
final List<String> entities = Preconditions.checkNotNull(options).getEntityConfig()
.resolveEntityListModifications(Collections.emptyList());
if (entities.contains(TextClassifier.TYPE_URL)) {
addLinks(links, string, TextClassifier.TYPE_URL);
}
if (entities.contains(TextClassifier.TYPE_PHONE)) {
addLinks(links, string, TextClassifier.TYPE_PHONE);
}
if (entities.contains(TextClassifier.TYPE_EMAIL)) {
addLinks(links, string, TextClassifier.TYPE_EMAIL);
}
// NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
return links.build();
}
private static void addLinks(
TextLinks.Builder links, String string, @EntityType String entityType) {
final Spannable spannable = new SpannableString(string);
if (Linkify.addLinks(spannable, linkMask(entityType))) {
final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
for (URLSpan urlSpan : spans) {
links.addLink(
spannable.getSpanStart(urlSpan),
spannable.getSpanEnd(urlSpan),
entityScores(entityType),
urlSpan);
}
}
}
@LinkifyMask
private static int linkMask(@EntityType String entityType) {
switch (entityType) {
case TextClassifier.TYPE_URL:
return Linkify.WEB_URLS;
case TextClassifier.TYPE_PHONE:
return Linkify.PHONE_NUMBERS;
case TextClassifier.TYPE_EMAIL:
return Linkify.EMAIL_ADDRESSES;
default:
// NOTE: Do not support MAP_ADDRESSES. Legacy version does not work well.
return 0;
}
}
private static Map<String, Float> entityScores(@EntityType String entityType) {
final Map<String, Float> scores = new ArrayMap<>();
scores.put(entityType, 1f);
return scores;
}
private static void checkMainThread(boolean allowInMainThread) {
if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) {
Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");

View File

@@ -209,13 +209,15 @@ public final class TextClassifierImpl implements TextClassifier {
public TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
Utils.validate(text, getMaxGenerateLinksTextLength(), false /* allowInMainThread */);
final boolean legacyFallback = options != null && options.isLegacyFallback();
if (!mSettings.isSmartLinkifyEnabled() && legacyFallback) {
return Utils.generateLegacyLinks(text, options);
}
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
if (!mSettings.isSmartLinkifyEnabled()) {
return builder.build();
}
try {
final long startTimeMs = System.currentTimeMillis();
final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;

View File

@@ -20,17 +20,21 @@ import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Spannable;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.text.util.Linkify.LinkifyMask;
import android.view.View;
import android.view.textclassifier.TextClassifier.EntityType;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
@@ -197,6 +201,7 @@ public final class TextLinks implements Parcelable {
private final EntityConfidence mEntityScores;
private final int mStart;
private final int mEnd;
@Nullable final URLSpan mUrlSpan;
/**
* Create a new TextLink.
@@ -204,16 +209,19 @@ public final class TextLinks implements Parcelable {
* @param start The start index of the identified subsequence
* @param end The end index of the identified subsequence
* @param entityScores A mapping of entity type to confidence score
* @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled
*
* @throws IllegalArgumentException if entityScores is null or empty
*/
TextLink(int start, int end, Map<String, Float> entityScores) {
TextLink(int start, int end, Map<String, Float> entityScores,
@Nullable URLSpan urlSpan) {
Preconditions.checkNotNull(entityScores);
Preconditions.checkArgument(!entityScores.isEmpty());
Preconditions.checkArgument(start <= end);
mStart = start;
mEnd = end;
mEntityScores = new EntityConfidence(entityScores);
mUrlSpan = urlSpan;
}
/**
@@ -291,6 +299,7 @@ public final class TextLinks implements Parcelable {
mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
mStart = in.readInt();
mEnd = in.readInt();
mUrlSpan = null;
}
}
@@ -301,6 +310,7 @@ public final class TextLinks implements Parcelable {
private LocaleList mDefaultLocales;
private TextClassifier.EntityConfig mEntityConfig;
private boolean mLegacyFallback;
private @ApplyStrategy int mApplyStrategy;
private Function<TextLink, TextLinkSpan> mSpanFactory;
@@ -353,6 +363,17 @@ public final class TextLinks implements Parcelable {
return this;
}
/**
* Sets whether the TextClassifier can fallback to legacy links if smart linkify is
* disabled.
* <strong>Note: </strong>This is not parcelled.
* @hide
*/
public Options setLegacyFallback(boolean legacyFallback) {
mLegacyFallback = legacyFallback;
return this;
}
/**
* Sets a strategy for resolving conflicts when applying generated links to text that
* already have links.
@@ -405,6 +426,16 @@ public final class TextLinks implements Parcelable {
return mEntityConfig;
}
/**
* Returns whether the TextClassifier can fallback to legacy links if smart linkify is
* disabled.
* <strong>Note: </strong>This is not parcelled.
* @hide
*/
public boolean isLegacyFallback() {
return mLegacyFallback;
}
/**
* @return the strategy for resolving conflictswhen applying generated links to text that
* already have links
@@ -497,7 +528,7 @@ public final class TextLinks implements Parcelable {
private final TextLink mTextLink;
public TextLinkSpan(@Nullable TextLink textLink) {
public TextLinkSpan(@NonNull TextLink textLink) {
mTextLink = textLink;
}
@@ -505,13 +536,38 @@ public final class TextLinks implements Parcelable {
public void onClick(View widget) {
if (widget instanceof TextView) {
final TextView textView = (TextView) widget;
textView.requestActionMode(this);
final Context context = textView.getContext();
if (TextClassificationManager.getSettings(context).isSmartLinkifyEnabled()) {
if (textView.requestFocus()) {
textView.requestActionMode(this);
} else {
// If textView can not take focus, then simply handle the click as it will
// be difficult to get rid of the floating action mode.
textView.handleClick(this);
}
} else {
if (mTextLink.mUrlSpan != null) {
mTextLink.mUrlSpan.onClick(textView);
} else {
textView.handleClick(this);
}
}
}
}
public final TextLink getTextLink() {
return mTextLink;
}
/** @hide */
@VisibleForTesting(visibility = Visibility.PRIVATE)
@Nullable
public final String getUrl() {
if (mTextLink.mUrlSpan != null) {
return mTextLink.mUrlSpan.getURL();
}
return null;
}
}
/**
@@ -534,12 +590,24 @@ public final class TextLinks implements Parcelable {
/**
* Adds a TextLink.
*
* @return this instance
* @param start The start index of the identified subsequence
* @param end The end index of the identified subsequence
* @param entityScores A mapping of entity type to confidence score
*
* @throws IllegalArgumentException if entityScores is null or empty.
*/
public Builder addLink(int start, int end, Map<String, Float> entityScores) {
mLinks.add(new TextLink(start, end, entityScores));
mLinks.add(new TextLink(start, end, entityScores, null));
return this;
}
/**
* @see #addLink(int, int, Map)
* @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled.
*/
Builder addLink(int start, int end, Map<String, Float> entityScores,
@Nullable URLSpan urlSpan) {
mLinks.add(new TextLink(start, end, entityScores, urlSpan));
return this;
}

View File

@@ -246,7 +246,7 @@ public final class SelectionActionModeHelper {
mTextView.invalidate();
}
mTextClassification = result.mClassification;
} else if (actionMode == Editor.TextActionMode.TEXT_LINK) {
} else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) {
mTextClassification = result.mClassification;
} else {
mTextClassification = null;

View File

@@ -162,6 +162,7 @@ import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
@@ -187,6 +188,10 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* A user interface element that displays text to the user.
@@ -11515,16 +11520,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
* Starts an ActionMode for the specified TextLink.
* Starts an ActionMode for the specified TextLinkSpan.
*
* @return Whether or not we're attempting to start the action mode.
* @hide
*/
public boolean requestActionMode(@NonNull TextLinks.TextLinkSpan clickedSpan) {
Preconditions.checkNotNull(clickedSpan);
final TextLinks.TextLink link = clickedSpan.getTextLink();
Preconditions.checkNotNull(link);
createEditorIfNeeded();
if (!(mText instanceof Spanned)) {
return false;
@@ -11533,14 +11535,54 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final int start = ((Spanned) mText).getSpanStart(clickedSpan);
final int end = ((Spanned) mText).getSpanEnd(clickedSpan);
if (start < 0 || end < 1) {
if (start < 0 || end > mText.length() || start >= end) {
return false;
}
createEditorIfNeeded();
mEditor.startLinkActionModeAsync(start, end);
return true;
}
/**
* Handles a click on the specified TextLinkSpan.
*
* @return Whether or not the click is being handled.
* @hide
*/
public boolean handleClick(@NonNull TextLinks.TextLinkSpan clickedSpan) {
Preconditions.checkNotNull(clickedSpan);
if (mText instanceof Spanned) {
final Spanned spanned = (Spanned) mText;
final int start = spanned.getSpanStart(clickedSpan);
final int end = spanned.getSpanEnd(clickedSpan);
if (start >= 0 && end <= mText.length() && start < end) {
final TextClassification.Options options = new TextClassification.Options()
.setDefaultLocales(getTextLocales());
final Supplier<TextClassification> supplier = () ->
getTextClassifier().classifyText(mText, start, end, options);
final Consumer<TextClassification> consumer = classification -> {
if (classification != null) {
final Intent intent = classification.getIntent();
if (intent != null) {
TextClassification.fireIntent(mContext, intent);
} else {
Log.d(LOG_TAG, "No link action to perform");
}
} else {
// classification == null
Log.d(LOG_TAG, "Timeout while classifying text");
}
};
CompletableFuture.supplyAsync(supplier)
.completeOnTimeout(null, 1, TimeUnit.SECONDS)
.thenAccept(consumer);
return true;
}
}
return false;
}
/**
* @hide
*/

View File

@@ -31,6 +31,11 @@ import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.Patterns;
import android.view.textclassifier.SystemTextClassifier;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextClassifierImpl;
import android.view.textclassifier.TextLinks.TextLinkSpan;
import android.widget.TextView;
import org.junit.After;
@@ -38,7 +43,11 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* LinkifyTest tests {@link Linkify}.
@@ -149,4 +158,55 @@ public class LinkifyTest {
assertEquals("android.com should be linkified", 1, spans.length);
assertEquals("https://android.com", spans[0].getURL());
}
@Test
public void testAddLinksAsync_useLegacyIfSmartDisabled_localTextClassifier()
throws Exception {
final TextClassificationConstants settings =
TextClassificationConstants.loadFromString("smart_linkify_enabled=false");
final TextClassifier classifier = new TextClassifierImpl(mContext, settings);
testAddLinksAsync_useLegacyIfSmartDisabled(classifier);
}
@Test
public void testAddLinksAsync_useLegacyIfSmartDisabled_systemTextClassifier()
throws Exception {
final TextClassificationConstants settings =
TextClassificationConstants.loadFromString("smart_linkify_enabled=false");
final TextClassifier classifier = new SystemTextClassifier(mContext, settings);
testAddLinksAsync_useLegacyIfSmartDisabled(classifier);
}
private void testAddLinksAsync_useLegacyIfSmartDisabled(TextClassifier classifier)
throws Exception {
final int linkMask = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS;
final String string = "example@android.com\n"
+ "(415) 555-1212\n"
+ "http://android.com\n"
+ "100 Android Rd California";
final Spannable expected = new SpannableString(string);
final Spannable actual = new SpannableString(string);
Linkify.addLinks(expected, linkMask); // legacy.
Linkify.addLinksAsync(actual, classifier, linkMask).get();
final URLSpan[] expectedSpans = expected.getSpans(0, expected.length(), URLSpan.class);
final TextLinkSpan[] actualSpans = actual.getSpans(0, actual.length(), TextLinkSpan.class);
assertEquals(expectedSpans.length, actualSpans.length);
final Set<List> expectedLinkSpec = new HashSet<>();
final Set<List> actualLinkSpec = new HashSet<>();
for (int i = 0; i < expectedSpans.length; i++) {
final URLSpan expectedSpan = expectedSpans[i];
final TextLinkSpan actualSpan = actualSpans[i];
expectedLinkSpec.add(Arrays.asList(
expected.getSpanStart(expectedSpan),
expected.getSpanEnd(expectedSpan),
expectedSpan.getURL()));
actualLinkSpec.add(Arrays.asList(
actual.getSpanStart(actualSpan),
actual.getSpanEnd(actualSpan),
actualSpan.getUrl()));
}
assertEquals(expectedLinkSpec, actualLinkSpec);
}
}