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