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

Commit 43e03509 authored by Abodunrinwa Toki's avatar Abodunrinwa Toki
Browse files

Implement TextClassification-related methods.

 Implements TextClassificationManager.detectLanguages
 Implements TextClassifier interface

Bug: 34661057
Test: See: Ic2a5eceeaec4cd2943c6c753084df46d30511fee
Change-Id: Ic640b96f48bcad7cdd8c4dfac354b008a7ae3961
parent 110dad7c
Loading
Loading
Loading
Loading
+60 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.text;

/**
 *  Java wrapper for LangId native library interface.
 *  This class is used to detect languages in text.
 *  @hide
 */
public final class LangId {
    // TODO: Move this to android.view.textclassifier and make it package-private.
    // We'll have to update the native library code to do this.

    static {
        System.loadLibrary("smart-selection_jni");
    }

    private final long mModelPtr;

    /**
     * Creates a new instance of LangId predictor, using the provided model image.
     */
    public LangId(int fd) {
        mModelPtr = nativeNew(fd);
    }

    /**
     * Detects the language for given text.
     */
    public String findLanguage(String text) {
        return nativeFindLanguage(mModelPtr, text);
    }

    /**
     * Frees up the allocated memory.
     */
    public void close() {
        nativeClose(mModelPtr);
    }

    private static native long nativeNew(int fd);

    private static native String nativeFindLanguage(long context, String text);

    private static native void nativeClose(long context);
}
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.text;

/**
 *  Java wrapper for SmartSelection native library interface.
 *  This library is used for detecting entities in text.
 *  @hide
 */
public final class SmartSelection {
    // TODO: Move this to android.view.textclassifier and make it package-private.
    // We'll have to update the native library code to do this.

    static {
        System.loadLibrary("smart-selection_jni");
    }

    private final long mCtx;

    /**
     * Creates a new instance of SmartSelect predictor, using the provided model image,
     * given as a file descriptor.
     */
    public SmartSelection(int fd) {
        mCtx = nativeNew(fd);
    }

    /**
     * Given a string context and current selection, computes the SmartSelection suggestion.
     *
     * The begin and end are character indices into the context UTF8 string. selectionBegin is the
     * character index where the selection begins, and selectionEnd is the index of one character
     * past the selection span.
     *
     * The return value is an array of two ints: suggested selection beginning and end, with the
     * same semantics as the input selectionBeginning and selectionEnd.
     */
    public int[] suggest(String context, int selectionBegin, int selectionEnd) {
        return nativeSuggest(mCtx, context, selectionBegin, selectionEnd);
    }

    /**
     * Given a string context and current selection, classifies the type of the selected text.
     *
     * The begin and end params are character indices in the context string.
     *
     * Returns the type of the selection, e.g. "email", "address", "phone".
     */
    public String classifyText(String context, int selectionBegin, int selectionEnd) {
        return nativeClassifyText(mCtx, context, selectionBegin, selectionEnd);
    }

    /**
     * Frees up the allocated memory.
     */
    public void close() {
        nativeClose(mCtx);
    }

    private static native long nativeNew(int fd);

    private static native int[] nativeSuggest(
            long context, String text, int selectionBegin, int selectionEnd);

    private static native String nativeClassifyText(
            long context, String text, int selectionBegin, int selectionEnd);

    private static native void nativeClose(long context);
}
+56 −3
Original line number Diff line number Diff line
@@ -18,9 +18,18 @@ package android.view.textclassifier;

import android.annotation.NonNull;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.text.LangId;
import android.util.Log;

import com.android.internal.util.Preconditions;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/**
 * Interface to the text classification service.
@@ -30,14 +39,35 @@ import java.util.List;
 */
public final class TextClassificationManager {

    private static final String LOG_TAG = "TextClassificationManager";

    private final Context mContext;
    // TODO: Implement a way to close the file descriptor.
    private ParcelFileDescriptor mFd;
    private TextClassifier mDefault;
    private LangId mLangId;

    /** @hide */
    public TextClassificationManager(Context context) {}
    public TextClassificationManager(Context context) {
        mContext = Preconditions.checkNotNull(context);
    }

