From 1fc998b0fda051188665e599c891da4a5750581d Mon Sep 17 00:00:00 2001 From: Richard Ledley Date: Fri, 16 Feb 2018 15:45:06 +0000 Subject: [PATCH] Replace Entity Presets with hints and an explicit factory. This remove APIs to inspect the list of entities a TextClassifier will choose. We decided the developer should have no need to see these, and by not exposing it we allow more flexibility on the TextClassifier. Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest CtsViewTestCases:android.view.textclassifier.cts.TextClassificationManagerTest FrameworksCoreTests:android.view.textclassifier.TextLinksTest Bug: 67629726 Change-Id: I70978e692bb8a1edee1567a10c31d5ded44baa49 --- api/current.txt | 15 +-- .../view/textclassifier/TextClassifier.java | 124 +++++++++++------- .../textclassifier/TextClassifierImpl.java | 24 +--- .../view/textclassifier/TextLinks.java | 14 +- .../TextClassificationManagerTest.java | 41 +++--- .../view/textclassifier/TextLinksTest.java | 14 +- 6 files changed, 119 insertions(+), 113 deletions(-) diff --git a/api/current.txt b/api/current.txt index 14fb7dd282444..eb903cecfa580 100644 --- a/api/current.txt +++ b/api/current.txt @@ -50311,15 +50311,13 @@ package android.view.textclassifier { method public default android.view.textclassifier.TextClassification classifyText(java.lang.CharSequence, int, int, android.os.LocaleList); method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options); 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); - field public static final int ENTITY_PRESET_ALL = 0; // 0x0 - field public static final int ENTITY_PRESET_BASE = 2; // 0x2 - field public static final int ENTITY_PRESET_NONE = 1; // 0x1 + field public static final java.lang.String HINT_TEXT_IS_EDITABLE = "android.text_is_editable"; + field public static final java.lang.String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable"; field public static final android.view.textclassifier.TextClassifier NO_OP; field public static final java.lang.String TYPE_ADDRESS = "address"; field public static final java.lang.String TYPE_DATE = "date"; @@ -50333,11 +50331,12 @@ package android.view.textclassifier { } public static final class TextClassifier.EntityConfig implements android.os.Parcelable { - ctor public TextClassifier.EntityConfig(int); + method public static android.view.textclassifier.TextClassifier.EntityConfig create(java.util.Collection); + method public static android.view.textclassifier.TextClassifier.EntityConfig create(java.util.Collection, java.util.Collection, java.util.Collection); + method public static android.view.textclassifier.TextClassifier.EntityConfig createWithEntityList(java.util.Collection); method public int describeContents(); - method public android.view.textclassifier.TextClassifier.EntityConfig excludeEntities(java.lang.String...); - method public java.util.List getEntities(android.view.textclassifier.TextClassifier); - method public android.view.textclassifier.TextClassifier.EntityConfig includeEntities(java.lang.String...); + method public java.util.Collection getHints(); + method public java.util.List resolveEntityListModifications(java.util.Collection); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 2a62f23f19c78..d52a30bcc018d 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -16,7 +16,6 @@ package android.view.textclassifier; -import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -86,19 +85,15 @@ public interface TextClassifier { }) @interface EntityType {} - /** Designates that the TextClassifier should identify all entity types it can. **/ - int ENTITY_PRESET_ALL = 0; - /** Designates that the TextClassifier should identify no entities. **/ - int ENTITY_PRESET_NONE = 1; - /** Designates that the TextClassifier should identify a base set of entities determined by the - * TextClassifier. **/ - int ENTITY_PRESET_BASE = 2; + /** Designates that the text in question is editable. **/ + String HINT_TEXT_IS_EDITABLE = "android.text_is_editable"; + /** Designates that the text in question is not editable. **/ + String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable"; /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "ENTITY_CONFIG_" }, - value = {ENTITY_PRESET_ALL, ENTITY_PRESET_NONE, ENTITY_PRESET_BASE}) - @interface EntityPreset {} + @StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE}) + @interface Hints {} /** * No-op TextClassifier. @@ -322,17 +317,6 @@ public interface TextClassifier { return Integer.MAX_VALUE; } - /** - * Returns a {@link Collection} of the entity types in the specified preset. - * - * @see #ENTITY_PRESET_ALL - * @see #ENTITY_PRESET_NONE - * @see #ENTITY_PRESET_BASE - */ - default Collection getEntitiesForPreset(@EntityPreset int entityPreset) { - return Collections.EMPTY_LIST; - } - /** * Returns a helper for logging TextClassifier related events. * @@ -358,54 +342,92 @@ public interface TextClassifier { * Configs are initially based on a predefined preset, and can be modified from there. */ final class EntityConfig implements Parcelable { - private final @TextClassifier.EntityPreset int mEntityPreset; + private final Collection mHints; private final Collection mExcludedEntityTypes; private final Collection mIncludedEntityTypes; + private final boolean mUseHints; - public EntityConfig(@TextClassifier.EntityPreset int mEntityPreset) { - this.mEntityPreset = mEntityPreset; - mExcludedEntityTypes = new ArraySet<>(); - mIncludedEntityTypes = new ArraySet<>(); + private EntityConfig(boolean useHints, Collection hints, + Collection includedEntityTypes, Collection excludedEntityTypes) { + mHints = hints == null + ? Collections.EMPTY_LIST + : Collections.unmodifiableCollection(new ArraySet<>(hints)); + mExcludedEntityTypes = excludedEntityTypes == null + ? Collections.EMPTY_LIST : new ArraySet<>(excludedEntityTypes); + mIncludedEntityTypes = includedEntityTypes == null + ? Collections.EMPTY_LIST : new ArraySet<>(includedEntityTypes); + mUseHints = useHints; } /** - * Specifies an entity to include in addition to any specified by the enity preset. + * Creates an EntityConfig. + * + * @param hints Hints for the TextClassifier to determine what types of entities to find. + */ + public static EntityConfig create(@Nullable Collection hints) { + return new EntityConfig(/* useHints */ true, hints, + /* includedEntityTypes */null, /* excludedEntityTypes */ null); + } + + /** + * Creates an EntityConfig. + * + * @param hints Hints for the TextClassifier to determine what types of entities to find + * @param includedEntityTypes Entity types, e.g. {@link #TYPE_EMAIL}, to explicitly include + * @param excludedEntityTypes Entity types, e.g. {@link #TYPE_PHONE}, to explicitly exclude + * * * Note that if an entity has been excluded, the exclusion will take precedence. */ - public EntityConfig includeEntities(String... entities) { - for (String entity : entities) { - mIncludedEntityTypes.add(entity); - } - return this; + public static EntityConfig create(@Nullable Collection hints, + @Nullable Collection includedEntityTypes, + @Nullable Collection excludedEntityTypes) { + return new EntityConfig(/* useHints */ true, hints, + includedEntityTypes, excludedEntityTypes); } /** - * Specifies an entity to be excluded. + * Creates an EntityConfig with an explicit entity list. + * + * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find. + * */ - public EntityConfig excludeEntities(String... entities) { - for (String entity : entities) { - mExcludedEntityTypes.add(entity); - } - return this; + public static EntityConfig createWithEntityList(@Nullable Collection entityTypes) { + return new EntityConfig(/* useHints */ false, /* hints */ null, + /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null); } /** - * Returns an unmodifiable list of the final set of entities to find. + * Returns a list of the final set of entities to find. + * + * @param entities Entities we think should be found before factoring in includes/excludes + * + * This method is intended for use by TextClassifier implementations. */ - public List getEntities(TextClassifier textClassifier) { - ArrayList entities = new ArrayList<>(); - for (String entity : textClassifier.getEntitiesForPreset(mEntityPreset)) { - if (!mExcludedEntityTypes.contains(entity)) { - entities.add(entity); + public List resolveEntityListModifications(@NonNull Collection entities) { + final ArrayList finalList = new ArrayList<>(); + if (mUseHints) { + for (String entity : entities) { + if (!mExcludedEntityTypes.contains(entity)) { + finalList.add(entity); + } } } for (String entity : mIncludedEntityTypes) { - if (!mExcludedEntityTypes.contains(entity) && !entities.contains(entity)) { - entities.add(entity); + if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) { + finalList.add(entity); } } - return Collections.unmodifiableList(entities); + return finalList; + } + + /** + * Retrieves the list of hints. + * + * @return An unmodifiable collection of the hints. + */ + public Collection getHints() { + return mHints; } @Override @@ -415,9 +437,10 @@ public interface TextClassifier { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mEntityPreset); + dest.writeStringList(new ArrayList<>(mHints)); dest.writeStringList(new ArrayList<>(mExcludedEntityTypes)); dest.writeStringList(new ArrayList<>(mIncludedEntityTypes)); + dest.writeInt(mUseHints ? 1 : 0); } public static final Parcelable.Creator CREATOR = @@ -434,9 +457,10 @@ public interface TextClassifier { }; private EntityConfig(Parcel in) { - mEntityPreset = in.readInt(); + mHints = new ArraySet<>(in.createStringArrayList()); mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList()); mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList()); + mUseHints = in.readInt() == 1; } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 795caffd04d54..c19e844234eed 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -88,12 +88,6 @@ public final class TextClassifierImpl implements TextClassifier { TextClassifier.TYPE_DATE, TextClassifier.TYPE_DATE_TIME, TextClassifier.TYPE_FLIGHT_NUMBER)); - private static final List ENTITY_TYPES_BASE = - Collections.unmodifiableList(Arrays.asList( - TextClassifier.TYPE_ADDRESS, - TextClassifier.TYPE_EMAIL, - TextClassifier.TYPE_PHONE, - TextClassifier.TYPE_URL)); private final Context mContext; private final TextClassifier mFallback; @@ -222,7 +216,9 @@ public final class TextClassifierImpl implements TextClassifier { final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; final Collection entitiesToIdentify = options != null && options.getEntityConfig() != null - ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL; + ? options.getEntityConfig().resolveEntityListModifications( + getEntitiesForHints(options.getEntityConfig().getHints())) + : ENTITY_TYPES_ALL; final SmartSelection smartSelection = getSmartSelection(defaultLocales); final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString); for (SmartSelection.AnnotatedSpan span : annotations) { @@ -250,18 +246,8 @@ public final class TextClassifierImpl implements TextClassifier { return getSettings().getGenerateLinksMaxTextLength(); } - @Override - public Collection getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) { - switch (entityPreset) { - case TextClassifier.ENTITY_PRESET_NONE: - return Collections.emptyList(); - case TextClassifier.ENTITY_PRESET_BASE: - return ENTITY_TYPES_BASE; - case TextClassifier.ENTITY_PRESET_ALL: - // fall through - default: - return ENTITY_TYPES_ALL; - } + private Collection getEntitiesForHints(Collection hints) { + return ENTITY_TYPES_ALL; } @Override diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java index 3d252f2936631..dc24d0c4d681d 100644 --- a/core/java/android/view/textclassifier/TextLinks.java +++ b/core/java/android/view/textclassifier/TextLinks.java @@ -309,23 +309,23 @@ public final class TextLinks implements Parcelable { * Returns a new options object based on the specified link mask. */ public static Options fromLinkMask(@LinkifyMask int mask) { - final TextClassifier.EntityConfig entityConfig = - new TextClassifier.EntityConfig(TextClassifier.ENTITY_PRESET_NONE); + final List entitiesToFind = new ArrayList<>(); if ((mask & Linkify.WEB_URLS) != 0) { - entityConfig.includeEntities(TextClassifier.TYPE_URL); + entitiesToFind.add(TextClassifier.TYPE_URL); } if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { - entityConfig.includeEntities(TextClassifier.TYPE_EMAIL); + entitiesToFind.add(TextClassifier.TYPE_EMAIL); } if ((mask & Linkify.PHONE_NUMBERS) != 0) { - entityConfig.includeEntities(TextClassifier.TYPE_PHONE); + entitiesToFind.add(TextClassifier.TYPE_PHONE); } if ((mask & Linkify.MAP_ADDRESSES) != 0) { - entityConfig.includeEntities(TextClassifier.TYPE_ADDRESS); + entitiesToFind.add(TextClassifier.TYPE_ADDRESS); } - return new Options().setEntityConfig(entityConfig); + return new Options().setEntityConfig( + TextClassifier.EntityConfig.createWithEntityList(entitiesToFind)); } public Options() {} diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java index 430b30fcbffee..624dce15698ec 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java @@ -35,6 +35,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; +import java.util.Collections; @SmallTest @RunWith(AndroidJUnit4.class) @@ -178,41 +179,33 @@ public class TextClassificationManagerTest { if (isTextClassifierDisabled()) return; String text = "The number is +12122537077. See you tonight!"; assertThat(mClassifier.generateLinks(text, mLinksOptions.setEntityConfig( - new TextClassifier.EntityConfig(TextClassifier.ENTITY_PRESET_ALL) - .excludeEntities(TextClassifier.TYPE_PHONE))), + TextClassifier.EntityConfig.create(Collections.EMPTY_LIST, + Collections.EMPTY_LIST, Arrays.asList(TextClassifier.TYPE_PHONE)))), not(isTextLinksContaining(text, "+12122537077", TextClassifier.TYPE_PHONE))); } @Test - public void testGenerateLinks_none_config() { + public void testGenerateLinks_explicit_address() { if (isTextClassifierDisabled()) return; + String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!"; + assertThat(mClassifier.generateLinks(text, mLinksOptions.setEntityConfig( + TextClassifier.EntityConfig.createWithEntityList( + Arrays.asList(TextClassifier.TYPE_ADDRESS)))), + isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA", + TextClassifier.TYPE_ADDRESS)); + } + @Test + public void testGenerateLinks_exclude_override() { + 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))), + TextClassifier.EntityConfig.create(Collections.EMPTY_LIST, + Arrays.asList(TextClassifier.TYPE_PHONE), + Arrays.asList(TextClassifier.TYPE_PHONE)))), not(isTextLinksContaining(text, "+12122537077", TextClassifier.TYPE_PHONE))); } - @Test - public void testGenerateLinks_address() { - if (isTextClassifierDisabled()) return; - String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!"; - assertThat(mClassifier.generateLinks(text, null), - isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA", - TextClassifier.TYPE_ADDRESS)); - } - - @Test - public void testGenerateLinks_include() { - if (isTextClassifierDisabled()) return; - String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!"; - assertThat(mClassifier.generateLinks(text, mLinksOptions.setEntityConfig( - new TextClassifier.EntityConfig(TextClassifier.ENTITY_PRESET_NONE) - .includeEntities(TextClassifier.TYPE_ADDRESS))), - isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA", - TextClassifier.TYPE_ADDRESS)); - } - @Test public void testGenerateLinks_maxLength() { if (isTextClassifierDisabled()) return; diff --git a/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java index d6ac8454b86b4..42796755f1ab2 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextLinksTest.java @@ -31,6 +31,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -98,10 +99,10 @@ public class TextLinksTest { @Test public void testParcelOptions() { - TextClassifier.EntityConfig entityConfig = new TextClassifier.EntityConfig( - TextClassifier.ENTITY_PRESET_NONE); - entityConfig.includeEntities("a", "b", "c"); - entityConfig.excludeEntities("b"); + TextClassifier.EntityConfig entityConfig = TextClassifier.EntityConfig.create( + Arrays.asList(TextClassifier.HINT_TEXT_IS_EDITABLE), + Arrays.asList("a", "b", "c"), + Arrays.asList("b")); TextLinks.Options reference = new TextLinks.Options(); reference.setDefaultLocales(new LocaleList(Locale.US, Locale.GERMANY)); reference.setEntityConfig(entityConfig); @@ -113,6 +114,9 @@ public class TextLinksTest { TextLinks.Options result = TextLinks.Options.CREATOR.createFromParcel(parcel); assertEquals("en-US,de-DE", result.getDefaultLocales().toLanguageTags()); - assertEquals(Arrays.asList("a", "c"), result.getEntityConfig().getEntities(mClassifier)); + assertEquals(new String[]{TextClassifier.HINT_TEXT_IS_EDITABLE}, + result.getEntityConfig().getHints().toArray()); + assertEquals(Arrays.asList("a", "c"), + result.getEntityConfig().resolveEntityListModifications(Collections.emptyList())); } }