Implement TextClassification-related methods.

Implements TextClassificationManager.detectLanguages
 Implements TextClassifier interface

Bug: 34661057
Test: See: Ic2a5eceeaec4cd2943c6c753084df46d30511fee
Change-Id: Ic640b96f48bcad7cdd8c4dfac354b008a7ae3961
This commit is contained in:
Abodunrinwa Toki
2017-01-13 13:46:33 -08:00
parent 110dad7c78
commit 43e0350922
6 changed files with 392 additions and 3 deletions

View 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);
}

View 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);
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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. -->

View File

@@ -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" />