diff options
| author | 2018-09-14 16:00:03 +0100 | |
|---|---|---|
| committer | 2018-10-18 03:04:28 +0100 | |
| commit | 7cefd4f20c2d25fad08b1d601c0502c119342be9 (patch) | |
| tree | 599321435429f577077e0efefbcd7c951fe6d4fb | |
| parent | 5aaa0742bff42fa2f2486e3cb38699930847578a (diff) | |
Introduce TextClassifier.detectLanguage() API.
Implementation will follow in TextClassifierImpl (for AOSP)
and SystemTextClassifier (for OEM version).
Bug: 116020587
Test: atest android.view.textclassifier.TextLanguageTest
Change-Id: Iaf7e8df2a14fe22335805bee41f138468430aea6
| -rwxr-xr-x | api/current.txt | 34 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifier.java | 98 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextLanguage.java | 307 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/view/textclassifier/TextLanguageTest.java | 85 |
4 files changed, 454 insertions, 70 deletions
diff --git a/api/current.txt b/api/current.txt index 0b6af293f667..5faf40681f56 100755 --- a/api/current.txt +++ b/api/current.txt @@ -51259,6 +51259,7 @@ package android.view.textclassifier { method public default android.view.textclassifier.TextClassification classifyText(android.view.textclassifier.TextClassification.Request); method public default android.view.textclassifier.TextClassification classifyText(java.lang.CharSequence, int, int, android.os.LocaleList); method public default void destroy(); + method public default android.view.textclassifier.TextLanguage detectLanguage(android.view.textclassifier.TextLanguage.Request); method public default android.view.textclassifier.TextLinks generateLinks(android.view.textclassifier.TextLinks.Request); method public default int getMaxGenerateLinksTextLength(); method public default boolean isDestroyed(); @@ -51299,6 +51300,39 @@ package android.view.textclassifier { field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassifier.EntityConfig> CREATOR; } + public final class TextLanguage implements android.os.Parcelable { + method public int describeContents(); + method public float getConfidenceScore(android.icu.util.ULocale); + method public android.os.Bundle getExtras(); + method public java.lang.String getId(); + method public android.icu.util.ULocale getLocale(int); + method public int getLocaleHypothesisCount(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLanguage> CREATOR; + } + + public static final class TextLanguage.Builder { + ctor public TextLanguage.Builder(); + method public android.view.textclassifier.TextLanguage build(); + method public android.view.textclassifier.TextLanguage.Builder putLocale(android.icu.util.ULocale, float); + method public android.view.textclassifier.TextLanguage.Builder setExtras(android.os.Bundle); + method public android.view.textclassifier.TextLanguage.Builder setId(java.lang.String); + } + + public static final class TextLanguage.Request implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.CharSequence getText(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLanguage.Request> CREATOR; + } + + public static final class TextLanguage.Request.Builder { + ctor public TextLanguage.Request.Builder(java.lang.CharSequence); + method public android.view.textclassifier.TextLanguage.Request build(); + method public android.view.textclassifier.TextLanguage.Request.Builder setExtras(android.os.Bundle); + } + public final class TextLinks implements android.os.Parcelable { method public int apply(android.text.Spannable, int, java.util.function.Function<android.view.textclassifier.TextLinks.TextLink, android.view.textclassifier.TextLinks.TextLinkSpan>); method public int describeContents(); diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 9692579de6ba..2e92f14c931c 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -21,7 +21,6 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; -import android.annotation.UnsupportedAppUsage; import android.annotation.WorkerThread; import android.os.LocaleList; import android.os.Looper; @@ -212,34 +211,13 @@ public interface TextClassifier { return suggestSelection(request); } - // TODO: Remove once apps can build against the latest sdk. - /** @hide */ - @UnsupportedAppUsage - default TextSelection suggestSelection( - @NonNull CharSequence text, - @IntRange(from = 0) int selectionStartIndex, - @IntRange(from = 0) int selectionEndIndex, - @Nullable TextSelection.Options options) { - if (options == null) { - return suggestSelection(new TextSelection.Request.Builder( - text, selectionStartIndex, selectionEndIndex).build()); - } else if (options.getRequest() != null) { - return suggestSelection(options.getRequest()); - } else { - return suggestSelection( - new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex) - .setDefaultLocales(options.getDefaultLocales()) - .build()); - } - } - /** * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. * * <p><strong>NOTE: </strong>Call on a worker thread. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * * @param request the text classification request @@ -262,7 +240,7 @@ public interface TextClassifier { * {@link #classifyText(TextClassification.Request)}. If that method calls this method, * a stack overflow error will happen. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * * @param text text providing context for the text to classify (which is specified @@ -292,34 +270,13 @@ public interface TextClassifier { return classifyText(request); } - // TODO: Remove once apps can build against the latest sdk. - /** @hide */ - @UnsupportedAppUsage - default TextClassification classifyText( - @NonNull CharSequence text, - @IntRange(from = 0) int startIndex, - @IntRange(from = 0) int endIndex, - @Nullable TextClassification.Options options) { - if (options == null) { - return classifyText( - new TextClassification.Request.Builder(text, startIndex, endIndex).build()); - } else if (options.getRequest() != null) { - return classifyText(options.getRequest()); - } else { - return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex) - .setDefaultLocales(options.getDefaultLocales()) - .setReferenceTime(options.getReferenceTime()) - .build()); - } - } - /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. * * <p><strong>NOTE: </strong>Call on a worker thread. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * * @param request the text links request @@ -334,27 +291,10 @@ public interface TextClassifier { return new TextLinks.Builder(request.getText().toString()).build(); } - // TODO: Remove once apps can build against the latest sdk. - /** @hide */ - @UnsupportedAppUsage - default TextLinks generateLinks( - @NonNull CharSequence text, @Nullable TextLinks.Options options) { - if (options == null) { - return generateLinks(new TextLinks.Request.Builder(text).build()); - } else if (options.getRequest() != null) { - return generateLinks(options.getRequest()); - } else { - return generateLinks(new TextLinks.Request.Builder(text) - .setDefaultLocales(options.getDefaultLocales()) - .setEntityConfig(options.getEntityConfig()) - .build()); - } - } - /** * Returns the maximal length of text that can be processed by generateLinks. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * * @see #generateLinks(TextLinks.Request) @@ -365,9 +305,29 @@ public interface TextClassifier { } /** + * Detects the language of the specified text. + * + * <p><strong>NOTE: </strong>Call on a worker thread. + * + * + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. + * + * @param request the {@link TextLanguage} request. + * @return the {@link TextLanguage} result. + */ + @WorkerThread + @NonNull + default TextLanguage detectLanguage(@NonNull TextLanguage.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); + return TextLanguage.EMPTY; + } + + /** * Reports a selection event. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. */ default void onSelectionEvent(@NonNull SelectionEvent event) {} @@ -375,7 +335,7 @@ public interface TextClassifier { /** * Destroys this TextClassifier. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * * <p>Subsequent calls to this method are no-ops. @@ -385,7 +345,7 @@ public interface TextClassifier { /** * Returns whether or not this TextClassifier has been destroyed. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact * with the classifier and an attempt to do so would throw an {@link IllegalStateException}. * However, this method should never throw an {@link IllegalStateException}. * @@ -396,9 +356,7 @@ public interface TextClassifier { } /** @hide **/ - default void dump(@NonNull IndentingPrintWriter printWriter) { - - } + default void dump(@NonNull IndentingPrintWriter printWriter) {} /** * Configuration object for specifying what entities to identify. diff --git a/core/java/android/view/textclassifier/TextLanguage.java b/core/java/android/view/textclassifier/TextLanguage.java new file mode 100644 index 000000000000..d28459e733f0 --- /dev/null +++ b/core/java/android/view/textclassifier/TextLanguage.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2018 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.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.icu.util.ULocale; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; + +import com.android.internal.util.Preconditions; + +import java.util.Locale; +import java.util.Map; + +/** + * Represents the result of language detection of a piece of text. + * <p> + * This contains a list of locales, each paired with a confidence score, sorted in decreasing + * order of those scores. E.g., for a given input text, the model may return + * {@code [<"en", 0.85>, <"fr", 0.15>]}. This sample result means the model reports that it is + * 85% likely that the entire text is in English and 15% likely that the entire text is in French, + * etc. It does not mean that 85% of the input is in English and 15% is in French. + */ +public final class TextLanguage implements Parcelable { + + public static final Creator<TextLanguage> CREATOR = new Creator<TextLanguage>() { + @Override + public TextLanguage createFromParcel(Parcel in) { + return readFromParcel(in); + } + + @Override + public TextLanguage[] newArray(int size) { + return new TextLanguage[size]; + } + }; + + static final TextLanguage EMPTY = new Builder().build(); + + @Nullable private final String mId; + private final EntityConfidence mEntityConfidence; + private final Bundle mBundle; + + private TextLanguage( + @Nullable String id, + EntityConfidence entityConfidence, + Bundle bundle) { + mId = id; + mEntityConfidence = entityConfidence; + mBundle = bundle; + } + + /** + * Returns the id, if one exists, for this object. + */ + @Nullable + public String getId() { + return mId; + } + + /** + * Returns the number of possible locales for the processed text. + */ + @IntRange(from = 0) + public int getLocaleHypothesisCount() { + return mEntityConfidence.getEntities().size(); + } + + /** + * Returns the language locale at the specified index. Locales are ordered from high + * confidence to low confidence. + * + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getLocaleCount() for the number of locales available. + */ + @NonNull + public ULocale getLocale(int index) { + return ULocale.forLanguageTag(mEntityConfidence.getEntities().get(index)); + } + + /** + * Returns the confidence score for the specified language locale. The value ranges from + * 0 (low confidence) to 1 (high confidence). 0 indicates that the locale was not found for + * the processed text. + */ + @FloatRange(from = 0.0, to = 1.0) + public float getConfidenceScore(@NonNull ULocale locale) { + return mEntityConfidence.getConfidenceScore(locale.toLanguageTag()); + } + + /** + * Returns a bundle containing non-structured extra information about this result. + * + * <p><b>NOTE: </b>Each call to this method returns a new bundle copy so clients should prefer + * to hold a reference to the returned bundle rather than frequently calling this method. + */ + @NonNull + public Bundle getExtras() { + return mBundle.deepCopy(); + } + + @Override + public String toString() { + return String.format( + Locale.US, + "TextLanguage {id=%s, locales=%s, bundle=%s}", + mId, mEntityConfidence, mBundle); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + mEntityConfidence.writeToParcel(dest, flags); + dest.writeBundle(mBundle); + } + + private static TextLanguage readFromParcel(Parcel in) { + return new TextLanguage( + in.readString(), + EntityConfidence.CREATOR.createFromParcel(in), + in.readBundle()); + } + + /** + * Builder used to build TextLanguage objects. + */ + public static final class Builder { + + @Nullable private String mId; + private final Map<String, Float> mEntityConfidenceMap = new ArrayMap<>(); + @Nullable private Bundle mBundle; + + /** + * Sets a language locale for the processed text and assigns a confidence score. If the + * locale has already been set, this updates it. + * + * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). + * 0 implies the locale does not exist for the processed text. + * Values greater than 1 are clamped to 1. + */ + @NonNull + public Builder putLocale( + @NonNull ULocale locale, + @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { + Preconditions.checkNotNull(locale); + mEntityConfidenceMap.put(locale.toLanguageTag(), confidenceScore); + return this; + } + + /** + * Sets an optional id for the TextLanguage object. + */ + @NonNull + public Builder setId(@Nullable String id) { + mId = id; + return this; + } + + /** + * Sets a bundle containing non-structured extra information about the TextLanguage object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle bundle) { + mBundle = Preconditions.checkNotNull(bundle); + return this; + } + + /** + * Builds and returns a new TextLanguage object. + * <p> + * If necessary, this method will verify fields, clamp them, and make them immutable. + */ + @NonNull + public TextLanguage build() { + mBundle = mBundle == null ? new Bundle() : mBundle.deepCopy(); + return new TextLanguage( + mId, + new EntityConfidence(mEntityConfidenceMap), + mBundle); + } + } + + /** + * A request object for detecting the language of a piece of text. + */ + public static final class Request implements Parcelable { + + public static final Creator<Request> CREATOR = new Creator<Request>() { + @Override + public Request createFromParcel(Parcel in) { + return readFromParcel(in); + } + + @Override + public Request[] newArray(int size) { + return new Request[size]; + } + }; + + private final CharSequence mText; + private final Bundle mBundle; + + private Request(CharSequence text, Bundle bundle) { + mText = text; + mBundle = bundle; + } + + /** + * Returns the text to process. + */ + @NonNull + public CharSequence getText() { + return mText; + } + + /** + * Returns a bundle containing non-structured extra information about this request. + * + * <p><b>NOTE: </b>Each call to this method returns a new bundle copy so clients should + * prefer to hold a reference to the returned bundle rather than frequently calling this + * method. + */ + @NonNull + public Bundle getExtras() { + return mBundle.deepCopy(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeCharSequence(mText); + dest.writeBundle(mBundle); + } + + private static Request readFromParcel(Parcel in) { + return new Request( + in.readCharSequence(), + in.readBundle()); + } + + /** + * A builder for building TextLanguage requests. + */ + public static final class Builder { + + private final CharSequence mText; + @Nullable private Bundle mBundle; + + /** + * Creates a builder to build TextLanguage requests. + * + * @param text the text to process. + */ + public Builder(@NonNull CharSequence text) { + mText = Preconditions.checkNotNull(text); + } + + /** + * Sets a bundle containing non-structured extra information about the request. + */ + @NonNull + public Builder setExtras(@NonNull Bundle bundle) { + mBundle = Preconditions.checkNotNull(bundle); + return this; + } + + /** + * Builds and returns a new TextLanguage request object. + * <p> + * If necessary, this method will verify fields, clamp them, and make them immutable. + */ + @NonNull + public Request build() { + mBundle = mBundle == null ? new Bundle() : mBundle.deepCopy(); + return new Request(mText.toString(), mBundle); + } + } + } +} diff --git a/core/tests/coretests/src/android/view/textclassifier/TextLanguageTest.java b/core/tests/coretests/src/android/view/textclassifier/TextLanguageTest.java new file mode 100644 index 000000000000..75ca769294ce --- /dev/null +++ b/core/tests/coretests/src/android/view/textclassifier/TextLanguageTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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 static org.junit.Assert.assertEquals; + +import android.icu.util.ULocale; +import android.os.Bundle; +import android.os.Parcel; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for TextLanguage. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class TextLanguageTest { + + private static final float EPSILON = 0.000001f; + + @Test + public void testParcel() throws Exception { + final String bundleKey = "experiment.int"; + final Bundle bundle = new Bundle(); + bundle.putInt(bundleKey, 1234); + + final TextLanguage reference = new TextLanguage.Builder() + .setId("id") + .setExtras(bundle) + .putLocale(ULocale.ENGLISH, 0.8f) + .putLocale(ULocale.GERMAN, 0.2f) + .build(); + + final Parcel parcel = Parcel.obtain(); + reference.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + final TextLanguage result = TextLanguage.CREATOR.createFromParcel(parcel); + + assertEquals("id", result.getId()); + assertEquals(1234, result.getExtras().getInt(bundleKey)); + assertEquals(2, result.getLocaleHypothesisCount()); + assertEquals(ULocale.ENGLISH, result.getLocale(0)); + assertEquals(0.8f, result.getConfidenceScore(ULocale.ENGLISH), EPSILON); + assertEquals(ULocale.GERMAN, result.getLocale(1)); + assertEquals(0.2f, result.getConfidenceScore(ULocale.GERMAN), EPSILON); + } + + @Test + public void testRequestParcel() throws Exception { + final String text = "This is random text"; + final String bundleKey = "experiment.str"; + final Bundle bundle = new Bundle(); + bundle.putString(bundleKey, "bundle"); + + final TextLanguage.Request reference = new TextLanguage.Request.Builder(text) + .setExtras(bundle) + .build(); + + final Parcel parcel = Parcel.obtain(); + reference.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + final TextLanguage.Request result = TextLanguage.Request.CREATOR.createFromParcel(parcel); + + assertEquals(text, result.getText()); + assertEquals("bundle", result.getExtras().getString(bundleKey)); + } +} |