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