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