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