diff options
| author | 2017-01-13 13:46:33 -0800 | |
|---|---|---|
| committer | 2017-01-24 15:13:04 +0000 | |
| commit | 43e0350922556e86b641da084a2c9dc2b07fc662 (patch) | |
| tree | ed5419913e423b37f6105595f84f3a3638fdfda9 | |
| parent | 110dad7c782c4388906dccb1d1891d4b6fc3e49f (diff) | |
Implement TextClassification-related methods.
Implements TextClassificationManager.detectLanguages
Implements TextClassifier interface
Bug: 34661057
Test: See: Ic2a5eceeaec4cd2943c6c753084df46d30511fee
Change-Id: Ic640b96f48bcad7cdd8c4dfac354b008a7ae3961
| -rw-r--r-- | core/java/android/text/LangId.java | 60 | ||||
| -rw-r--r-- | core/java/android/text/SmartSelection.java | 84 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassificationManager.java | 59 | ||||
| -rw-r--r-- | core/java/android/view/textclassifier/TextClassifierImpl.java | 180 | ||||
| -rw-r--r-- | core/res/res/values/strings.xml | 9 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 3 |
6 files changed, 392 insertions, 3 deletions
diff --git a/core/java/android/text/LangId.java b/core/java/android/text/LangId.java new file mode 100644 index 000000000000..ed6e9097d669 --- /dev/null +++ b/core/java/android/text/LangId.java @@ -0,0 +1,60 @@ +/* + * 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); +} + diff --git a/core/java/android/text/SmartSelection.java b/core/java/android/text/SmartSelection.java new file mode 100644 index 000000000000..97ef5149cf23 --- /dev/null +++ b/core/java/android/text/SmartSelection.java @@ -0,0 +1,84 @@ +/* + * 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); +} + diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java index b5ab4bfb1d0a..4673c50cddc9 100644 --- a/core/java/android/view/textclassifier/TextClassificationManager.java +++ b/core/java/android/view/textclassifier/TextClassificationManager.java @@ -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; + } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java new file mode 100644 index 000000000000..72796cf160d0 --- /dev/null +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -0,0 +1,180 @@ +/* + * 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); + } +} diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d09b19069e77..eece9fc36c30 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -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. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6eb3bee67741..16356c73f79b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -476,6 +476,9 @@ <java-symbol type="string" name="replace" /> <java-symbol type="string" name="undo" /> <java-symbol type="string" name="redo" /> + <java-symbol type="string" name="email" /> + <java-symbol type="string" name="dial" /> + <java-symbol type="string" name="map" /> <java-symbol type="string" name="textSelectionCABTitle" /> <java-symbol type="string" name="BaMmi" /> <java-symbol type="string" name="CLIRDefaultOffNextCallOff" /> |