summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Abodunrinwa Toki <toki@google.com> 2017-01-13 13:46:33 -0800
committer Abodunrinwa Toki <toki@google.com> 2017-01-24 15:13:04 +0000
commit43e0350922556e86b641da084a2c9dc2b07fc662 (patch)
treeed5419913e423b37f6105595f84f3a3638fdfda9
parent110dad7c782c4388906dccb1d1891d4b6fc3e49f (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.java60
-rw-r--r--core/java/android/text/SmartSelection.java84
-rw-r--r--core/java/android/view/textclassifier/TextClassificationManager.java59
-rw-r--r--core/java/android/view/textclassifier/TextClassifierImpl.java180
-rw-r--r--core/res/res/values/strings.xml9
-rw-r--r--core/res/res/values/symbols.xml3
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" />