• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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