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