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