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