blob: 90ae0f799b53461cee3c164ff0a5c25e37a827e8 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.telephony.common;
import android.content.Context;
import android.database.Cursor;
import android.icu.text.Collator;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Encapsulates data about a phone Contact entry. Typically loaded from the local Contact store.
*/
public class Contact implements Parcelable, Comparable<Contact> {
private static final String TAG = "CD.Contact";
private static final String PHONEBOOK_LABEL = "phonebook_label";
private static final String PHONEBOOK_LABEL_ALT = "phonebook_label_alt";
/**
* Contact belongs to TYPE_LETTER if its display name starts with a letter
*/
private static final int TYPE_LETTER = 1;
/**
* Contact belongs to TYPE_DIGIT if its display name starts with a digit
*/
private static final int TYPE_DIGIT = 2;
/**
* Contact belongs to TYPE_OTHER if it does not belong to TYPE_LETTER or TYPE_DIGIT
* Such as empty display name or the display name starts with "_"
*/
private static final int TYPE_OTHER = 3;
/**
* A reference to the {@link ContactsContract.Contacts#_ID} that this data belongs to. See
* {@link ContactsContract.Contacts.Entity#CONTACT_ID}
*/
private long mId;
/**
* Whether this contact entry is starred by user.
*/
private boolean mIsStarred;
/**
* Contact-specific information about whether or not a contact has been pinned by the user at
* a particular position within the system contact application's user interface.
*/
private int mPinnedPosition;
/**
* All phone numbers of this contact mapping to the unique primary key for the raw data entry.
*/
private List<PhoneNumber> mPhoneNumbers = new ArrayList<>();
/**
* The display name.
* <p>
* The standard text shown as the contact's display name, based on the best
* available information for the contact.
* </p>
*
* @see ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME
*/
private String mDisplayName;
/**
* Sort key that takes into account locale-based traditions for sorting names in address books.
*
* @see ContactsContract.CommonDataKinds.Phone#SORT_KEY_PRIMARY
*/
private String mSortKeyPrimary;
/**
* The alternative display name.
* <p>
* An alternative representation of the display name, such as "family name first"
* instead of "given name first" for Western names. If an alternative is not
* available, the values should be the same as {@link #mDisplayName}.
* </p>
*
* @see ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME_ALTERNATIVE
*/
private String mAltDisplayName;
/**
* Sort key based on the alternative representation of the full name.
*
* @see ContactsContract.CommonDataKinds.Phone#SORT_KEY_ALTERNATIVE
*/
private String mAltSortKey;
/**
* The phonebook label.
* <p>
* For {@link #mDisplayName}s starting with letters, label will be the first character of
* {@link #mDisplayName}. For {@link #mDisplayName}s starting with numbers, the label will
* be "#". For {@link #mDisplayName}s starting with other characters, the label will be "...".
* </p>
*/
private String mPhoneBookLabel;
/**
* The alternative phonebook label.
* <p>
* It is similar with {@link #mPhoneBookLabel}. But instead of generating from
* {@link #mDisplayName}, it will use {@link #mAltDisplayName}.
* </p>
*/
private String mPhoneBookLabelAlt;
/**
* A URI that can be used to retrieve a thumbnail of the contact's photo.
*/
private Uri mAvatarThumbnailUri;
/**
* A URI that can be used to retrieve the contact's full-size photo.
*/
private Uri mAvatarUri;
/**
* An opaque value that contains hints on how to find the contact if its row id changed
* as a result of a sync or aggregation. If a contact has multiple phone numbers, all phone
* numbers are recorded in a single entry and they all have the same look up key in a single
* load.
*/
private String mLookupKey;
/**
* Whether this contact represents a voice mail.
*/
private boolean mIsVoiceMail;
private PhoneNumber mPrimaryPhoneNumber;
/**
* Parses a Contact entry for a Cursor loaded from the Contact Database.
*/
public static Contact fromCursor(Context context, Cursor cursor) {
int contactIdColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID);
int starredColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.STARRED);
int pinnedColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PINNED);
int displayNameColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int altDisplayNameColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_ALTERNATIVE);
int phoneBookLabelColumn = cursor.getColumnIndex(PHONEBOOK_LABEL);
int phoneBookLabelAltColumn = cursor.getColumnIndex(PHONEBOOK_LABEL_ALT);
int avatarUriColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.PHOTO_URI);
int avatarThumbnailColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI);
int lookupKeyColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY);
int sortKeyPrimaryColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY);
int sortKeyAltColumn = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.SORT_KEY_ALTERNATIVE);
Contact contact = new Contact();
contact.mId = cursor.getLong(contactIdColumn);
contact.mDisplayName = cursor.getString(displayNameColumn);
contact.mSortKeyPrimary = cursor.getString(sortKeyPrimaryColumn);
contact.mAltDisplayName = cursor.getString(altDisplayNameColumn);
contact.mAltSortKey = cursor.getString(sortKeyAltColumn);
contact.mPhoneBookLabel = cursor.getString(phoneBookLabelColumn);
contact.mPhoneBookLabelAlt = cursor.getString(phoneBookLabelAltColumn);
PhoneNumber number = PhoneNumber.fromCursor(context, cursor);
contact.mPhoneNumbers.add(number);
if (number.isPrimary()) {
contact.mPrimaryPhoneNumber = number;
}
contact.mIsStarred = cursor.getInt(starredColumn) > 0;
contact.mPinnedPosition = cursor.getInt(pinnedColumn);
contact.mIsVoiceMail = TelecomUtils.isVoicemailNumber(context, number.getNumber());
String avatarUriStr = cursor.getString(avatarUriColumn);
contact.mAvatarUri = avatarUriStr == null ? null : Uri.parse(avatarUriStr);
String avatarThumbnailStringUri = cursor.getString(avatarThumbnailColumn);
contact.mAvatarThumbnailUri = avatarThumbnailStringUri == null ? null : Uri.parse(
avatarThumbnailStringUri);
String lookUpKey = cursor.getString(lookupKeyColumn);
if (lookUpKey != null) {
contact.mLookupKey = lookUpKey;
} else {
Log.w(TAG, "Look up key is null. Fallback to use display name");
contact.mLookupKey = contact.mDisplayName;
}
return contact;
}
@Override
public boolean equals(Object obj) {
return obj instanceof Contact && mLookupKey.equals(((Contact) obj).mLookupKey);
}
@Override
public int hashCode() {
return mLookupKey.hashCode();
}
@Override
public String toString() {
return mDisplayName + mPhoneNumbers;
}
public String getDisplayName() {
return mDisplayName;
}
/**
* Returns alternative display name.
*/
public String getAltDisplayName() {
return mAltDisplayName;
}
/**
* Returns {@link #mPhoneBookLabel}
*/
public String getPhonebookLabel() {
return mPhoneBookLabel;
}
/**
* Returns {@link #mPhoneBookLabelAlt}
*/
public String getPhonebookLabelAlt() {
return mPhoneBookLabelAlt;
}
public boolean isVoicemail() {
return mIsVoiceMail;
}
@Nullable
public Uri getAvatarUri() {
return mAvatarThumbnailUri != null ? mAvatarThumbnailUri : mAvatarUri;
}
public String getLookupKey() {
return mLookupKey;
}
public Uri getLookupUri() {
return ContactsContract.Contacts.getLookupUri(mId, mLookupKey);
}
/** Return all phone numbers associated with this contact. */
public List<PhoneNumber> getNumbers() {
return mPhoneNumbers;
}
/** Return the aggregated contact id. */
public long getId() {
return mId;
}
public boolean isStarred() {
return mIsStarred;
}
public int getPinnedPosition() {
return mPinnedPosition;
}
/**
* Merges a Contact entry with another if they represent different numbers of the same contact.
*
* @return A merged contact.
*/
public Contact merge(Contact contact) {
if (equals(contact)) {
for (PhoneNumber phoneNumber : contact.mPhoneNumbers) {
int indexOfPhoneNumber = mPhoneNumbers.indexOf(phoneNumber);
if (indexOfPhoneNumber < 0) {
mPhoneNumbers.add(phoneNumber);
} else {
PhoneNumber existingPhoneNumber = mPhoneNumbers.get(indexOfPhoneNumber);
existingPhoneNumber.merge(phoneNumber);
}
}
if (contact.mPrimaryPhoneNumber != null) {
mPrimaryPhoneNumber = contact.mPrimaryPhoneNumber.merge(mPrimaryPhoneNumber);
}
}
return this;
}
/**
* Looks up a {@link PhoneNumber} of this contact for the given phone number. Returns {@code
* null} if this contact doesn't contain the given phone number.
*/
@Nullable
public PhoneNumber getPhoneNumber(Context context, String number) {
I18nPhoneNumberWrapper i18nPhoneNumber = I18nPhoneNumberWrapper.Factory.INSTANCE.get(
context, number);
for (PhoneNumber phoneNumber : mPhoneNumbers) {
if (phoneNumber.getI18nPhoneNumberWrapper().equals(i18nPhoneNumber)) {
return phoneNumber;
}
}
return null;
}
public PhoneNumber getPrimaryPhoneNumber() {
return mPrimaryPhoneNumber;
}
/** Return if this contact has a primary phone number. */
public boolean hasPrimaryPhoneNumber() {
return mPrimaryPhoneNumber != null;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(mId);
dest.writeBoolean(mIsStarred);
dest.writeInt(mPinnedPosition);
dest.writeInt(mPhoneNumbers.size());
for (PhoneNumber phoneNumber : mPhoneNumbers) {
dest.writeParcelable(phoneNumber, flags);
}
dest.writeString(mDisplayName);
dest.writeString(mSortKeyPrimary);
dest.writeString(mAltDisplayName);
dest.writeString(mAltSortKey);
dest.writeString(mPhoneBookLabel);
dest.writeString(mPhoneBookLabelAlt);
dest.writeParcelable(mAvatarThumbnailUri, 0);
dest.writeParcelable(mAvatarUri, 0);
dest.writeString(mLookupKey);
dest.writeBoolean(mIsVoiceMail);
}
public static final Creator<Contact> CREATOR = new Creator<Contact>() {
@Override
public Contact createFromParcel(Parcel source) {
return Contact.fromParcel(source);
}
@Override
public Contact[] newArray(int size) {
return new Contact[size];
}
};
/** Create {@link Contact} object from saved parcelable. */
private static Contact fromParcel(Parcel source) {
Contact contact = new Contact();
contact.mId = source.readLong();
contact.mIsStarred = source.readBoolean();
contact.mPinnedPosition = source.readInt();
int phoneNumberListLength = source.readInt();
contact.mPhoneNumbers = new ArrayList<>();
for (int i = 0; i < phoneNumberListLength; i++) {
PhoneNumber phoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader());
contact.mPhoneNumbers.add(phoneNumber);
if (phoneNumber.isPrimary()) {
contact.mPrimaryPhoneNumber = phoneNumber;
}
}
contact.mDisplayName = source.readString();
contact.mSortKeyPrimary = source.readString();
contact.mAltDisplayName = source.readString();
contact.mAltSortKey = source.readString();
contact.mPhoneBookLabel = source.readString();
contact.mPhoneBookLabelAlt = source.readString();
contact.mAvatarThumbnailUri = source.readParcelable(Uri.class.getClassLoader());
contact.mAvatarUri = source.readParcelable(Uri.class.getClassLoader());
contact.mLookupKey = source.readString();
contact.mIsVoiceMail = source.readBoolean();
return contact;
}
@Override
public int compareTo(Contact otherContact) {
// Use a helper function to classify Contacts
// and by default, it should be compared by first name order.
return compareByDisplayName(otherContact);
}
/**
* Compares contacts by their {@link #mDisplayName} in an order of
* letters, numbers, then special characters.
*/
public int compareByDisplayName(@NonNull Contact otherContact) {
return compareNames(mSortKeyPrimary, otherContact.mSortKeyPrimary, mPhoneBookLabel,
otherContact.getPhonebookLabel());
}
/**
* Compares contacts by their {@link #mAltDisplayName} in an order of
* letters, numbers, then special characters.
*/
public int compareByAltDisplayName(@NonNull Contact otherContact) {
return compareNames(mAltSortKey, otherContact.mAltSortKey,
mPhoneBookLabelAlt, otherContact.getPhonebookLabelAlt());
}
/**
* Compares two strings in an order of letters, numbers, then special characters.
*/
private int compareNames(String name, String otherName, String label, String otherLabel) {
int type = getNameType(label);
int otherType = getNameType(otherLabel);
if (type != otherType) {
return Integer.compare(type, otherType);
}
Collator collator = Collator.getInstance();
return collator.compare(name == null ? "" : name, otherName == null ? "" : otherName);
}
/**
* Returns the type of the name string.
* Types can be {@link #TYPE_LETTER}, {@link #TYPE_DIGIT} and {@link #TYPE_OTHER}.
*/
private static int getNameType(String label) {
// A helper function to classify Contacts
if (!TextUtils.isEmpty(label)) {
if (Character.isLetter(label.charAt(0))) {
return TYPE_LETTER;
}
if (label.contains("#")) {
return TYPE_DIGIT;
}
}
return TYPE_OTHER;
}
}