Implement TextClassification-related methods.
Implements TextClassificationManager.detectLanguages Implements TextClassifier interface Bug: 34661057 Test: See: Ic2a5eceeaec4cd2943c6c753084df46d30511fee Change-Id: Ic640b96f48bcad7cdd8c4dfac354b008a7ae3961
This commit is contained in:
60
core/java/android/text/LangId.java
Normal file
60
core/java/android/text/LangId.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
84
core/java/android/text/SmartSelection.java
Normal file
84
core/java/android/text/SmartSelection.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
core/java/android/view/textclassifier/TextClassifierImpl.java
Normal file
180
core/java/android/view/textclassifier/TextClassifierImpl.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user