    /**
     * Returns the default text classifier.
     */
    public TextClassifier getDefaultTextClassifier() {
        return TextClassifier.NO_OP;
        if (mDefault == null) {
            try {
                mFd = ParcelFileDescriptor.open(
                        new File("/etc/assistant/smart-selection.model"),
                        ParcelFileDescriptor.MODE_READ_ONLY);
                mDefault = new TextClassifierImpl(mContext, mFd);
            } catch (FileNotFoundException e) {
                Log.e(LOG_TAG, "Error accessing 'text classifier selection' model file.", e);
                mDefault = TextClassifier.NO_OP;
            }
        }
        return mDefault;
    }

    /**
@@ -47,7 +77,30 @@ public final class TextClassificationManager {
     * @throws IllegalArgumentException if text is null
     */
    public List<TextLanguage> detectLanguages(@NonNull CharSequence text) {
        // TODO: Implement
        Preconditions.checkArgument(text != null);
        try {
            if (text.length() > 0) {
                final String language = getLanguageDetector().findLanguage(text.toString());
                final Locale locale = new Locale.Builder().setLanguageTag(language).build();
                return Collections.unmodifiableList(Arrays.asList(
                        new TextLanguage.Builder(0, text.length())
                                .setLanguage(locale, 1.0f /* confidence */)
                                .build()));
            }
        } catch (Throwable t) {
            // Avoid throwing from this method. Log the error.
            Log.e(LOG_TAG, "Error detecting languages for text. Returning empty result.", t);
        }
        // Getting here means something went wrong. Return an empty result.
        return Collections.emptyList();
    }

    private LangId getLanguageDetector() {
        if (mLangId == null) {
            // TODO: Use a file descriptor as soon as we start to depend on a model file
            // for language detection.
            mLangId = new LangId(0);
        }
        return mLangId;
    }
}
+180 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.NonNull;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.SmartSelection;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.util.Preconditions;

import java.io.FileNotFoundException;

/**
 * Default implementation of the {@link TextClassifier} interface.
 *
 * <p>This class uses machine learning to recognize entities in text.
 * Unless otherwise stated, methods of this class are blocking operations and should most
 * likely not be called on the UI thread.
 *
 * @hide
 */
final class TextClassifierImpl implements TextClassifier {

    private static final String LOG_TAG = "TextClassifierImpl";

    private final Context mContext;
    private final ParcelFileDescriptor mFd;
    private SmartSelection mSmartSelection;

    TextClassifierImpl(Context context, ParcelFileDescriptor fd) {
        mContext = Preconditions.checkNotNull(context);
        mFd = Preconditions.checkNotNull(fd);
    }

    @Override
    public TextSelection suggestSelection(
            @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex) {
        validateInput(text, selectionStartIndex, selectionEndIndex);
        try {
            if (text.length() > 0) {
                final String string = text.toString();
                final int[] startEnd = getSmartSelection()
                        .suggest(string, selectionStartIndex, selectionEndIndex);
                final int start = startEnd[0];
                final int end = startEnd[1];
                if (start >= 0 && end <= string.length() && start <= end) {
                    final String type = getSmartSelection().classifyText(string, start, end);
                    return new TextSelection.Builder(start, end)
                            .setEntityType(type, 1.0f)
                            .build();
                } else {
                    // We can not trust the result. Log the issue and ignore the result.
                    Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
                }
            }
        } catch (Throwable t) {
            // Avoid throwing from this method. Log the error.
            Log.e(LOG_TAG,
                    "Error suggesting selection for text. No changes to selection suggested.",
                    t);
        }
        // Getting here means something went wrong, return a NO_OP result.
        return TextClassifier.NO_OP.suggestSelection(
                text, selectionStartIndex, selectionEndIndex);
    }

