TextClassifierImpl.java revision 70d41cd792cbbc1eb6b2c36be54cfcae7b53c03a
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textclassifier;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.graphics.drawable.Drawable;
27import android.icu.text.BreakIterator;
28import android.net.Uri;
29import android.os.LocaleList;
30import android.os.ParcelFileDescriptor;
31import android.provider.Browser;
32import android.text.Spannable;
33import android.text.TextUtils;
34import android.text.method.WordIterator;
35import android.text.style.ClickableSpan;
36import android.text.util.Linkify;
37import android.util.Log;
38import android.util.Patterns;
39import android.view.View;
40import android.widget.TextViewMetrics;
41
42import com.android.internal.annotations.GuardedBy;
43import com.android.internal.util.Preconditions;
44
45import java.io.File;
46import java.io.FileNotFoundException;
47import java.io.IOException;
48import java.util.ArrayList;
49import java.util.Collections;
50import java.util.Comparator;
51import java.util.HashMap;
52import java.util.LinkedHashMap;
53import java.util.LinkedList;
54import java.util.List;
55import java.util.Locale;
56import java.util.Map;
57import java.util.Objects;
58import java.util.regex.Matcher;
59import java.util.regex.Pattern;
60
61/**
62 * Default implementation of the {@link TextClassifier} interface.
63 *
64 * <p>This class uses machine learning to recognize entities in text.
65 * Unless otherwise stated, methods of this class are blocking operations and should most
66 * likely not be called on the UI thread.
67 *
68 * @hide
69 */
70final class TextClassifierImpl implements TextClassifier {
71
72    private static final String LOG_TAG = "TextClassifierImpl";
73    private static final String MODEL_DIR = "/etc/textclassifier/";
74    private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
75    private static final String UPDATED_MODEL_FILE_PATH =
76            "/data/misc/textclassifier/textclassifier.smartselection.model";
77
78    private final Context mContext;
79
80    private final Object mSmartSelectionLock = new Object();
81    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
82    private Map<Locale, String> mModelFilePaths;
83    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
84    private Locale mLocale;
85    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
86    private SmartSelection mSmartSelection;
87
88    TextClassifierImpl(Context context) {
89        mContext = Preconditions.checkNotNull(context);
90    }
91
92    @Override
93    public TextSelection suggestSelection(
94            @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
95            @Nullable LocaleList defaultLocales) {
96        validateInput(text, selectionStartIndex, selectionEndIndex);
97        try {
98            if (text.length() > 0) {
99                final SmartSelection smartSelection = getSmartSelection(defaultLocales);
100                final String string = text.toString();
101                final int[] startEnd = smartSelection.suggest(
102                        string, selectionStartIndex, selectionEndIndex);
103                final int start = startEnd[0];
104                final int end = startEnd[1];
105                if (start >= 0 && end <= string.length() && start <= end) {
106                    final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
107                    final SmartSelection.ClassificationResult[] results =
108                            smartSelection.classifyText(
109                                    string, start, end,
110                                    getHintFlags(string, start, end));
111                    final int size = results.length;
112                    for (int i = 0; i < size; i++) {
113                        tsBuilder.setEntityType(results[i].mCollection, results[i].mScore);
114                    }
115                    return tsBuilder.build();
116                } else {
117                    // We can not trust the result. Log the issue and ignore the result.
118                    Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
119                }
120            }
121        } catch (Throwable t) {
122            // Avoid throwing from this method. Log the error.
123            Log.e(LOG_TAG,
124                    "Error suggesting selection for text. No changes to selection suggested.",
125                    t);
126        }
127        // Getting here means something went wrong, return a NO_OP result.
128        return TextClassifier.NO_OP.suggestSelection(
129                text, selectionStartIndex, selectionEndIndex, defaultLocales);
130    }
131
132    @Override
133    public TextClassification classifyText(
134            @NonNull CharSequence text, int startIndex, int endIndex,
135            @Nullable LocaleList defaultLocales) {
136        validateInput(text, startIndex, endIndex);
137        try {
138            if (text.length() > 0) {
139                final String string = text.toString();
140                SmartSelection.ClassificationResult[] results = getSmartSelection(defaultLocales)
141                        .classifyText(string, startIndex, endIndex,
142                                getHintFlags(string, startIndex, endIndex));
143                if (results.length > 0) {
144                    final TextClassification classificationResult =
145                            createClassificationResult(
146                                    results, string.subSequence(startIndex, endIndex));
147                    // TODO: Added this log for debug only. Remove before release.
148                    Log.d(LOG_TAG, String.format(
149                            "Classification type: %s", classificationResult));
150                    return classificationResult;
151                }
152            }
153        } catch (Throwable t) {
154            // Avoid throwing from this method. Log the error.
155            Log.e(LOG_TAG, "Error getting assist info.", t);
156        }
157        // Getting here means something went wrong, return a NO_OP result.
158        return TextClassifier.NO_OP.classifyText(
159                text, startIndex, endIndex, defaultLocales);
160    }
161
162    @Override
163    public LinksInfo getLinks(
164            @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales) {
165        Preconditions.checkArgument(text != null);
166        try {
167            return LinksInfoFactory.create(
168                    mContext, getSmartSelection(defaultLocales), text.toString(), linkMask);
169        } catch (Throwable t) {
170            // Avoid throwing from this method. Log the error.
171            Log.e(LOG_TAG, "Error getting links info.", t);
172        }
173        // Getting here means something went wrong, return a NO_OP result.
174        return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales);
175    }
176
177    private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
178        synchronized (mSmartSelectionLock) {
179            localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
180            final Locale locale = findBestSupportedLocaleLocked(localeList);
181            if (locale == null) {
182                throw new FileNotFoundException("No file for null locale");
183            }
184            if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
185                destroySmartSelectionIfExistsLocked();
186                final ParcelFileDescriptor fd = getFdLocked(locale);
187                mSmartSelection = new SmartSelection(fd.getFd());
188                closeAndLogError(fd);
189                mLocale = locale;
190            }
191            return mSmartSelection;
192        }
193    }
194
195    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
196    private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
197        ParcelFileDescriptor updateFd;
198        try {
199            updateFd = ParcelFileDescriptor.open(
200                    new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
201        } catch (FileNotFoundException e) {
202            updateFd = null;
203        }
204        ParcelFileDescriptor factoryFd;
205        try {
206            final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale);
207            if (factoryModelFilePath != null) {
208                factoryFd = ParcelFileDescriptor.open(
209                        new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY);
210            } else {
211                factoryFd = null;
212            }
213        } catch (FileNotFoundException e) {
214            factoryFd = null;
215        }
216
217        if (updateFd == null) {
218            if (factoryFd != null) {
219                return factoryFd;
220            } else {
221                throw new FileNotFoundException(
222                        String.format("No model file found for %s", locale));
223            }
224        }
225
226        final int updateFdInt = updateFd.getFd();
227        final boolean localeMatches = Objects.equals(
228                locale.getLanguage().trim().toLowerCase(),
229                SmartSelection.getLanguage(updateFdInt).trim().toLowerCase());
230        if (factoryFd == null) {
231            if (localeMatches) {
232                return updateFd;
233            } else {
234                closeAndLogError(updateFd);
235                throw new FileNotFoundException(
236                        String.format("No model file found for %s", locale));
237            }
238        }
239
240        if (!localeMatches) {
241            closeAndLogError(updateFd);
242            return factoryFd;
243        }
244
245        final int updateVersion = SmartSelection.getVersion(updateFdInt);
246        final int factoryVersion = SmartSelection.getVersion(factoryFd.getFd());
247        if (updateVersion > factoryVersion) {
248            closeAndLogError(factoryFd);
249            return updateFd;
250        } else {
251            closeAndLogError(updateFd);
252            return factoryFd;
253        }
254    }
255
256    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
257    private void destroySmartSelectionIfExistsLocked() {
258        if (mSmartSelection != null) {
259            mSmartSelection.close();
260            mSmartSelection = null;
261        }
262    }
263
264    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
265    @Nullable
266    private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
267        // Specified localeList takes priority over the system default, so it is listed first.
268        final String languages = localeList.isEmpty()
269                ? LocaleList.getDefault().toLanguageTags()
270                : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags();
271        final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
272
273        final List<Locale> supportedLocales =
274                new ArrayList<>(getFactoryModelFilePathsLocked().keySet());
275        final Locale updatedModelLocale = getUpdatedModelLocale();
276        if (updatedModelLocale != null) {
277            supportedLocales.add(updatedModelLocale);
278        }
279        return Locale.lookup(languageRangeList, supportedLocales);
280    }
281
282    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
283    private Map<Locale, String> getFactoryModelFilePathsLocked() {
284        if (mModelFilePaths == null) {
285            final Map<Locale, String> modelFilePaths = new HashMap<>();
286            final File modelsDir = new File(MODEL_DIR);
287            if (modelsDir.exists() && modelsDir.isDirectory()) {
288                final File[] models = modelsDir.listFiles();
289                final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
290                final int size = models.length;
291                for (int i = 0; i < size; i++) {
292                    final File modelFile = models[i];
293                    final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
294                    if (matcher.matches() && modelFile.isFile()) {
295                        final String language = matcher.group(1);
296                        final Locale locale = Locale.forLanguageTag(language);
297                        modelFilePaths.put(locale, modelFile.getAbsolutePath());
298                    }
299                }
300            }
301            mModelFilePaths = modelFilePaths;
302        }
303        return mModelFilePaths;
304    }
305
306    @Nullable
307    private Locale getUpdatedModelLocale() {
308        try {
309            final ParcelFileDescriptor updateFd = ParcelFileDescriptor.open(
310                    new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY);
311            final Locale locale = Locale.forLanguageTag(
312                    SmartSelection.getLanguage(updateFd.getFd()));
313            closeAndLogError(updateFd);
314            return locale;
315        } catch (FileNotFoundException e) {
316            return null;
317        }
318    }
319
320    private TextClassification createClassificationResult(
321            SmartSelection.ClassificationResult[] classifications, CharSequence text) {
322        final TextClassification.Builder builder = new TextClassification.Builder()
323                .setText(text.toString());
324
325        final int size = classifications.length;
326        for (int i = 0; i < size; i++) {
327            builder.setEntityType(classifications[i].mCollection, classifications[i].mScore);
328        }
329
330        final String type = getHighestScoringType(classifications);
331        builder.setLogType(IntentFactory.getLogType(type));
332
333        final Intent intent = IntentFactory.create(mContext, type, text.toString());
334        final PackageManager pm;
335        final ResolveInfo resolveInfo;
336        if (intent != null) {
337            pm = mContext.getPackageManager();
338            resolveInfo = pm.resolveActivity(intent, 0);
339        } else {
340            pm = null;
341            resolveInfo = null;
342        }
343        if (resolveInfo != null && resolveInfo.activityInfo != null) {
344            builder.setIntent(intent)
345                    .setOnClickListener(TextClassification.createStartActivityOnClickListener(
346                            mContext, intent));
347
348            final String packageName = resolveInfo.activityInfo.packageName;
349            if ("android".equals(packageName)) {
350                // Requires the chooser to find an activity to handle the intent.
351                builder.setLabel(IntentFactory.getLabel(mContext, type));
352            } else {
353                // A default activity will handle the intent.
354                intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
355                Drawable icon = resolveInfo.activityInfo.loadIcon(pm);
356                if (icon == null) {
357                    icon = resolveInfo.loadIcon(pm);
358                }
359                builder.setIcon(icon);
360                CharSequence label = resolveInfo.activityInfo.loadLabel(pm);
361                if (label == null) {
362                    label = resolveInfo.loadLabel(pm);
363                }
364                builder.setLabel(label != null ? label.toString() : null);
365            }
366        }
367        return builder.build();
368    }
369
370    private static int getHintFlags(CharSequence text, int start, int end) {
371        int flag = 0;
372        final CharSequence subText = text.subSequence(start, end);
373        if (Patterns.AUTOLINK_EMAIL_ADDRESS.matcher(subText).matches()) {
374            flag |= SmartSelection.HINT_FLAG_EMAIL;
375        }
376        if (Patterns.AUTOLINK_WEB_URL.matcher(subText).matches()
377                && Linkify.sUrlMatchFilter.acceptMatch(text, start, end)) {
378            flag |= SmartSelection.HINT_FLAG_URL;
379        }
380        // TODO: Added this log for debug only. Remove before release.
381        Log.d(LOG_TAG, String.format("Email hint: %b",
382                (flag & SmartSelection.HINT_FLAG_EMAIL) != 0));
383        Log.d(LOG_TAG, String.format("Url hint: %b",
384                (flag & SmartSelection.HINT_FLAG_URL) != 0));
385        return flag;
386    }
387
388    private static String getHighestScoringType(SmartSelection.ClassificationResult[] types) {
389        if (types.length < 1) {
390            return "";
391        }
392
393        String type = types[0].mCollection;
394        float highestScore = types[0].mScore;
395        final int size = types.length;
396        for (int i = 1; i < size; i++) {
397            if (types[i].mScore > highestScore) {
398                type = types[i].mCollection;
399                highestScore = types[i].mScore;
400            }
401        }
402        return type;
403    }
404
405    /**
406     * Closes the ParcelFileDescriptor and logs any errors that occur.
407     */
408    private static void closeAndLogError(ParcelFileDescriptor fd) {
409        try {
410            fd.close();
411        } catch (IOException e) {
412            Log.e(LOG_TAG, "Error closing file.", e);
413        }
414    }
415
416    /**
417     * @throws IllegalArgumentException if text is null; startIndex is negative;
418     *      endIndex is greater than text.length() or is not greater than startIndex
419     */
420    private static void validateInput(@NonNull CharSequence text, int startIndex, int endIndex) {
421        Preconditions.checkArgument(text != null);
422        Preconditions.checkArgument(startIndex >= 0);
423        Preconditions.checkArgument(endIndex <= text.length());
424        Preconditions.checkArgument(endIndex > startIndex);
425    }
426
427    /**
428     * Detects and creates links for specified text.
429     */
430    private static final class LinksInfoFactory {
431
432        private LinksInfoFactory() {}
433
434        public static LinksInfo create(
435                Context context, SmartSelection smartSelection, String text, int linkMask) {
436            final WordIterator wordIterator = new WordIterator();
437            wordIterator.setCharSequence(text, 0, text.length());
438            final List<SpanSpec> spans = new ArrayList<>();
439            int start = 0;
440            int end;
441            while ((end = wordIterator.nextBoundary(start)) != BreakIterator.DONE) {
442                final String token = text.substring(start, end);
443                if (TextUtils.isEmpty(token)) {
444                    continue;
445                }
446
447                final int[] selection = smartSelection.suggest(text, start, end);
448                final int selectionStart = selection[0];
449                final int selectionEnd = selection[1];
450                if (selectionStart >= 0 && selectionEnd <= text.length()
451                        && selectionStart <= selectionEnd) {
452                    final SmartSelection.ClassificationResult[] results =
453                            smartSelection.classifyText(
454                                    text, selectionStart, selectionEnd,
455                                    getHintFlags(text, selectionStart, selectionEnd));
456                    if (results.length > 0) {
457                        final String type = getHighestScoringType(results);
458                        if (matches(type, linkMask)) {
459                            final Intent intent = IntentFactory.create(
460                                    context, type, text.substring(selectionStart, selectionEnd));
461                            if (hasActivityHandler(context, intent)) {
462                                final ClickableSpan span = createSpan(context, intent);
463                                spans.add(new SpanSpec(selectionStart, selectionEnd, span));
464                            }
465                        }
466                    }
467                }
468                start = end;
469            }
470            return new LinksInfoImpl(text, avoidOverlaps(spans, text));
471        }
472
473        /**
474         * Returns true if the classification type matches the specified linkMask.
475         */
476        private static boolean matches(String type, int linkMask) {
477            type = type.trim().toLowerCase(Locale.ENGLISH);
478            if ((linkMask & Linkify.PHONE_NUMBERS) != 0
479                    && TextClassifier.TYPE_PHONE.equals(type)) {
480                return true;
481            }
482            if ((linkMask & Linkify.EMAIL_ADDRESSES) != 0
483                    && TextClassifier.TYPE_EMAIL.equals(type)) {
484                return true;
485            }
486            if ((linkMask & Linkify.MAP_ADDRESSES) != 0
487                    && TextClassifier.TYPE_ADDRESS.equals(type)) {
488                return true;
489            }
490            if ((linkMask & Linkify.WEB_URLS) != 0
491                    && TextClassifier.TYPE_URL.equals(type)) {
492                return true;
493            }
494            return false;
495        }
496
497        /**
498         * Trim the number of spans so that no two spans overlap.
499         *
500         * This algorithm first ensures that there is only one span per start index, then it
501         * makes sure that no two spans overlap.
502         */
503        private static List<SpanSpec> avoidOverlaps(List<SpanSpec> spans, String text) {
504            Collections.sort(spans, Comparator.comparingInt(span -> span.mStart));
505            // Group spans by start index. Take the longest span.
506            final Map<Integer, SpanSpec> reps = new LinkedHashMap<>();  // order matters.
507            final int size = spans.size();
508            for (int i = 0; i < size; i++) {
509                final SpanSpec span = spans.get(i);
510                final LinksInfoFactory.SpanSpec rep = reps.get(span.mStart);
511                if (rep == null || rep.mEnd < span.mEnd) {
512                    reps.put(span.mStart, span);
513                }
514            }
515            // Avoid span intersections. Take the longer span.
516            final LinkedList<SpanSpec> result = new LinkedList<>();
517            for (SpanSpec rep : reps.values()) {
518                if (result.isEmpty()) {
519                    result.add(rep);
520                    continue;
521                }
522
523                final SpanSpec last = result.getLast();
524                if (rep.mStart < last.mEnd) {
525                    // Spans intersect. Use the one with characters.
526                    if ((rep.mEnd - rep.mStart) > (last.mEnd - last.mStart)) {
527                        result.set(result.size() - 1, rep);
528                    }
529                } else {
530                    result.add(rep);
531                }
532            }
533            return result;
534        }
535
536        private static ClickableSpan createSpan(final Context context, final Intent intent) {
537            return new ClickableSpan() {
538                // TODO: Style this span.
539                @Override
540                public void onClick(View widget) {
541                    context.startActivity(intent);
542                }
543            };
544        }
545
546        private static boolean hasActivityHandler(Context context, @Nullable Intent intent) {
547            if (intent == null) {
548                return false;
549            }
550            final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
551            return resolveInfo != null && resolveInfo.activityInfo != null;
552        }
553
554        /**
555         * Implementation of LinksInfo that adds ClickableSpans to the specified text.
556         */
557        private static final class LinksInfoImpl implements LinksInfo {
558
559            private final CharSequence mOriginalText;
560            private final List<SpanSpec> mSpans;
561
562            LinksInfoImpl(CharSequence originalText, List<SpanSpec> spans) {
563                mOriginalText = originalText;
564                mSpans = spans;
565            }
566
567            @Override
568            public boolean apply(@NonNull CharSequence text) {
569                Preconditions.checkArgument(text != null);
570                if (text instanceof Spannable && mOriginalText.toString().equals(text.toString())) {
571                    Spannable spannable = (Spannable) text;
572                    final int size = mSpans.size();
573                    for (int i = 0; i < size; i++) {
574                        final SpanSpec span = mSpans.get(i);
575                        spannable.setSpan(span.mSpan, span.mStart, span.mEnd, 0);
576                    }
577                    return true;
578                }
579                return false;
580            }
581        }
582
583        /**
584         * Span plus its start and end index.
585         */
586        private static final class SpanSpec {
587
588            private final int mStart;
589            private final int mEnd;
590            private final ClickableSpan mSpan;
591
592            SpanSpec(int start, int end, ClickableSpan span) {
593                mStart = start;
594                mEnd = end;
595                mSpan = span;
596            }
597        }
598    }
599
600    /**
601     * Creates intents based on the classification type.
602     */
603    private static final class IntentFactory {
604
605        private IntentFactory() {}
606
607        @Nullable
608        public static Intent create(Context context, String type, String text) {
609            type = type.trim().toLowerCase(Locale.ENGLISH);
610            text = text.trim();
611            switch (type) {
612                case TextClassifier.TYPE_EMAIL:
613                    return new Intent(Intent.ACTION_SENDTO)
614                            .setData(Uri.parse(String.format("mailto:%s", text)));
615                case TextClassifier.TYPE_PHONE:
616                    return new Intent(Intent.ACTION_DIAL)
617                            .setData(Uri.parse(String.format("tel:%s", text)));
618                case TextClassifier.TYPE_ADDRESS:
619                    return new Intent(Intent.ACTION_VIEW)
620                            .setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
621                case TextClassifier.TYPE_URL:
622                    if (!text.startsWith("https://") && !text.startsWith("http://")) {
623                        text = "http://" + text;
624                    }
625                    return new Intent(Intent.ACTION_VIEW, Uri.parse(text))
626                            .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
627                default:
628                    return null;
629            }
630        }
631
632        @Nullable
633        public static String getLabel(Context context, String type) {
634            type = type.trim().toLowerCase(Locale.ENGLISH);
635            switch (type) {
636                case TextClassifier.TYPE_EMAIL:
637                    return context.getString(com.android.internal.R.string.email);
638                case TextClassifier.TYPE_PHONE:
639                    return context.getString(com.android.internal.R.string.dial);
640                case TextClassifier.TYPE_ADDRESS:
641                    return context.getString(com.android.internal.R.string.map);
642                case TextClassifier.TYPE_URL:
643                    return context.getString(com.android.internal.R.string.browse);
644                default:
645                    return null;
646            }
647        }
648
649        @Nullable
650        public static int getLogType(String type) {
651            type = type.trim().toLowerCase(Locale.ENGLISH);
652            switch (type) {
653                case TextClassifier.TYPE_EMAIL:
654                    return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_EMAIL;
655                case TextClassifier.TYPE_PHONE:
656                    return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_PHONE;
657                case TextClassifier.TYPE_ADDRESS:
658                    return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_ADDRESS;
659                case TextClassifier.TYPE_URL:
660                    return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_URL;
661                default:
662                    return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_OTHER;
663            }
664        }
665    }
666}
667