Merge "Implement TextClassifierImpl.getLinks"
This commit is contained in:
committed by
Android (Google) Code Review
commit
e363f427f8
@@ -17,20 +17,34 @@
|
||||
package android.view.textclassifier;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
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.icu.text.BreakIterator;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.SmartSelection;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.WordIterator;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Default implementation of the {@link TextClassifier} interface.
|
||||
@@ -99,29 +113,7 @@ final class TextClassifierImpl implements TextClassifier {
|
||||
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.
|
||||
}
|
||||
return createClassificationResult(type, classified);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
@@ -132,9 +124,18 @@ final class TextClassifierImpl implements TextClassifier {
|
||||
return TextClassifier.NO_OP.getTextClassificationResult(text, startIndex, endIndex);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LinksInfo getLinks(@NonNull CharSequence text, int linkMask) {
|
||||
// TODO: Implement
|
||||
public LinksInfo getLinks(CharSequence text, int linkMask) {
|
||||
Preconditions.checkArgument(text != null);
|
||||
try {
|
||||
return LinksInfoFactory.create(
|
||||
mContext, getSmartSelection(), text.toString(), linkMask);
|
||||
} catch (Throwable t) {
|
||||
// Avoid throwing from this method. Log the error.
|
||||
Log.e(LOG_TAG, "Error getting links info.", t);
|
||||
}
|
||||
// Getting here means something went wrong, return a NO_OP result.
|
||||
return TextClassifier.NO_OP.getLinks(text, linkMask);
|
||||
}
|
||||
|
||||
@@ -145,19 +146,23 @@ final class TextClassifierImpl implements TextClassifier {
|
||||
return mSmartSelection;
|
||||
}
|
||||
|
||||
private TextClassificationResult createClassificationResult(
|
||||
CharSequence text, String type, Intent intent, String label) {
|
||||
TextClassificationResult.Builder builder = new TextClassificationResult.Builder()
|
||||
private TextClassificationResult createClassificationResult(String type, CharSequence text) {
|
||||
final Intent intent = IntentFactory.create(type, text.toString());
|
||||
if (intent == null) {
|
||||
return TextClassificationResult.EMPTY;
|
||||
}
|
||||
|
||||
final 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.
|
||||
.setLabel(IntentFactory.getLabel(mContext, type));
|
||||
final PackageManager pm = mContext.getPackageManager();
|
||||
final 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) {
|
||||
@@ -177,4 +182,198 @@ final class TextClassifierImpl implements TextClassifier {
|
||||
Preconditions.checkArgument(endIndex <= text.length());
|
||||
Preconditions.checkArgument(endIndex >= startIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and creates links for specified text.
|
||||
*/
|
||||
private static final class LinksInfoFactory {
|
||||
|
||||
private LinksInfoFactory() {}
|
||||
|
||||
public static LinksInfo create(
|
||||
Context context, SmartSelection smartSelection, String text, int linkMask) {
|
||||
final WordIterator wordIterator = new WordIterator();
|
||||
wordIterator.setCharSequence(text, 0, text.length());
|
||||
final List<SpanSpec> spans = new ArrayList<>();
|
||||
int start = 0;
|
||||
int end;
|
||||
while ((end = wordIterator.nextBoundary(start)) != BreakIterator.DONE) {
|
||||
final String token = text.substring(start, end);
|
||||
if (TextUtils.isEmpty(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int[] selection = smartSelection.suggest(text, start, end);
|
||||
final int selectionStart = selection[0];
|
||||
final int selectionEnd = selection[1];
|
||||
if (selectionStart >= 0 && selectionEnd <= text.length()
|
||||
&& selectionStart <= selectionEnd) {
|
||||
final String type =
|
||||
smartSelection.classifyText(text, selectionStart, selectionEnd);
|
||||
if (matches(type, linkMask)) {
|
||||
final ClickableSpan span = createSpan(
|
||||
context, type, text.substring(selectionStart, selectionEnd));
|
||||
spans.add(new SpanSpec(selectionStart, selectionEnd, span));
|
||||
}
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
return new LinksInfoImpl(text, avoidOverlaps(spans, text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the classification type matches the specified linkMask.
|
||||
*/
|
||||
private static boolean matches(String type, int linkMask) {
|
||||
if ((linkMask & Linkify.PHONE_NUMBERS) != 0
|
||||
&& TextClassifier.TYPE_PHONE.equals(type)) {
|
||||
return true;
|
||||
}
|
||||
if ((linkMask & Linkify.EMAIL_ADDRESSES) != 0
|
||||
&& TextClassifier.TYPE_EMAIL.equals(type)) {
|
||||
return true;
|
||||
}
|
||||
if ((linkMask & Linkify.MAP_ADDRESSES) != 0
|
||||
&& TextClassifier.TYPE_ADDRESS.equals(type)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim the number of spans so that no two spans overlap.
|
||||
*
|
||||
* This algorithm first ensures that there is only one span per start index, then it
|
||||
* makes sure that no two spans overlap.
|
||||
*/
|
||||
private static List<SpanSpec> avoidOverlaps(List<SpanSpec> spans, String text) {
|
||||
Collections.sort(spans, Comparator.comparingInt(span -> span.mStart));
|
||||
// Group spans by start index. Take the longest span.
|
||||
final Map<Integer, SpanSpec> reps = new LinkedHashMap<>(); // order matters.
|
||||
final int size = spans.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final SpanSpec span = spans.get(i);
|
||||
final LinksInfoFactory.SpanSpec rep = reps.get(span.mStart);
|
||||
if (rep == null || rep.mEnd < span.mEnd) {
|
||||
reps.put(span.mStart, span);
|
||||
}
|
||||
}
|
||||
// Avoid span intersections. Take the longer span.
|
||||
final LinkedList<SpanSpec> result = new LinkedList<>();
|
||||
for (SpanSpec rep : reps.values()) {
|
||||
if (result.isEmpty()) {
|
||||
result.add(rep);
|
||||
continue;
|
||||
}
|
||||
|
||||
final SpanSpec last = result.getLast();
|
||||
if (rep.mStart < last.mEnd) {
|
||||
// Spans intersect. Use the one with characters.
|
||||
if ((rep.mEnd - rep.mStart) > (last.mEnd - last.mStart)) {
|
||||
result.set(result.size() - 1, rep);
|
||||
}
|
||||
} else {
|
||||
result.add(rep);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ClickableSpan createSpan(
|
||||
final Context context, final String type, final String text) {
|
||||
return new ClickableSpan() {
|
||||
// TODO: Style this span.
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
context.startActivity(IntentFactory.create(type, text));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of LinksInfo that adds ClickableSpans to the specified text.
|
||||
*/
|
||||
private static final class LinksInfoImpl implements LinksInfo {
|
||||
|
||||
private final CharSequence mOriginalText;
|
||||
private final List<SpanSpec> mSpans;
|
||||
|
||||
LinksInfoImpl(CharSequence originalText, List<SpanSpec> spans) {
|
||||
mOriginalText = originalText;
|
||||
mSpans = spans;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(@NonNull CharSequence text) {
|
||||
Preconditions.checkArgument(text != null);
|
||||
if (text instanceof Spannable && mOriginalText.toString().equals(text.toString())) {
|
||||
Spannable spannable = (Spannable) text;
|
||||
final int size = mSpans.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
final SpanSpec span = mSpans.get(i);
|
||||
spannable.setSpan(span.mSpan, span.mStart, span.mEnd, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Span plus its start and end index.
|
||||
*/
|
||||
private static final class SpanSpec {
|
||||
|
||||
private final int mStart;
|
||||
private final int mEnd;
|
||||
private final ClickableSpan mSpan;
|
||||
|
||||
SpanSpec(int start, int end, ClickableSpan span) {
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
mSpan = span;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates intents based on the classification type.
|
||||
*/
|
||||
private static final class IntentFactory {
|
||||
|
||||
private IntentFactory() {}
|
||||
|
||||
@Nullable
|
||||
public static Intent create(String type, String text) {
|
||||
switch (type) {
|
||||
case TextClassifier.TYPE_EMAIL:
|
||||
return new Intent(Intent.ACTION_SENDTO)
|
||||
.setData(Uri.parse(String.format("mailto:%s", text)));
|
||||
case TextClassifier.TYPE_PHONE:
|
||||
return new Intent(Intent.ACTION_DIAL)
|
||||
.setData(Uri.parse(String.format("tel:%s", text)));
|
||||
case TextClassifier.TYPE_ADDRESS:
|
||||
return new Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
|
||||
default:
|
||||
return null;
|
||||
// TODO: Add other classification types.
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLabel(Context context, String type) {
|
||||
switch (type) {
|
||||
case TextClassifier.TYPE_EMAIL:
|
||||
return context.getString(com.android.internal.R.string.email);
|
||||
case TextClassifier.TYPE_PHONE:
|
||||
return context.getString(com.android.internal.R.string.dial);
|
||||
case TextClassifier.TYPE_ADDRESS:
|
||||
return context.getString(com.android.internal.R.string.map);
|
||||
default:
|
||||
return null;
|
||||
// TODO: Add other classification types.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user