    @Override
    public TextClassificationResult getTextClassificationResult(
            @NonNull CharSequence text, int startIndex, int endIndex) {
        validateInput(text, startIndex, endIndex);
        try {
            if (text.length() > 0) {
                final CharSequence classified = text.subSequence(startIndex, endIndex);
                String type = getSmartSelection()
                        .classifyText(text.toString(), startIndex, endIndex);
                if (!TextUtils.isEmpty(type)) {
                    type = type.toLowerCase().trim();
                    // TODO: Added this log for debug only. Remove before release.
                    Log.d(LOG_TAG, String.format("Classification type: %s", type));
                    final Intent intent;
                    final String title;
                    switch (type) {
                        case TextClassifier.TYPE_EMAIL:
                            intent = new Intent(Intent.ACTION_SENDTO);
                            intent.setData(Uri.parse(String.format("mailto:%s", text)));
                            title = mContext.getString(com.android.internal.R.string.email);
                            return createClassificationResult(classified, type, intent, title);
                        case TextClassifier.TYPE_PHONE:
                            intent = new Intent(Intent.ACTION_DIAL);
                            intent.setData(Uri.parse(String.format("tel:%s", text)));
                            title = mContext.getString(com.android.internal.R.string.dial);
                            return createClassificationResult(classified, type, intent, title);
                        case TextClassifier.TYPE_ADDRESS:
                            intent = new Intent(Intent.ACTION_VIEW);
                            intent.setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
                            title = mContext.getString(com.android.internal.R.string.map);
                            return createClassificationResult(classified, type, intent, title);
                        default:
                            // No classification type found. Return a no-op result.
                            break;
                        // TODO: Add other classification types.
                    }
                }
            }
        } catch (Throwable t) {
            // Avoid throwing from this method. Log the error.
            Log.e(LOG_TAG, "Error getting assist info.", t);
        }
        // Getting here means something went wrong, return a NO_OP result.
        return TextClassifier.NO_OP.getTextClassificationResult(text, startIndex, endIndex);
    }

    @Override
    public LinksInfo getLinks(@NonNull CharSequence text, int linkMask) {
        // TODO: Implement
        return TextClassifier.NO_OP.getLinks(text, linkMask);
    }

    private synchronized SmartSelection getSmartSelection() throws FileNotFoundException {
        if (mSmartSelection == null) {
            mSmartSelection = new SmartSelection(mFd.getFd());
        }
        return mSmartSelection;
    }

    private TextClassificationResult createClassificationResult(
            CharSequence text, String type, Intent intent, String label) {
        TextClassificationResult.Builder builder = new TextClassificationResult.Builder()
                .setText(text.toString())
                .setEntityType(type, 1.0f /* confidence */)
                .setIntent(intent)
                .setOnClickListener(TextClassificationResult.createStartActivityOnClick(
                        mContext, intent))
                .setLabel(label);
        PackageManager pm = mContext.getPackageManager();
        ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
        // TODO: If the resolveInfo is the "chooser", do not set the package name and use a
        // default icon for this classification type.
        intent.setPackage(resolveInfo.activityInfo.packageName);
        Drawable icon = resolveInfo.activityInfo.loadIcon(pm);
        if (icon == null) {
            icon = resolveInfo.loadIcon(pm);
        }
        builder.setIcon(icon);
        return builder.build();
    }

    /**
     * @throws IllegalArgumentException if text is null; startIndex is negative;
     *      endIndex is greater than text.length() or less than startIndex
     */
    private static void validateInput(@NonNull CharSequence text, int startIndex, int endIndex) {
        Preconditions.checkArgument(text != null);
        Preconditions.checkArgument(startIndex >= 0);
        Preconditions.checkArgument(endIndex <= text.length());
        Preconditions.checkArgument(endIndex >= startIndex);
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -2589,6 +2589,15 @@
    <!-- Title for EditText context menu [CHAR LIMIT=20] -->
    <string name="editTextMenuTitle">Text actions</string>

    <!-- Label for item in the text selection menu to trigger an Email app [CHAR LIMIT=20] -->
    <string name="email">Email</string>

    <!-- Label for item in the text selection menu to trigger a Dialer app [CHAR LIMIT=20] -->
    <string name="dial">Dial</string>

    <!-- Label for item in the text selection menu to trigger a Map app [CHAR LIMIT=20] -->
    <string name="map">Map</string>

    <!-- If the device is getting low on internal storage, a notification is shown to the user.  This is the title of that notification. -->
    <string name="low_internal_storage_view_title">Storage space running out</string>
    <!-- If the device is getting low on internal storage, a notification is shown to the user.  This is the message of that notification. -->
Loading