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