1 /* 2 * Copyright (C) 2019 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 com.android.car.telephony.common; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.icu.text.Collator; 22 import android.net.Uri; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.provider.ContactsContract; 26 import android.text.TextUtils; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 31 import com.android.car.apps.common.log.L; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Encapsulates data about a phone Contact entry. Typically loaded from the local Contact store. 38 */ 39 public class Contact implements Parcelable, Comparable<Contact> { 40 private static final String TAG = "CD.Contact"; 41 42 /** 43 * Column name for phonebook label column. 44 */ 45 private static final String PHONEBOOK_LABEL = "phonebook_label"; 46 /** 47 * Column name for alternative phonebook label column. 48 */ 49 private static final String PHONEBOOK_LABEL_ALT = "phonebook_label_alt"; 50 51 /** 52 * Contact belongs to TYPE_LETTER if its display name starts with a letter 53 */ 54 private static final int TYPE_LETTER = 1; 55 /** 56 * Contact belongs to TYPE_DIGIT if its display name starts with a digit 57 */ 58 private static final int TYPE_DIGIT = 2; 59 /** 60 * Contact belongs to TYPE_OTHER if it does not belong to TYPE_LETTER or TYPE_DIGIT Such as 61 * empty display name or the display name starts with "_" 62 */ 63 private static final int TYPE_OTHER = 3; 64 65 /** 66 * A reference to the {@link ContactsContract.RawContacts#CONTACT_ID}. 67 */ 68 private long mContactId; 69 70 /** 71 * A reference to the {@link ContactsContract.Data#RAW_CONTACT_ID}. 72 */ 73 private long mRawContactId; 74 75 /** 76 * The name of the account instance to which this row belongs, which identifies a specific 77 * account. See {@link ContactsContract.RawContacts#ACCOUNT_NAME}. 78 */ 79 private String mAccountName; 80 81 /** 82 * The display name. 83 * <p> 84 * The standard text shown as the contact's display name, based on the best available 85 * information for the contact. 86 * </p> 87 * <p> 88 * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME}. 89 */ 90 private String mDisplayName; 91 92 /** 93 * The alternative display name. 94 * <p> 95 * An alternative representation of the display name, such as "family name first" instead of 96 * "given name first" for Western names. If an alternative is not available, the values should 97 * be the same as {@link #mDisplayName}. 98 * </p> 99 * <p> 100 * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME_ALTERNATIVE}. 101 */ 102 private String mDisplayNameAlt; 103 104 /** 105 * The given name for the contact. See 106 * {@link ContactsContract.CommonDataKinds.StructuredName#GIVEN_NAME}. 107 */ 108 private String mGivenName; 109 110 /** 111 * The family name for the contact. See 112 * {@link ContactsContract.CommonDataKinds.StructuredName#FAMILY_NAME}. 113 */ 114 private String mFamilyName; 115 116 /** 117 * The initials of the contact's name. 118 */ 119 private String mInitials; 120 121 /** 122 * The phonebook label. 123 * <p> 124 * For {@link #mDisplayName}s starting with letters, label will be the first character of {@link 125 * #mDisplayName}. For {@link #mDisplayName}s starting with numbers, the label will be "#". For 126 * {@link #mDisplayName}s starting with other characters, the label will be "...". 127 * </p> 128 */ 129 private String mPhoneBookLabel; 130 131 /** 132 * The alternative phonebook label. 133 * <p> 134 * It is similar with {@link #mPhoneBookLabel}. But instead of generating from {@link 135 * #mDisplayName}, it will use {@link #mDisplayNameAlt}. 136 * </p> 137 */ 138 private String mPhoneBookLabelAlt; 139 140 /** 141 * Sort key that takes into account locale-based traditions for sorting names in address books. 142 * <p> 143 * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_PRIMARY}. 144 */ 145 private String mSortKeyPrimary; 146 147 /** 148 * Sort key based on the alternative representation of the full name. 149 * <p> 150 * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_ALTERNATIVE}. 151 */ 152 private String mSortKeyAlt; 153 154 /** 155 * An opaque value that contains hints on how to find the contact if its row id changed as a 156 * result of a sync or aggregation. If a contact has multiple phone numbers, all phone numbers 157 * are recorded in a single entry and they all have the same look up key in a single load. See 158 * {@link ContactsContract.Data#LOOKUP_KEY}. 159 */ 160 private String mLookupKey; 161 162 /** 163 * A URI that can be used to retrieve a thumbnail of the contact's photo. 164 */ 165 @Nullable 166 private Uri mAvatarThumbnailUri; 167 168 /** 169 * A URI that can be used to retrieve the contact's full-size photo. 170 */ 171 @Nullable 172 private Uri mAvatarUri; 173 174 /** 175 * Whether this contact entry is starred by user. 176 */ 177 private boolean mIsStarred; 178 179 /** 180 * Contact-specific information about whether or not a contact has been pinned by the user at a 181 * particular position within the system contact application's user interface. 182 */ 183 private int mPinnedPosition; 184 185 /** 186 * This contact's primary phone number. Its value is null if a primary phone number is not set. 187 */ 188 @Nullable 189 private PhoneNumber mPrimaryPhoneNumber; 190 191 /** 192 * Whether this contact represents a voice mail. 193 */ 194 private boolean mIsVoiceMail; 195 196 /** 197 * All phone numbers of this contact mapping to the unique primary key for the raw data entry. 198 */ 199 private final List<PhoneNumber> mPhoneNumbers = new ArrayList<>(); 200 201 /** 202 * All postal addresses of this contact mapping to the unique primary key for the raw data 203 * entry 204 */ 205 private final List<PostalAddress> mPostalAddresses = new ArrayList<>(); 206 207 /** 208 * Parses a contact entry for a Cursor loaded from the Contact Database. A new contact will be 209 * created and returned. 210 */ fromCursor(Context context, Cursor cursor)211 public static Contact fromCursor(Context context, Cursor cursor) { 212 return fromCursor(context, cursor, null); 213 } 214 215 /** 216 * Parses a contact entry for a Cursor loaded from the Contact Database. 217 * 218 * @param contact should have the same {@link #mLookupKey} and {@link #mAccountName} with the 219 * data read from the cursor, so all the data from the cursor can be loaded into 220 * this contact. If either of their {@link #mLookupKey} and {@link #mAccountName} 221 * is not the same or this contact is null, a new contact will be created and 222 * returned. 223 */ fromCursor(Context context, Cursor cursor, @Nullable Contact contact)224 public static Contact fromCursor(Context context, Cursor cursor, @Nullable Contact contact) { 225 int accountNameColumn = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME); 226 int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY); 227 String accountName = cursor.getString(accountNameColumn); 228 String lookupKey = cursor.getString(lookupKeyColumn); 229 230 if (contact == null) { 231 contact = new Contact(); 232 contact.loadBasicInfo(cursor); 233 } 234 235 if (!TextUtils.equals(accountName, contact.mAccountName) 236 || !TextUtils.equals(lookupKey, contact.mLookupKey)) { 237 L.w(TAG, "A wrong contact is passed in. A new contact will be created."); 238 contact = new Contact(); 239 contact.loadBasicInfo(cursor); 240 } 241 242 int mimetypeColumn = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE); 243 String mimeType = cursor.getString(mimetypeColumn); 244 245 // More mimeType can be added here if more types of data needs to be loaded. 246 switch (mimeType) { 247 case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE: 248 contact.loadNameDetails(cursor); 249 break; 250 case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE: 251 contact.addPhoneNumber(context, cursor); 252 break; 253 case ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE: 254 contact.addPostalAddress(cursor); 255 break; 256 default: 257 L.d(TAG, String.format("This mimetype %s will not be loaded right now.", mimeType)); 258 } 259 260 return contact; 261 } 262 263 /** 264 * The data columns that are the same in every cursor no matter what the mimetype is will be 265 * loaded here. 266 */ loadBasicInfo(Cursor cursor)267 private void loadBasicInfo(Cursor cursor) { 268 int contactIdColumn = cursor.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID); 269 int rawContactIdColumn = cursor.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID); 270 int accountNameColumn = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME); 271 int displayNameColumn = cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME); 272 int displayNameAltColumn = cursor.getColumnIndex( 273 ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE); 274 int phoneBookLabelColumn = cursor.getColumnIndex(PHONEBOOK_LABEL); 275 int phoneBookLabelAltColumn = cursor.getColumnIndex(PHONEBOOK_LABEL_ALT); 276 int sortKeyPrimaryColumn = cursor.getColumnIndex( 277 ContactsContract.RawContacts.SORT_KEY_PRIMARY); 278 int sortKeyAltColumn = cursor.getColumnIndex( 279 ContactsContract.RawContacts.SORT_KEY_ALTERNATIVE); 280 int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY); 281 282 int avatarUriColumn = cursor.getColumnIndex(ContactsContract.Data.PHOTO_URI); 283 int avatarThumbnailColumn = cursor.getColumnIndex( 284 ContactsContract.Data.PHOTO_THUMBNAIL_URI); 285 int starredColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.STARRED); 286 int pinnedColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PINNED); 287 288 mContactId = cursor.getLong(contactIdColumn); 289 mRawContactId = cursor.getLong(rawContactIdColumn); 290 mAccountName = cursor.getString(accountNameColumn); 291 mDisplayName = cursor.getString(displayNameColumn); 292 mDisplayNameAlt = cursor.getString(displayNameAltColumn); 293 mSortKeyPrimary = cursor.getString(sortKeyPrimaryColumn); 294 mSortKeyAlt = cursor.getString(sortKeyAltColumn); 295 mPhoneBookLabel = cursor.getString(phoneBookLabelColumn); 296 mPhoneBookLabelAlt = cursor.getString(phoneBookLabelAltColumn); 297 mLookupKey = cursor.getString(lookupKeyColumn); 298 299 String avatarUriStr = cursor.getString(avatarUriColumn); 300 mAvatarUri = avatarUriStr == null ? null : Uri.parse(avatarUriStr); 301 String avatarThumbnailStringUri = cursor.getString(avatarThumbnailColumn); 302 mAvatarThumbnailUri = avatarThumbnailStringUri == null ? null : Uri.parse( 303 avatarThumbnailStringUri); 304 305 mIsStarred = cursor.getInt(starredColumn) > 0; 306 mPinnedPosition = cursor.getInt(pinnedColumn); 307 } 308 309 /** 310 * Loads the data whose mimetype is 311 * {@link ContactsContract.CommonDataKinds.StructuredName#CONTENT_ITEM_TYPE}. 312 */ loadNameDetails(Cursor cursor)313 private void loadNameDetails(Cursor cursor) { 314 int firstNameColumn = cursor.getColumnIndex( 315 ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME); 316 int lastNameColumn = cursor.getColumnIndex( 317 ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME); 318 319 mGivenName = cursor.getString(firstNameColumn); 320 mFamilyName = cursor.getString(lastNameColumn); 321 } 322 323 /** 324 * Loads the data whose mimetype is 325 * {@link ContactsContract.CommonDataKinds.Phone#CONTENT_ITEM_TYPE}. 326 */ addPhoneNumber(Context context, Cursor cursor)327 private void addPhoneNumber(Context context, Cursor cursor) { 328 PhoneNumber newNumber = PhoneNumber.fromCursor(context, cursor); 329 330 boolean hasSameNumber = false; 331 for (PhoneNumber number : mPhoneNumbers) { 332 if (newNumber.equals(number)) { 333 hasSameNumber = true; 334 number.merge(newNumber); 335 } 336 } 337 338 if (!hasSameNumber) { 339 mPhoneNumbers.add(newNumber); 340 } 341 342 if (newNumber.isPrimary()) { 343 mPrimaryPhoneNumber = newNumber.merge(mPrimaryPhoneNumber); 344 } 345 346 // TODO: update voice mail number part when start to support voice mail. 347 if (TelecomUtils.isVoicemailNumber(context, newNumber.getNumber())) { 348 mIsVoiceMail = true; 349 } 350 } 351 352 /** 353 * Loads the data whose mimetype is 354 * {@link ContactsContract.CommonDataKinds.StructuredPostal#CONTENT_ITEM_TYPE}. 355 */ addPostalAddress(Cursor cursor)356 private void addPostalAddress(Cursor cursor) { 357 PostalAddress newAddress = PostalAddress.fromCursor(cursor); 358 359 if (!mPostalAddresses.contains(newAddress)) { 360 mPostalAddresses.add(newAddress); 361 } 362 } 363 364 @Override equals(Object obj)365 public boolean equals(Object obj) { 366 return obj instanceof Contact && mLookupKey.equals(((Contact) obj).mLookupKey) 367 && mAccountName.equals(((Contact) obj).mAccountName); 368 } 369 370 @Override hashCode()371 public int hashCode() { 372 return mLookupKey.hashCode(); 373 } 374 375 @Override toString()376 public String toString() { 377 return mDisplayName + mPhoneNumbers; 378 } 379 380 /** 381 * Returns the aggregated contact id. 382 */ getId()383 public long getId() { 384 return mContactId; 385 } 386 387 /** 388 * Returns the raw contact id. 389 */ getRawContactId()390 public long getRawContactId() { 391 return mRawContactId; 392 } 393 394 /** 395 * Returns a lookup uri using {@link #mContactId} and {@link #mLookupKey}. Returns null if 396 * unable to get a valid lookup URI from the provided parameters. See {@link 397 * ContactsContract.Contacts#getLookupUri(long, String)}. 398 */ 399 @Nullable getLookupUri()400 public Uri getLookupUri() { 401 return ContactsContract.Contacts.getLookupUri(mContactId, mLookupKey); 402 } 403 404 /** 405 * Returns {@link #mAccountName}. 406 */ getAccountName()407 public String getAccountName() { 408 return mAccountName; 409 } 410 411 /** 412 * Returns {@link #mDisplayName}. 413 */ getDisplayName()414 public String getDisplayName() { 415 return mDisplayName; 416 } 417 418 /** 419 * Returns {@link #mDisplayNameAlt}. 420 */ getDisplayNameAlt()421 public String getDisplayNameAlt() { 422 return mDisplayNameAlt; 423 } 424 425 /** 426 * Returns {@link #mGivenName}. 427 */ getGivenName()428 public String getGivenName() { 429 return mGivenName; 430 } 431 432 /** 433 * Returns {@link #mFamilyName}. 434 */ getFamilyName()435 public String getFamilyName() { 436 return mFamilyName; 437 } 438 439 /** 440 * Returns the initials of the contact's name. 441 */ 442 //TODO: update how to get initials after refactoring. Could use last name and first name to 443 // get initials after refactoring to avoid error for those names with prefix. getInitials()444 public String getInitials() { 445 if (mInitials == null) { 446 mInitials = TelecomUtils.getInitials(mDisplayName, mDisplayNameAlt); 447 } 448 449 return mInitials; 450 } 451 452 /** 453 * Returns {@link #mPhoneBookLabel} 454 */ getPhonebookLabel()455 public String getPhonebookLabel() { 456 return mPhoneBookLabel; 457 } 458 459 /** 460 * Returns {@link #mPhoneBookLabelAlt} 461 */ getPhonebookLabelAlt()462 public String getPhonebookLabelAlt() { 463 return mPhoneBookLabelAlt; 464 } 465 466 /** 467 * Returns {@link #mLookupKey}. 468 */ getLookupKey()469 public String getLookupKey() { 470 return mLookupKey; 471 } 472 473 /** 474 * Returns the Uri for avatar. 475 */ 476 @Nullable getAvatarUri()477 public Uri getAvatarUri() { 478 return mAvatarUri != null ? mAvatarUri : mAvatarThumbnailUri; 479 } 480 481 /** 482 * Return all phone numbers associated with this contact. 483 */ getNumbers()484 public List<PhoneNumber> getNumbers() { 485 return mPhoneNumbers; 486 } 487 488 /** 489 * Return all postal addresses associated with this contact. 490 */ getPostalAddresses()491 public List<PostalAddress> getPostalAddresses() { 492 return mPostalAddresses; 493 } 494 495 /** 496 * Returns if this Contact represents a voice mail number. 497 */ isVoicemail()498 public boolean isVoicemail() { 499 return mIsVoiceMail; 500 } 501 502 /** 503 * Returns if this contact has a primary phone number. 504 */ hasPrimaryPhoneNumber()505 public boolean hasPrimaryPhoneNumber() { 506 return mPrimaryPhoneNumber != null; 507 } 508 509 /** 510 * Returns the primary phone number for this Contact. Returns null if there is not one. 511 */ 512 @Nullable getPrimaryPhoneNumber()513 public PhoneNumber getPrimaryPhoneNumber() { 514 return mPrimaryPhoneNumber; 515 } 516 517 /** 518 * Returns if this Contact is starred. 519 */ isStarred()520 public boolean isStarred() { 521 return mIsStarred; 522 } 523 524 /** 525 * Returns {@link #mPinnedPosition}. 526 */ getPinnedPosition()527 public int getPinnedPosition() { 528 return mPinnedPosition; 529 } 530 531 /** 532 * Looks up a {@link PhoneNumber} of this contact for the given phone number string. Returns 533 * {@code null} if this contact doesn't contain the given phone number. 534 */ 535 @Nullable getPhoneNumber(Context context, String number)536 public PhoneNumber getPhoneNumber(Context context, String number) { 537 I18nPhoneNumberWrapper i18nPhoneNumber = I18nPhoneNumberWrapper.Factory.INSTANCE.get( 538 context, number); 539 for (PhoneNumber phoneNumber : mPhoneNumbers) { 540 if (phoneNumber.getI18nPhoneNumberWrapper().equals(i18nPhoneNumber)) { 541 return phoneNumber; 542 } 543 } 544 return null; 545 } 546 547 @Override describeContents()548 public int describeContents() { 549 return 0; 550 } 551 552 @Override writeToParcel(Parcel dest, int flags)553 public void writeToParcel(Parcel dest, int flags) { 554 dest.writeLong(mContactId); 555 dest.writeLong(mRawContactId); 556 dest.writeString(mLookupKey); 557 dest.writeString(mAccountName); 558 dest.writeString(mDisplayName); 559 dest.writeString(mDisplayNameAlt); 560 dest.writeString(mSortKeyPrimary); 561 dest.writeString(mSortKeyAlt); 562 dest.writeString(mPhoneBookLabel); 563 dest.writeString(mPhoneBookLabelAlt); 564 dest.writeParcelable(mAvatarThumbnailUri, 0); 565 dest.writeParcelable(mAvatarUri, 0); 566 dest.writeBoolean(mIsStarred); 567 dest.writeInt(mPinnedPosition); 568 569 dest.writeBoolean(mIsVoiceMail); 570 dest.writeParcelable(mPrimaryPhoneNumber, flags); 571 dest.writeInt(mPhoneNumbers.size()); 572 for (PhoneNumber phoneNumber : mPhoneNumbers) { 573 dest.writeParcelable(phoneNumber, flags); 574 } 575 576 dest.writeInt(mPostalAddresses.size()); 577 for (PostalAddress postalAddress : mPostalAddresses) { 578 dest.writeParcelable(postalAddress, flags); 579 } 580 } 581 582 public static final Creator<Contact> CREATOR = new Creator<Contact>() { 583 @Override 584 public Contact createFromParcel(Parcel source) { 585 return Contact.fromParcel(source); 586 } 587 588 @Override 589 public Contact[] newArray(int size) { 590 return new Contact[size]; 591 } 592 }; 593 594 /** 595 * Create {@link Contact} object from saved parcelable. 596 */ fromParcel(Parcel source)597 private static Contact fromParcel(Parcel source) { 598 Contact contact = new Contact(); 599 contact.mContactId = source.readLong(); 600 contact.mRawContactId = source.readLong(); 601 contact.mLookupKey = source.readString(); 602 contact.mAccountName = source.readString(); 603 contact.mDisplayName = source.readString(); 604 contact.mDisplayNameAlt = source.readString(); 605 contact.mSortKeyPrimary = source.readString(); 606 contact.mSortKeyAlt = source.readString(); 607 contact.mPhoneBookLabel = source.readString(); 608 contact.mPhoneBookLabelAlt = source.readString(); 609 contact.mAvatarThumbnailUri = source.readParcelable(Uri.class.getClassLoader()); 610 contact.mAvatarUri = source.readParcelable(Uri.class.getClassLoader()); 611 contact.mIsStarred = source.readBoolean(); 612 contact.mPinnedPosition = source.readInt(); 613 614 contact.mIsVoiceMail = source.readBoolean(); 615 contact.mPrimaryPhoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader()); 616 int phoneNumberListLength = source.readInt(); 617 for (int i = 0; i < phoneNumberListLength; i++) { 618 PhoneNumber phoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader()); 619 contact.mPhoneNumbers.add(phoneNumber); 620 if (phoneNumber.isPrimary()) { 621 contact.mPrimaryPhoneNumber = phoneNumber; 622 } 623 } 624 625 int postalAddressListLength = source.readInt(); 626 for (int i = 0; i < postalAddressListLength; i++) { 627 PostalAddress address = source.readParcelable(PostalAddress.class.getClassLoader()); 628 contact.mPostalAddresses.add(address); 629 } 630 631 return contact; 632 } 633 634 @Override compareTo(Contact otherContact)635 public int compareTo(Contact otherContact) { 636 // Use a helper function to classify Contacts 637 // and by default, it should be compared by first name order. 638 return compareBySortKeyPrimary(otherContact); 639 } 640 641 /** 642 * Compares contacts by their {@link #mSortKeyPrimary} in an order of letters, numbers, then 643 * special characters. 644 */ compareBySortKeyPrimary(@onNull Contact otherContact)645 public int compareBySortKeyPrimary(@NonNull Contact otherContact) { 646 return compareNames(mSortKeyPrimary, otherContact.mSortKeyPrimary, 647 mPhoneBookLabel, otherContact.getPhonebookLabel()); 648 } 649 650 /** 651 * Compares contacts by their {@link #mSortKeyAlt} in an order of letters, numbers, then special 652 * characters. 653 */ compareBySortKeyAlt(@onNull Contact otherContact)654 public int compareBySortKeyAlt(@NonNull Contact otherContact) { 655 return compareNames(mSortKeyAlt, otherContact.mSortKeyAlt, 656 mPhoneBookLabelAlt, otherContact.getPhonebookLabelAlt()); 657 } 658 659 /** 660 * Compares two strings in an order of letters, numbers, then special characters. 661 */ compareNames(String name, String otherName, String label, String otherLabel)662 private int compareNames(String name, String otherName, String label, String otherLabel) { 663 int type = getNameType(label); 664 int otherType = getNameType(otherLabel); 665 if (type != otherType) { 666 return Integer.compare(type, otherType); 667 } 668 Collator collator = Collator.getInstance(); 669 return collator.compare(name == null ? "" : name, otherName == null ? "" : otherName); 670 } 671 672 /** 673 * Returns the type of the name string. Types can be {@link #TYPE_LETTER}, {@link #TYPE_DIGIT} 674 * and {@link #TYPE_OTHER}. 675 */ getNameType(String label)676 private static int getNameType(String label) { 677 // A helper function to classify Contacts 678 if (!TextUtils.isEmpty(label)) { 679 if (Character.isLetter(label.charAt(0))) { 680 return TYPE_LETTER; 681 } 682 if (label.contains("#")) { 683 return TYPE_DIGIT; 684 } 685 } 686 return TYPE_OTHER; 687 } 688 } 689