Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e428c326 authored by Abodunrinwa Toki's avatar Abodunrinwa Toki Committed by Android (Google) Code Review
Browse files

Merge "Introduce TextClassifier.detectLanguage() API."

parents b6271fed 7cefd4f2
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
@@ -51415,6 +51415,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();
@@ -51455,6 +51456,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();
+28 −70
Original line number Diff line number Diff line
@@ -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)
@@ -364,10 +304,30 @@ public interface TextClassifier {
        return Integer.MAX_VALUE;
    }

    /**
     * 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.
+307 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+85 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}