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