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 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 35 /** 36 * Encapsulates data about a phone Contact entry. Typically loaded from the local Contact store. 37 */ 38 public class Contact implements Parcelable, Comparable<Contact> { 39 private static final String TAG = "CD.Contact"; 40 41 /** 42 * Column name for phonebook label column. 43 */ 44 private static final String PHONEBOOK_LABEL = "phonebook_label"; 45 /** 46 * Column name for alternative phonebook label column. 47 */ 48 private static final String PHONEBOOK_LABEL_ALT = "phonebook_label_alt"; 49 50 /** 51 * Contact belongs to TYPE_LETTER if its display name starts with a letter 52 */ 53 private static final int TYPE_LETTER = 1; 54 /** 55 * Contact belongs to TYPE_DIGIT if its display name starts with a digit 56 */ 57 private static final int TYPE_DIGIT = 2; 58 /** 59 * Contact belongs to TYPE_OTHER if it does not belong to TYPE_LETTER or TYPE_DIGIT Such as 60 * empty display name or the display name starts with "_" 61 */ 62 private static final int TYPE_OTHER = 3; 63 64 /** 65 * A reference to the {@link ContactsContract.RawContacts#CONTACT_ID}. 66 */ 67 private long mContactId; 68 69 /** 70 * A reference to the {@link ContactsContract.Data#RAW_CONTACT_ID}. 71 */ 72 private long mRawContactId; 73 74 /** 75 * The name of the account instance to which this row belongs, which identifies a specific 76 * account. See {@link ContactsContract.RawContacts#ACCOUNT_NAME}. 77 */ 78 private String mAccountName; 79 80 /** 81 * The display name. 82 * <p> 83 * The standard text shown as the contact's display name, based on the best available 84 * information for the contact. 85 * </p> 86 * <p> 87 * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME}. 88 */ 89 private String mDisplayName; 90 91 /** 92 * The alternative display name. 93 * <p> 94 * An alternative representation of the display name, such as "family name first" instead of 95 * "given name first" for Western names. If an alternative is not available, the values should 96 * be the same as {@link #mDisplayName}. 97 * </p> 98 * <p> 99 * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME_ALTERNATIVE}. 100 */ 101 private String mDisplayNameAlt; 102 103 /** 104 * The given name for the contact. See 105 * {@link ContactsContract.CommonDataKinds.StructuredName#GIVEN_NAME}. 106 */ 107 private String mGivenName; 108 109 /** 110 * The family name for the contact. See 111 * {@link ContactsContract.CommonDataKinds.StructuredName#FAMILY_NAME}. 112 */ 113 private String mFamilyName; 114 115 /** 116 * The initials of the contact's name. 117 */ 118 private String mInitials; 119 120 /** 121 * The phonebook label. 122 * <p> 123 * For {@link #mDisplayName}s starting with letters, label will be the first character of {@link 124 * #mDisplayName}. For {@link #mDisplayName}s starting with numbers, the label will be "#". For 125 * {@link #mDisplayName}s starting with other characters, the label will be "...". 126 * </p> 127 */ 128 private String mPhoneBookLabel; 129 130 /** 131 * The alternative phonebook label. 132 * <p> 133 * It is similar with {@link #mPhoneBookLabel}. But instead of generating from {@link 134 * #mDisplayName}, it will use {@link #mDisplayNameAlt}. 135 * </p> 136 */ 137 private String mPhoneBookLabelAlt; 138 139 /** 140 * Sort key that takes into account locale-based traditions for sorting names in address books. 141 * <p> 142 * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_PRIMARY}. 143 */ 144 private String mSortKeyPrimary; 145 146 /** 147 * Sort key based on the alternative representation of the full name. 148 * <p> 149 * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_ALTERNATIVE}. 150 */ 151 private String mSortKeyAlt; 152 153 /** 154 * An opaque value that contains hints on how to find the contact if its row id changed as a 155 * result of a sync or aggregation. If a contact has multiple phone numbers, all phone numbers 156 * are recorded in a single entry and they all have the same look up key in a single load. See 157 * {@link ContactsContract.Data#LOOKUP_KEY}. 158 */ 159 private String mLookupKey; 160 161 /** 162 * A URI that can be used to retrieve a thumbnail of the contact's photo. 163 */ 164 @Nullable 165 private Uri mAvatarThumbnailUri; 166 167 /** 168 * A URI that can be used to retrieve the contact's full-size photo. 169 */ 170 @Nullable 171 private Uri mAvatarUri; 172 173 /** 174 * Whether this contact entry is starred by user. 175 */ 176 private boolean mIsStarred; 177 178 /** 179 * Contact-specific information about whether or not a contact has been pinned by the user at a 180 * particular position within the system contact application's user interface. 181 */ 182 private int mPinnedPosition; 183 184 /** 185 * This contact's primary phone number. Its value is null if a primary phone number is not set. 186 */ 187 @Nullable 188 private PhoneNumber mPrimaryPhoneNumber; 189 190 /** 191 * Whether this contact represents a voice mail. 192 */ 193 private boolean mIsVoiceMail; 194 195 /** 196 * All phone numbers of this contact mapping to the unique primary key for the raw data entry. 197 */ 198 private final List<PhoneNumber> mPhoneNumbers = new ArrayList<>(); 199 200 /** 201 * All postal addresses of this contact mapping to the unique primary key for the raw data 202 * entry 203 */ 204 private final List<PostalAddress> mPostalAddresses = new ArrayList<>(); 205 206 /** 207 * Parses a contact entry for a Cursor loaded from the Contact Database. A new contact will be 208 * created and returned. 209 */ fromCursor(Context context, Cursor cursor)210 public static Contact fromCursor(Context context, Cursor cursor) { 211 return fromCursor(context, cursor, null); 212 } 213 214 /** 215 * Parses a contact entry for a Cursor loaded from the Contact Database. 216 * 217 * @param contact should have the same {@link #mLookupKey} and {@link #mAccountName} with the 218 * data read from the cursor, so all the data from the cursor can be loaded into 219 * this contact. If either of their {@link #mLookupKey} and {@link #mAccountName} 220 * is not the same or this contact is null, a new contact will be created and 221 * returned. 222 */ fromCursor(Context context, Cursor cursor, @Nullable Contact contact)223 public static Contact fromCursor(Context context, Cursor cursor, @Nullable Contact contact) { 224 int accountNameColumn = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME); 225 int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY); 226 String accountName = cursor.getString(accountNameColumn); 227 String lookupKey = cursor.getString(lookupKeyColumn); 228 229 if (contact == null) { 230 contact = new Contact(); 231 contact.loadBasicInfo(cursor); 232 } 233 234 if (!TextUtils.equals(accountName, contact.mAccountName) 235 || !TextUtils.equals(lookupKey, contact.mLookupKey)) { 236 Log.w(TAG, "A wrong contact is passed in. A new contact will be created."); 237 contact = new Contact(); 238 contact.loadBasicInfo(cursor); 239 } 240 241 int mimetypeColumn = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE); 242 String mimeType = cursor.getString(mimetypeColumn); 243 244 // More mimeType can be added here if more types of data needs to be loaded. 245 switch (mimeType) { 246 case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE: 247 contact.loadNameDetails(cursor); 248 break; 249 case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE: 250 contact.addPhoneNumber(context, cursor); 251 break; 252 case ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE: 253 contact.addPostalAddress(cursor); 254 break; 255 default: 256 Log.d(TAG, 257 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