diff --git a/api/current.txt b/api/current.txt index ccce335f8306f..14fb7dd282444 100644 --- a/api/current.txt +++ b/api/current.txt @@ -50313,6 +50313,7 @@ package android.view.textclassifier { method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence); method public default java.util.Collection getEntitiesForPreset(int); method public default android.view.textclassifier.logging.Logger getLogger(android.view.textclassifier.logging.Logger.Config); + method public default int getMaxGenerateLinksTextLength(); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.os.LocaleList); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c53d005606c46..598eeefbd81ad 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10457,6 +10457,9 @@ public final class Settings { *
          * smart_selection_dark_launch              (boolean)
          * smart_selection_enabled_for_edit_text    (boolean)
+         * suggest_selection_max_range_length       (int)
+         * classify_text_max_range_length           (int)
+         * generate_links_max_text_length           (int)
          * 
* *

diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index d973d4ac076c0..3a22db2bfb22a 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -644,7 +644,13 @@ public class Linkify { @Nullable Runnable modifyTextView) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(classifier); - final Supplier supplier = () -> classifier.generateLinks(text, options); + + // The input text may exceed the maximum length the text classifier can handle. In such + // cases, we process the text up to the maximum length. + final CharSequence truncatedText = text.subSequence( + 0, Math.min(text.length(), classifier.getMaxGenerateLinksTextLength())); + + final Supplier supplier = () -> classifier.generateLinks(truncatedText, options); final Consumer consumer = links -> { if (links.getLinks().isEmpty()) { if (callback != null) { @@ -653,7 +659,8 @@ public class Linkify { return; } - final TextLinkSpan[] old = text.getSpans(0, text.length(), TextLinkSpan.class); + // Remove spans only for the part of the text we generated links for. + final TextLinkSpan[] old = text.getSpans(0, truncatedText.length(), TextLinkSpan.class); for (int i = old.length - 1; i >= 0; i--) { text.removeSpan(old[i]); } @@ -662,7 +669,8 @@ public class Linkify { ? null : options.getSpanFactory(); final @TextLinks.ApplyStrategy int applyStrategy = (options == null) ? TextLinks.APPLY_STRATEGY_IGNORE : options.getApplyStrategy(); - final @TextLinks.Status int result = links.apply(text, applyStrategy, spanFactory); + final @TextLinks.Status int result = links.apply(text, applyStrategy, spanFactory, + true /*allowPrefix*/); if (result == TextLinks.STATUS_LINKS_APPLIED) { if (modifyTextView != null) { modifyTextView.run(); diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java index af55dcd0ed725..cbc3828ba56ef 100644 --- a/core/java/android/view/textclassifier/SystemTextClassifier.java +++ b/core/java/android/view/textclassifier/SystemTextClassifier.java @@ -121,6 +121,15 @@ final class SystemTextClassifier implements TextClassifier { return mFallback.generateLinks(text, options); } + /** + * @inheritDoc + */ + @Override + public int getMaxGenerateLinksTextLength() { + // TODO: retrieve this from the bound service. + return mFallback.getMaxGenerateLinksTextLength(); + } + private static final class TextSelectionCallback extends ITextSelectionCallback.Stub { final ResponseReceiver mReceiver = new ResponseReceiver<>(); diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 9f75c4a80ca2c..2a62f23f19c78 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -276,9 +276,11 @@ public interface TextClassifier { * @param text the text to generate annotations for * @param options configuration for link generation * - * @throws IllegalArgumentException if text is null + * @throws IllegalArgumentException if text is null or the text is too long for the + * TextClassifier implementation. * * @see #generateLinks(CharSequence) + * @see #getMaxGenerateLinksTextLength() */ @WorkerThread default TextLinks generateLinks( @@ -299,15 +301,27 @@ public interface TextClassifier { * * @param text the text to generate annotations for * - * @throws IllegalArgumentException if text is null + * @throws IllegalArgumentException if text is null or the text is too long for the + * TextClassifier implementation. * * @see #generateLinks(CharSequence, TextLinks.Options) + * @see #getMaxGenerateLinksTextLength() */ @WorkerThread default TextLinks generateLinks(@NonNull CharSequence text) { return generateLinks(text, null); } + /** + * Returns the maximal length of text that can be processed by generateLinks. + * + * @see #generateLinks(CharSequence) + * @see #generateLinks(CharSequence, TextLinks.Options) + */ + default int getMaxGenerateLinksTextLength() { + return Integer.MAX_VALUE; + } + /** * Returns a {@link Collection} of the entity types in the specified preset. * @@ -461,6 +475,15 @@ public interface TextClassifier { checkMainThread(allowInMainThread); } + /** + * @throws IllegalArgumentException if text is null; the text is too long or options is null + */ + public static void validate(@NonNull CharSequence text, int maxLength, + boolean allowInMainThread) { + validate(text, allowInMainThread); + Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()"); + } + private static void checkMainThread(boolean allowInMainThread) { if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) { Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread"); diff --git a/core/java/android/view/textclassifier/TextClassifierConstants.java b/core/java/android/view/textclassifier/TextClassifierConstants.java index 00695b797cb3d..efa69488521f5 100644 --- a/core/java/android/view/textclassifier/TextClassifierConstants.java +++ b/core/java/android/view/textclassifier/TextClassifierConstants.java @@ -47,10 +47,19 @@ public final class TextClassifierConstants { "smart_selection_enabled_for_edit_text"; private static final String SMART_LINKIFY_ENABLED = "smart_linkify_enabled"; + private static final String SUGGEST_SELECTION_MAX_RANGE_LENGTH = + "suggest_selection_max_range_length"; + private static final String CLASSIFY_TEXT_MAX_RANGE_LENGTH = + "classify_text_max_range_length"; + private static final String GENERATE_LINKS_MAX_TEXT_LENGTH = + "generate_links_max_text_length"; private static final boolean SMART_SELECTION_DARK_LAUNCH_DEFAULT = false; private static final boolean SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT = true; private static final boolean SMART_LINKIFY_ENABLED_DEFAULT = true; + private static final int SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000; + private static final int CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT = 10 * 1000; + private static final int GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT = 100 * 1000; /** Default settings. */ static final TextClassifierConstants DEFAULT = new TextClassifierConstants(); @@ -58,11 +67,17 @@ public final class TextClassifierConstants { private final boolean mDarkLaunch; private final boolean mSuggestSelectionEnabledForEditableText; private final boolean mSmartLinkifyEnabled; + private final int mSuggestSelectionMaxRangeLength; + private final int mClassifyTextMaxRangeLength; + private final int mGenerateLinksMaxTextLength; private TextClassifierConstants() { mDarkLaunch = SMART_SELECTION_DARK_LAUNCH_DEFAULT; mSuggestSelectionEnabledForEditableText = SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT; mSmartLinkifyEnabled = SMART_LINKIFY_ENABLED_DEFAULT; + mSuggestSelectionMaxRangeLength = SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT; + mClassifyTextMaxRangeLength = CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT; + mGenerateLinksMaxTextLength = GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT; } private TextClassifierConstants(@Nullable String settings) { @@ -82,6 +97,15 @@ public final class TextClassifierConstants { mSmartLinkifyEnabled = parser.getBoolean( SMART_LINKIFY_ENABLED, SMART_LINKIFY_ENABLED_DEFAULT); + mSuggestSelectionMaxRangeLength = parser.getInt( + SUGGEST_SELECTION_MAX_RANGE_LENGTH, + SUGGEST_SELECTION_MAX_RANGE_LENGTH_DEFAULT); + mClassifyTextMaxRangeLength = parser.getInt( + CLASSIFY_TEXT_MAX_RANGE_LENGTH, + CLASSIFY_TEXT_MAX_RANGE_LENGTH_DEFAULT); + mGenerateLinksMaxTextLength = parser.getInt( + GENERATE_LINKS_MAX_TEXT_LENGTH, + GENERATE_LINKS_MAX_TEXT_LENGTH_DEFAULT); } static TextClassifierConstants loadFromString(String settings) { @@ -99,4 +123,16 @@ public final class TextClassifierConstants { public boolean isSmartLinkifyEnabled() { return mSmartLinkifyEnabled; } + + public int getSuggestSelectionMaxRangeLength() { + return mSuggestSelectionMaxRangeLength; + } + + public int getClassifyTextMaxRangeLength() { + return mClassifyTextMaxRangeLength; + } + + public int getGenerateLinksMaxTextLength() { + return mGenerateLinksMaxTextLength; + } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index fc034937312c8..795caffd04d54 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -128,7 +128,9 @@ public final class TextClassifierImpl implements TextClassifier { @Nullable TextSelection.Options options) { Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */); try { - if (text.length() > 0) { + final int rangeLength = selectionEndIndex - selectionStartIndex; + if (text.length() > 0 + && rangeLength <= getSettings().getSuggestSelectionMaxRangeLength()) { final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); final boolean darkLaunchAllowed = options != null && options.isDarkLaunchAllowed(); final SmartSelection smartSelection = getSmartSelection(locales); @@ -183,7 +185,8 @@ public final class TextClassifierImpl implements TextClassifier { @Nullable TextClassification.Options options) { Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */); try { - if (text.length() > 0) { + final int rangeLength = endIndex - startIndex; + if (text.length() > 0 && rangeLength <= getSettings().getClassifyTextMaxRangeLength()) { final String string = text.toString(); final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); final Calendar refTime = (options == null) ? null : options.getReferenceTime(); @@ -207,7 +210,7 @@ public final class TextClassifierImpl implements TextClassifier { @Override public TextLinks generateLinks( @NonNull CharSequence text, @Nullable TextLinks.Options options) { - Utils.validate(text, false /* allowInMainThread */); + Utils.validate(text, getMaxGenerateLinksTextLength(), false /* allowInMainThread */); final String textString = text.toString(); final TextLinks.Builder builder = new TextLinks.Builder(textString); @@ -241,6 +244,12 @@ public final class TextClassifierImpl implements TextClassifier { return mFallback.generateLinks(text, options); } + /** @inheritDoc */ + @Override + public int getMaxGenerateLinksTextLength() { + return getSettings().getGenerateLinksMaxTextLength(); + } + @Override public Collection getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) { switch (entityPreset) { diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java index d866d1305172a..3d252f2936631 100644 --- a/core/java/android/view/textclassifier/TextLinks.java +++ b/core/java/android/view/textclassifier/TextLinks.java @@ -108,6 +108,7 @@ public final class TextLinks implements Parcelable { * @param text the text to apply the links to. Must match the original text * @param applyStrategy strategy for resolving link conflicts * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null + * @param allowPrefix whether to allow applying links only to a prefix of the text. * * @return a status code indicating whether or not the links were successfully applied * @@ -117,10 +118,12 @@ public final class TextLinks implements Parcelable { public int apply( @NonNull Spannable text, @ApplyStrategy int applyStrategy, - @Nullable Function spanFactory) { + @Nullable Function spanFactory, + boolean allowPrefix) { Preconditions.checkNotNull(text); checkValidApplyStrategy(applyStrategy); - if (!mFullText.equals(text.toString())) { + final String textString = text.toString(); + if (!mFullText.equals(textString) && !(allowPrefix && textString.startsWith(mFullText))) { return STATUS_DIFFERENT_TEXT; } if (mLinks.isEmpty()) { diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java index a8de374cf30b3..430b30fcbffee 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java @@ -19,6 +19,7 @@ package android.view.textclassifier; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.os.LocaleList; @@ -33,6 +34,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Arrays; + @SmallTest @RunWith(AndroidJUnit4.class) public class TextClassificationManagerTest { @@ -183,6 +186,7 @@ public class TextClassificationManagerTest { @Test public void testGenerateLinks_none_config() { if (isTextClassifierDisabled()) return; + String text = "The number is +12122537077. See you tonight!"; assertThat(mClassifier.generateLinks(text, mLinksOptions.setEntityConfig( new TextClassifier.EntityConfig(TextClassifier.ENTITY_PRESET_NONE))), @@ -209,6 +213,25 @@ public class TextClassificationManagerTest { TextClassifier.TYPE_ADDRESS)); } + @Test + public void testGenerateLinks_maxLength() { + if (isTextClassifierDisabled()) return; + char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength()]; + Arrays.fill(manySpaces, ' '); + TextLinks links = mClassifier.generateLinks(new String(manySpaces), null); + assertTrue(links.getLinks().isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGenerateLinks_tooLong() { + if (isTextClassifierDisabled()) { + throw new IllegalArgumentException("pass if disabled"); + } + char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength() + 1]; + Arrays.fill(manySpaces, ' '); + mClassifier.generateLinks(new String(manySpaces), null); + } + @Test public void testSetTextClassifier() { TextClassifier classifier = mock(TextClassifier.class); diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java index 7f4f9f7b928a6..79433ac67aa07 100644 --- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java +++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java @@ -382,7 +382,7 @@ public class TextViewActivityTest { TextClassifier textClassifier = textClassificationManager.getTextClassifier(); Spannable content = new SpannableString("Call me at +19148277737"); TextLinks links = textClassifier.generateLinks(content); - links.apply(content, TextLinks.APPLY_STRATEGY_REPLACE, null); + links.apply(content, TextLinks.APPLY_STRATEGY_REPLACE, null, false /* allowPrefix */); mActivityRule.runOnUiThread(() -> { textView.setText(content);