blob: ee60578eada0f6e95673861423f17e47812e2043 [file] [log] [blame]
/*
* Copyright (C) 2009 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.vcard;
import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
import android.accounts.Account;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents one vCard entry, which should start with "BEGIN:VCARD" and end
* with "END:VCARD". This class is for bridging between real vCard data and
* Android's {@link ContactsContract}, which means some aspects of vCard are
* dropped before this object being constructed. Raw vCard data should be first
* supplied with {@link #addProperty(VCardProperty)}. After supplying all data,
* user should call {@link #consolidateFields()} to prepare some additional
* information which is constructable from supplied raw data. TODO: preserve raw
* data using {@link VCardProperty}. If it may just waste memory, this at least
* should contain them when it cannot convert vCard as a string to Android's
* Contacts representation. Those raw properties should _not_ be used for
* {@link #isIgnorable()}.
*/
public class VCardEntry {
private static final String LOG_TAG = VCardConstants.LOG_TAG;
private static final int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
static {
sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
Im.PROTOCOL_GOOGLE_TALK);
}
/**
* Whether to insert this VCardEntry as RawContacts.STARRED
*/
private boolean mStarred = false;
public void setStarred(boolean val) {
mStarred = val;
}
public boolean getStarred() {
return mStarred;
}
public enum EntryLabel {
NAME,
PHONE,
EMAIL,
POSTAL_ADDRESS,
ORGANIZATION,
IM,
PHOTO,
WEBSITE,
SIP,
NICKNAME,
NOTE,
BIRTHDAY,
ANNIVERSARY,
ANDROID_CUSTOM
}
public static interface EntryElement {
// Also need to inherit toString(), equals().
public EntryLabel getEntryLabel();
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex);
public boolean isEmpty();
}
// TODO: vCard 4.0 logically has multiple formatted names and we need to
// select the most preferable one using PREF parameter.
//
// e.g. (based on rev.13)
// FN;PREF=1:John M. Doe
// FN;PREF=2:John Doe
// FN;PREF=3;John
public static class NameData implements EntryElement {
private String mFamily;
private String mGiven;
private String mMiddle;
private String mPrefix;
private String mSuffix;
// Used only when no family nor given name is found.
private String mFormatted;
private String mPhoneticFamily;
private String mPhoneticGiven;
private String mPhoneticMiddle;
// For "SORT-STRING" in vCard 3.0.
private String mSortString;
/**
* Not in vCard but for {@link StructuredName#DISPLAY_NAME}. This field
* is constructed by VCardEntry on demand. Consider using
* {@link VCardEntry#getDisplayName()}.
*/
// This field should reflect the other Elem fields like Email,
// PostalAddress, etc., while
// This is static class which cannot see other data. Thus we ask
// VCardEntry to populate it.
public String displayName;
public boolean emptyStructuredName() {
return TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mGiven)
&& TextUtils.isEmpty(mMiddle) && TextUtils.isEmpty(mPrefix)
&& TextUtils.isEmpty(mSuffix);
}
public boolean emptyPhoneticStructuredName() {
return TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticGiven)
&& TextUtils.isEmpty(mPhoneticMiddle);
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
if (!TextUtils.isEmpty(mGiven)) {
builder.withValue(StructuredName.GIVEN_NAME, mGiven);
}
if (!TextUtils.isEmpty(mFamily)) {
builder.withValue(StructuredName.FAMILY_NAME, mFamily);
}
if (!TextUtils.isEmpty(mMiddle)) {
builder.withValue(StructuredName.MIDDLE_NAME, mMiddle);
}
if (!TextUtils.isEmpty(mPrefix)) {
builder.withValue(StructuredName.PREFIX, mPrefix);
}
if (!TextUtils.isEmpty(mSuffix)) {
builder.withValue(StructuredName.SUFFIX, mSuffix);
}
boolean phoneticNameSpecified = false;
if (!TextUtils.isEmpty(mPhoneticGiven)) {
builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGiven);
phoneticNameSpecified = true;
}
if (!TextUtils.isEmpty(mPhoneticFamily)) {
builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamily);
phoneticNameSpecified = true;
}
if (!TextUtils.isEmpty(mPhoneticMiddle)) {
builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddle);
phoneticNameSpecified = true;
}
// SORT-STRING is used only when phonetic names aren't specified in
// the original vCard.
if (!phoneticNameSpecified) {
builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mSortString);
}
builder.withValue(StructuredName.DISPLAY_NAME, displayName);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return (TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mMiddle)
&& TextUtils.isEmpty(mGiven) && TextUtils.isEmpty(mPrefix)
&& TextUtils.isEmpty(mSuffix) && TextUtils.isEmpty(mFormatted)
&& TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticMiddle)
&& TextUtils.isEmpty(mPhoneticGiven) && TextUtils.isEmpty(mSortString));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof NameData)) {
return false;
}
NameData nameData = (NameData) obj;
return (TextUtils.equals(mFamily, nameData.mFamily)
&& TextUtils.equals(mMiddle, nameData.mMiddle)
&& TextUtils.equals(mGiven, nameData.mGiven)
&& TextUtils.equals(mPrefix, nameData.mPrefix)
&& TextUtils.equals(mSuffix, nameData.mSuffix)
&& TextUtils.equals(mFormatted, nameData.mFormatted)
&& TextUtils.equals(mPhoneticFamily, nameData.mPhoneticFamily)
&& TextUtils.equals(mPhoneticMiddle, nameData.mPhoneticMiddle)
&& TextUtils.equals(mPhoneticGiven, nameData.mPhoneticGiven)
&& TextUtils.equals(mSortString, nameData.mSortString));
}
@Override
public int hashCode() {
final String[] hashTargets = new String[] {mFamily, mMiddle, mGiven, mPrefix, mSuffix,
mFormatted, mPhoneticFamily, mPhoneticMiddle,
mPhoneticGiven, mSortString};
int hash = 0;
for (String hashTarget : hashTargets) {
hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
}
return hash;
}
@Override
public String toString() {
return String.format("family: %s, given: %s, middle: %s, prefix: %s, suffix: %s",
mFamily, mGiven, mMiddle, mPrefix, mSuffix);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.NAME;
}
public String getFamily() {
return mFamily;
}
public String getMiddle() {
return mMiddle;
}
public String getGiven() {
return mGiven;
}
public String getPrefix() {
return mPrefix;
}
public String getSuffix() {
return mSuffix;
}
public String getFormatted() {
return mFormatted;
}
public String getSortString() {
return mSortString;
}
/** @hide Just for testing. */
public void setFamily(String family) { mFamily = family; }
/** @hide Just for testing. */
public void setMiddle(String middle) { mMiddle = middle; }
/** @hide Just for testing. */
public void setGiven(String given) { mGiven = given; }
/** @hide Just for testing. */
public void setPrefix(String prefix) { mPrefix = prefix; }
/** @hide Just for testing. */
public void setSuffix(String suffix) { mSuffix = suffix; }
}
public static class PhoneData implements EntryElement {
private final String mNumber;
private final int mType;
private final String mLabel;
// isPrimary is (not final but) changable, only when there's no
// appropriate one existing
// in the original VCard.
private boolean mIsPrimary;
public PhoneData(String data, int type, String label, boolean isPrimary) {
mNumber = data;
mType = type;
mLabel = label;
mIsPrimary = isPrimary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Phone.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
builder.withValue(Phone.TYPE, mType);
if (mType == Phone.TYPE_CUSTOM) {
builder.withValue(Phone.LABEL, mLabel);
}
builder.withValue(Phone.NUMBER, mNumber);
if (mIsPrimary) {
builder.withValue(Phone.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mNumber);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PhoneData)) {
return false;
}
PhoneData phoneData = (PhoneData) obj;
return (mType == phoneData.mType
&& TextUtils.equals(mNumber, phoneData.mNumber)
&& TextUtils.equals(mLabel, phoneData.mLabel)
&& (mIsPrimary == phoneData.mIsPrimary));
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + (mNumber != null ? mNumber.hashCode() : 0);
hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mNumber,
mLabel, mIsPrimary);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.PHONE;
}
public String getNumber() {
return mNumber;
}
public int getType() {
return mType;
}
public String getLabel() {
return mLabel;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class EmailData implements EntryElement {
private final String mAddress;
private final int mType;
// Used only when TYPE is TYPE_CUSTOM.
private final String mLabel;
private final boolean mIsPrimary;
public EmailData(String data, int type, String label, boolean isPrimary) {
mType = type;
mAddress = data;
mLabel = label;
mIsPrimary = isPrimary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Email.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
builder.withValue(Email.TYPE, mType);
if (mType == Email.TYPE_CUSTOM) {
builder.withValue(Email.LABEL, mLabel);
}
builder.withValue(Email.DATA, mAddress);
if (mIsPrimary) {
builder.withValue(Data.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mAddress);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof EmailData)) {
return false;
}
EmailData emailData = (EmailData) obj;
return (mType == emailData.mType
&& TextUtils.equals(mAddress, emailData.mAddress)
&& TextUtils.equals(mLabel, emailData.mLabel)
&& (mIsPrimary == emailData.mIsPrimary));
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mAddress,
mLabel, mIsPrimary);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.EMAIL;
}
public String getAddress() {
return mAddress;
}
public int getType() {
return mType;
}
public String getLabel() {
return mLabel;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class PostalData implements EntryElement {
// Determined by vCard specification.
// - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
private static final int ADDR_MAX_DATA_SIZE = 7;
private final String mPobox;
private final String mExtendedAddress;
private final String mStreet;
private final String mLocalty;
private final String mRegion;
private final String mPostalCode;
private final String mCountry;
private final int mType;
private final String mLabel;
private boolean mIsPrimary;
/** We keep this for {@link StructuredPostal#FORMATTED_ADDRESS} */
// TODO: need better way to construct formatted address.
private int mVCardType;
public PostalData(String pobox, String extendedAddress, String street, String localty,
String region, String postalCode, String country, int type, String label,
boolean isPrimary, int vcardType) {
mType = type;
mPobox = pobox;
mExtendedAddress = extendedAddress;
mStreet = street;
mLocalty = localty;
mRegion = region;
mPostalCode = postalCode;
mCountry = country;
mLabel = label;
mIsPrimary = isPrimary;
mVCardType = vcardType;
}
/**
* Accepts raw propertyValueList in vCard and constructs PostalData.
*/
public static PostalData constructPostalData(final List<String> propValueList,
final int type, final String label, boolean isPrimary, int vcardType) {
final String[] dataArray = new String[ADDR_MAX_DATA_SIZE];
int size = propValueList.size();
if (size > ADDR_MAX_DATA_SIZE) {
size = ADDR_MAX_DATA_SIZE;
}
// adr-value = 0*6(text-value ";") text-value
// ; PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name
//
// Use Iterator assuming List may be LinkedList, though actually it is
// always ArrayList in the current implementation.
int i = 0;
for (String addressElement : propValueList) {
dataArray[i] = addressElement;
if (++i >= size) {
break;
}
}
while (i < ADDR_MAX_DATA_SIZE) {
dataArray[i++] = null;
}
return new PostalData(dataArray[0], dataArray[1], dataArray[2], dataArray[3],
dataArray[4], dataArray[5], dataArray[6], type, label, isPrimary, vcardType);
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
builder.withValue(StructuredPostal.TYPE, mType);
if (mType == StructuredPostal.TYPE_CUSTOM) {
builder.withValue(StructuredPostal.LABEL, mLabel);
}
final String streetString;
if (TextUtils.isEmpty(mStreet)) {
if (TextUtils.isEmpty(mExtendedAddress)) {
streetString = null;
} else {
streetString = mExtendedAddress;
}
} else {
if (TextUtils.isEmpty(mExtendedAddress)) {
streetString = mStreet;
} else {
streetString = mStreet + " " + mExtendedAddress;
}
}
builder.withValue(StructuredPostal.POBOX, mPobox);
builder.withValue(StructuredPostal.STREET, streetString);
builder.withValue(StructuredPostal.CITY, mLocalty);
builder.withValue(StructuredPostal.REGION, mRegion);
builder.withValue(StructuredPostal.POSTCODE, mPostalCode);
builder.withValue(StructuredPostal.COUNTRY, mCountry);
builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress(mVCardType));
if (mIsPrimary) {
builder.withValue(Data.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
public String getFormattedAddress(final int vcardType) {
StringBuilder builder = new StringBuilder();
boolean empty = true;
final String[] dataArray = new String[] {
mPobox, mExtendedAddress, mStreet, mLocalty, mRegion, mPostalCode, mCountry
};
if (VCardConfig.isJapaneseDevice(vcardType)) {
// In Japan, the order is reversed.
for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
String addressPart = dataArray[i];
if (!TextUtils.isEmpty(addressPart)) {
if (!empty) {
builder.append(' ');
} else {
empty = false;
}
builder.append(addressPart);
}
}
} else {
for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
String addressPart = dataArray[i];
if (!TextUtils.isEmpty(addressPart)) {
if (!empty) {
builder.append(' ');
} else {
empty = false;
}
builder.append(addressPart);
}
}
}
return builder.toString().trim();
}
@Override
public boolean isEmpty() {
return (TextUtils.isEmpty(mPobox)
&& TextUtils.isEmpty(mExtendedAddress)
&& TextUtils.isEmpty(mStreet)
&& TextUtils.isEmpty(mLocalty)
&& TextUtils.isEmpty(mRegion)
&& TextUtils.isEmpty(mPostalCode)
&& TextUtils.isEmpty(mCountry));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PostalData)) {
return false;
}
final PostalData postalData = (PostalData) obj;
return (mType == postalData.mType)
&& (mType == StructuredPostal.TYPE_CUSTOM ? TextUtils.equals(mLabel,
postalData.mLabel) : true)
&& (mIsPrimary == postalData.mIsPrimary)
&& TextUtils.equals(mPobox, postalData.mPobox)
&& TextUtils.equals(mExtendedAddress, postalData.mExtendedAddress)
&& TextUtils.equals(mStreet, postalData.mStreet)
&& TextUtils.equals(mLocalty, postalData.mLocalty)
&& TextUtils.equals(mRegion, postalData.mRegion)
&& TextUtils.equals(mPostalCode, postalData.mPostalCode)
&& TextUtils.equals(mCountry, postalData.mCountry);
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
final String[] hashTargets = new String[] {mPobox, mExtendedAddress, mStreet,
mLocalty, mRegion, mPostalCode, mCountry};
for (String hashTarget : hashTargets) {
hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
}
return hash;
}
@Override
public String toString() {
return String.format("type: %d, label: %s, isPrimary: %s, pobox: %s, "
+ "extendedAddress: %s, street: %s, localty: %s, region: %s, postalCode %s, "
+ "country: %s", mType, mLabel, mIsPrimary, mPobox, mExtendedAddress, mStreet,
mLocalty, mRegion, mPostalCode, mCountry);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.POSTAL_ADDRESS;
}
public String getPobox() {
return mPobox;
}
public String getExtendedAddress() {
return mExtendedAddress;
}
public String getStreet() {
return mStreet;
}
public String getLocalty() {
return mLocalty;
}
public String getRegion() {
return mRegion;
}
public String getPostalCode() {
return mPostalCode;
}
public String getCountry() {
return mCountry;
}
public int getType() {
return mType;
}
public String getLabel() {
return mLabel;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class OrganizationData implements EntryElement {
// non-final is Intentional: we may change the values since this info is separated into
// two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in different
// timing.
private String mOrganizationName;
private String mDepartmentName;
private String mTitle;
private final String mPhoneticName; // We won't have this in "TITLE" property.
private final int mType;
private boolean mIsPrimary;
public OrganizationData(final String organizationName, final String departmentName,
final String titleName, final String phoneticName, int type,
final boolean isPrimary) {
mType = type;
mOrganizationName = organizationName;
mDepartmentName = departmentName;
mTitle = titleName;
mPhoneticName = phoneticName;
mIsPrimary = isPrimary;
}
public String getFormattedString() {
final StringBuilder builder = new StringBuilder();
if (!TextUtils.isEmpty(mOrganizationName)) {
builder.append(mOrganizationName);
}
if (!TextUtils.isEmpty(mDepartmentName)) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(mDepartmentName);
}
if (!TextUtils.isEmpty(mTitle)) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(mTitle);
}
return builder.toString();
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Organization.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
builder.withValue(Organization.TYPE, mType);
if (mOrganizationName != null) {
builder.withValue(Organization.COMPANY, mOrganizationName);
}
if (mDepartmentName != null) {
builder.withValue(Organization.DEPARTMENT, mDepartmentName);
}
if (mTitle != null) {
builder.withValue(Organization.TITLE, mTitle);
}
if (mPhoneticName != null) {
builder.withValue(Organization.PHONETIC_NAME, mPhoneticName);
}
if (mIsPrimary) {
builder.withValue(Organization.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mOrganizationName) && TextUtils.isEmpty(mDepartmentName)
&& TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mPhoneticName);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof OrganizationData)) {
return false;
}
OrganizationData organization = (OrganizationData) obj;
return (mType == organization.mType
&& TextUtils.equals(mOrganizationName, organization.mOrganizationName)
&& TextUtils.equals(mDepartmentName, organization.mDepartmentName)
&& TextUtils.equals(mTitle, organization.mTitle)
&& (mIsPrimary == organization.mIsPrimary));
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + (mOrganizationName != null ? mOrganizationName.hashCode() : 0);
hash = hash * 31 + (mDepartmentName != null ? mDepartmentName.hashCode() : 0);
hash = hash * 31 + (mTitle != null ? mTitle.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
return String.format(
"type: %d, organization: %s, department: %s, title: %s, isPrimary: %s", mType,
mOrganizationName, mDepartmentName, mTitle, mIsPrimary);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.ORGANIZATION;
}
public String getOrganizationName() {
return mOrganizationName;
}
public String getDepartmentName() {
return mDepartmentName;
}
public String getTitle() {
return mTitle;
}
public String getPhoneticName() {
return mPhoneticName;
}
public int getType() {
return mType;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class ImData implements EntryElement {
private final String mAddress;
private final int mProtocol;
private final String mCustomProtocol;
private final int mType;
private final boolean mIsPrimary;
public ImData(final int protocol, final String customProtocol, final String address,
final int type, final boolean isPrimary) {
mProtocol = protocol;
mCustomProtocol = customProtocol;
mType = type;
mAddress = address;
mIsPrimary = isPrimary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Im.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
builder.withValue(Im.TYPE, mType);
builder.withValue(Im.PROTOCOL, mProtocol);
builder.withValue(Im.DATA, mAddress);
if (mProtocol == Im.PROTOCOL_CUSTOM) {
builder.withValue(Im.CUSTOM_PROTOCOL, mCustomProtocol);
}
if (mIsPrimary) {
builder.withValue(Data.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mAddress);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ImData)) {
return false;
}
ImData imData = (ImData) obj;
return (mType == imData.mType
&& mProtocol == imData.mProtocol
&& TextUtils.equals(mCustomProtocol, imData.mCustomProtocol)
&& TextUtils.equals(mAddress, imData.mAddress)
&& (mIsPrimary == imData.mIsPrimary));
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + mProtocol;
hash = hash * 31 + (mCustomProtocol != null ? mCustomProtocol.hashCode() : 0);
hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
return String.format(
"type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", mType,
mProtocol, mCustomProtocol, mAddress, mIsPrimary);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.IM;
}
public String getAddress() {
return mAddress;
}
/**
* One of the value available for {@link Im#PROTOCOL}. e.g.
* {@link Im#PROTOCOL_GOOGLE_TALK}
*/
public int getProtocol() {
return mProtocol;
}
public String getCustomProtocol() {
return mCustomProtocol;
}
public int getType() {
return mType;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class PhotoData implements EntryElement {
// private static final String FORMAT_FLASH = "SWF";
// used when type is not defined in ContactsContract.
private final String mFormat;
private final boolean mIsPrimary;
private final byte[] mBytes;
private Integer mHashCode = null;
public PhotoData(String format, byte[] photoBytes, boolean isPrimary) {
mFormat = format;
mBytes = photoBytes;
mIsPrimary = isPrimary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
builder.withValue(Photo.PHOTO, mBytes);
if (mIsPrimary) {
builder.withValue(Photo.IS_PRIMARY, 1);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return mBytes == null || mBytes.length == 0;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PhotoData)) {
return false;
}
PhotoData photoData = (PhotoData) obj;
return (TextUtils.equals(mFormat, photoData.mFormat)
&& Arrays.equals(mBytes, photoData.mBytes)
&& (mIsPrimary == photoData.mIsPrimary));
}
@Override
public int hashCode() {
if (mHashCode != null) {
return mHashCode;
}
int hash = mFormat != null ? mFormat.hashCode() : 0;
hash = hash * 31;
if (mBytes != null) {
for (byte b : mBytes) {
hash += b;
}
}
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
mHashCode = hash;
return hash;
}
@Override
public String toString() {
return String.format("format: %s: size: %d, isPrimary: %s", mFormat, mBytes.length,
mIsPrimary);
}
@Override
public final EntryLabel getEntryLabel() {
return EntryLabel.PHOTO;
}
public String getFormat() {
return mFormat;
}
public byte[] getBytes() {
return mBytes;
}
public boolean isPrimary() {
return mIsPrimary;
}
}
public static class NicknameData implements EntryElement {
private final String mNickname;
public NicknameData(String nickname) {
mNickname = nickname;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
builder.withValue(Nickname.NAME, mNickname);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mNickname);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof NicknameData)) {
return false;
}
NicknameData nicknameData = (NicknameData) obj;
return TextUtils.equals(mNickname, nicknameData.mNickname);
}
@Override
public int hashCode() {
return mNickname != null ? mNickname.hashCode() : 0;
}
@Override
public String toString() {
return "nickname: " + mNickname;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.NICKNAME;
}
public String getNickname() {
return mNickname;
}
}
public static class NoteData implements EntryElement {
public final String mNote;
public NoteData(String note) {
mNote = note;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
builder.withValue(Note.NOTE, mNote);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mNote);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof NoteData)) {
return false;
}
NoteData noteData = (NoteData) obj;
return TextUtils.equals(mNote, noteData.mNote);
}
@Override
public int hashCode() {
return mNote != null ? mNote.hashCode() : 0;
}
@Override
public String toString() {
return "note: " + mNote;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.NOTE;
}
public String getNote() {
return mNote;
}
}
public static class WebsiteData implements EntryElement {
private final String mWebsite;
public WebsiteData(String website) {
mWebsite = website;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
builder.withValue(Website.URL, mWebsite);
// There's no information about the type of URL in vCard.
// We use TYPE_HOMEPAGE for safety.
builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mWebsite);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof WebsiteData)) {
return false;
}
WebsiteData websiteData = (WebsiteData) obj;
return TextUtils.equals(mWebsite, websiteData.mWebsite);
}
@Override
public int hashCode() {
return mWebsite != null ? mWebsite.hashCode() : 0;
}
@Override
public String toString() {
return "website: " + mWebsite;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.WEBSITE;
}
public String getWebsite() {
return mWebsite;
}
}
public static class BirthdayData implements EntryElement {
private final String mBirthday;
public BirthdayData(String birthday) {
mBirthday = birthday;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
builder.withValue(Event.START_DATE, mBirthday);
builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mBirthday);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof BirthdayData)) {
return false;
}
BirthdayData birthdayData = (BirthdayData) obj;
return TextUtils.equals(mBirthday, birthdayData.mBirthday);
}
@Override
public int hashCode() {
return mBirthday != null ? mBirthday.hashCode() : 0;
}
@Override
public String toString() {
return "birthday: " + mBirthday;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.BIRTHDAY;
}
public String getBirthday() {
return mBirthday;
}
}
public static class AnniversaryData implements EntryElement {
private final String mAnniversary;
public AnniversaryData(String anniversary) {
mAnniversary = anniversary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
builder.withValue(Event.START_DATE, mAnniversary);
builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mAnniversary);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AnniversaryData)) {
return false;
}
AnniversaryData anniversaryData = (AnniversaryData) obj;
return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary);
}
@Override
public int hashCode() {
return mAnniversary != null ? mAnniversary.hashCode() : 0;
}
@Override
public String toString() {
return "anniversary: " + mAnniversary;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.ANNIVERSARY;
}
public String getAnniversary() { return mAnniversary; }
}
public static class SipData implements EntryElement {
/**
* Note that schema part ("sip:") is automatically removed. e.g.
* "sip:username:password@host:port" becomes
* "username:password@host:port"
*/
private final String mAddress;
private final int mType;
private final String mLabel;
private final boolean mIsPrimary;
public SipData(String rawSip, int type, String label, boolean isPrimary) {
if (rawSip.startsWith("sip:")) {
mAddress = rawSip.substring(4);
} else {
mAddress = rawSip;
}
mType = type;
mLabel = label;
mIsPrimary = isPrimary;
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
builder.withValue(SipAddress.SIP_ADDRESS, mAddress);
builder.withValue(SipAddress.TYPE, mType);
if (mType == SipAddress.TYPE_CUSTOM) {
builder.withValue(SipAddress.LABEL, mLabel);
}
if (mIsPrimary) {
builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary);
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mAddress);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SipData)) {
return false;
}
SipData sipData = (SipData) obj;
return (mType == sipData.mType
&& TextUtils.equals(mLabel, sipData.mLabel)
&& TextUtils.equals(mAddress, sipData.mAddress)
&& (mIsPrimary == sipData.mIsPrimary));
}
@Override
public int hashCode() {
int hash = mType;
hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
return "sip: " + mAddress;
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.SIP;
}
/**
* @return Address part of the sip data. The schema ("sip:") isn't contained here.
*/
public String getAddress() { return mAddress; }
public int getType() { return mType; }
public String getLabel() { return mLabel; }
}
/**
* Some Contacts data in Android cannot be converted to vCard
* representation. VCardEntry preserves those data using this class.
*/
public static class AndroidCustomData implements EntryElement {
private final String mMimeType;
private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN
public AndroidCustomData(String mimeType, List<String> dataList) {
mMimeType = mimeType;
mDataList = dataList;
}
public static AndroidCustomData constructAndroidCustomData(List<String> list) {
String mimeType;
List<String> dataList;
if (list == null) {
mimeType = null;
dataList = null;
} else if (list.size() < 2) {
mimeType = list.get(0);
dataList = null;
} else {
final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size()
: VCardConstants.MAX_DATA_COLUMN + 1;
mimeType = list.get(0);
dataList = list.subList(1, max);
}
return new AndroidCustomData(mimeType, dataList);
}
@Override
public void constructInsertOperation(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(Data.CONTENT_URI);
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex);
builder.withValue(Data.MIMETYPE, mMimeType);
for (int i = 0; i < mDataList.size(); i++) {
String value = mDataList.get(i);
if (!TextUtils.isEmpty(value)) {
// 1-origin
builder.withValue("data" + (i + 1), value);
}
}
operationList.add(builder.build());
}
@Override
public boolean isEmpty() {
return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AndroidCustomData)) {
return false;
}
AndroidCustomData data = (AndroidCustomData) obj;
if (!TextUtils.equals(mMimeType, data.mMimeType)) {
return false;
}
if (mDataList == null) {
return data.mDataList == null;
} else {
final int size = mDataList.size();
if (size != data.mDataList.size()) {
return false;
}
for (int i = 0; i < size; i++) {
if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) {
return false;
}
}
return true;
}
}
@Override
public int hashCode() {
int hash = mMimeType != null ? mMimeType.hashCode() : 0;
if (mDataList != null) {
for (String data : mDataList) {
hash = hash * 31 + (data != null ? data.hashCode() : 0);
}
}
return hash;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("android-custom: " + mMimeType + ", data: ");
builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray()));
return builder.toString();
}
@Override
public EntryLabel getEntryLabel() {
return EntryLabel.ANDROID_CUSTOM;
}
public String getMimeType() { return mMimeType; }
public List<String> getDataList() { return mDataList; }
}
private final NameData mNameData = new NameData();
private List<PhoneData> mPhoneList;
private List<EmailData> mEmailList;
private List<PostalData> mPostalList;
private List<OrganizationData> mOrganizationList;
private List<ImData> mImList;
private List<PhotoData> mPhotoList;
private List<WebsiteData> mWebsiteList;
private List<SipData> mSipList;
private List<NicknameData> mNicknameList;
private List<NoteData> mNoteList;
private List<AndroidCustomData> mAndroidCustomDataList;
private BirthdayData mBirthday;
private AnniversaryData mAnniversary;
private List<Pair<String, String>> mUnknownXData;
/**
* Inner iterator interface.
*/
public interface EntryElementIterator {
public void onIterationStarted();
public void onIterationEnded();
/**
* Called when there are one or more {@link EntryElement} instances
* associated with {@link EntryLabel}.
*/
public void onElementGroupStarted(EntryLabel label);
/**
* Called after all {@link EntryElement} instances for
* {@link EntryLabel} provided on {@link #onElementGroupStarted(EntryLabel)}
* being processed by {@link #onElement(EntryElement)}
*/
public void onElementGroupEnded();
/**
* @return should be true when child wants to continue the operation.
* False otherwise.
*/
public boolean onElement(EntryElement elem);
}
public final void iterateAllData(EntryElementIterator iterator) {
iterator.onIterationStarted();
iterator.onElementGroupStarted(mNameData.getEntryLabel());
iterator.onElement(mNameData);
iterator.onElementGroupEnded();
iterateOneList(mPhoneList, iterator);
iterateOneList(mEmailList, iterator);
iterateOneList(mPostalList, iterator);
iterateOneList(mOrganizationList, iterator);
iterateOneList(mImList, iterator);
iterateOneList(mPhotoList, iterator);
iterateOneList(mWebsiteList, iterator);
iterateOneList(mSipList, iterator);
iterateOneList(mNicknameList, iterator);
iterateOneList(mNoteList, iterator);
iterateOneList(mAndroidCustomDataList, iterator);
if (mBirthday != null) {
iterator.onElementGroupStarted(mBirthday.getEntryLabel());
iterator.onElement(mBirthday);
iterator.onElementGroupEnded();
}
if (mAnniversary != null) {
iterator.onElementGroupStarted(mAnniversary.getEntryLabel());
iterator.onElement(mAnniversary);
iterator.onElementGroupEnded();
}
iterator.onIterationEnded();
}
private void iterateOneList(List<? extends EntryElement> elemList,
EntryElementIterator iterator) {
if (elemList != null && elemList.size() > 0) {
iterator.onElementGroupStarted(elemList.get(0).getEntryLabel());
for (EntryElement elem : elemList) {
iterator.onElement(elem);
}
iterator.onElementGroupEnded();
}
}
private class IsIgnorableIterator implements EntryElementIterator {
private boolean mEmpty = true;
@Override
public void onIterationStarted() {
}
@Override
public void onIterationEnded() {
}
@Override
public void onElementGroupStarted(EntryLabel label) {
}
@Override
public void onElementGroupEnded() {
}
@Override
public boolean onElement(EntryElement elem) {
if (!elem.isEmpty()) {
mEmpty = false;
// exit now
return false;
} else {
return true;
}
}
public boolean getResult() {
return mEmpty;
}
}
private class ToStringIterator implements EntryElementIterator {
private StringBuilder mBuilder;
private boolean mFirstElement;
@Override
public void onIterationStarted() {
mBuilder = new StringBuilder();
mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n");
}
@Override
public void onElementGroupStarted(EntryLabel label) {
mBuilder.append(label.toString() + ": ");
mFirstElement = true;
}
@Override
public boolean onElement(EntryElement elem) {
if (!mFirstElement) {
mBuilder.append(", ");
mFirstElement = false;
}
mBuilder.append("[").append(elem.toString()).append("]");
return true;
}
@Override
public void onElementGroupEnded() {
mBuilder.append("\n");
}
@Override
public void onIterationEnded() {
mBuilder.append("]]\n");
}
@Override
public String toString() {
return mBuilder.toString();
}
}
private class InsertOperationConstrutor implements EntryElementIterator {
private final List<ContentProviderOperation> mOperationList;
private final int mBackReferenceIndex;
public InsertOperationConstrutor(List<ContentProviderOperation> operationList,
int backReferenceIndex) {
mOperationList = operationList;
mBackReferenceIndex = backReferenceIndex;
}
@Override
public void onIterationStarted() {
}
@Override
public void onIterationEnded() {
}
@Override
public void onElementGroupStarted(EntryLabel label) {
}
@Override
public void onElementGroupEnded() {
}
@Override
public boolean onElement(EntryElement elem) {
if (!elem.isEmpty()) {
elem.constructInsertOperation(mOperationList, mBackReferenceIndex);
}
return true;
}
}
private final int mVCardType;
private final Account mAccount;
private List<VCardEntry> mChildren;
@Override
public String toString() {
ToStringIterator iterator = new ToStringIterator();
iterateAllData(iterator);
return iterator.toString();
}
public VCardEntry() {
this(VCardConfig.VCARD_TYPE_V21_GENERIC);
}
public VCardEntry(int vcardType) {
this(vcardType, null);
}
public VCardEntry(int vcardType, Account account) {
mVCardType = vcardType;
mAccount = account;
}
private void addPhone(int type, String data, String label, boolean isPrimary) {
if (mPhoneList == null) {
mPhoneList = new ArrayList<PhoneData>();
}
final StringBuilder builder = new StringBuilder();
final String trimmed = data.trim();
final String formattedNumber;
if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
formattedNumber = trimmed;
} else {
// TODO: from the view of vCard spec these auto conversions should be removed.
// Note that some other codes (like the phone number formatter) or modules expect this
// auto conversion (bug 5178723), so just omitting this code won't be preferable enough
// (bug 4177894)
boolean hasPauseOrWait = false;
final int length = trimmed.length();
for (int i = 0; i < length; i++) {
char ch = trimmed.charAt(i);
// See RFC 3601 and docs for PhoneNumberUtils for more info.
if (ch == 'p' || ch == 'P') {
builder.append(PhoneNumberUtils.PAUSE);
hasPauseOrWait = true;
} else if (ch == 'w' || ch == 'W') {
builder.append(PhoneNumberUtils.WAIT);
hasPauseOrWait = true;
} else if (PhoneNumberUtils.is12Key(ch) || (i == 0 && ch == '+')) {
builder.append(ch);
}
}
if (!hasPauseOrWait) {
final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
formattedNumber = PhoneNumberUtilsPort.formatNumber(
builder.toString(), formattingType);
} else {
formattedNumber = builder.toString();
}
}
PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary);
mPhoneList.add(phoneData);
}
private void addSip(String sipData, int type, String label, boolean isPrimary) {
if (mSipList == null) {
mSipList = new ArrayList<SipData>();
}
mSipList.add(new SipData(sipData, type, label, isPrimary));
}
private void addNickName(final String nickName) {
if (mNicknameList == null) {
mNicknameList = new ArrayList<NicknameData>();
}
mNicknameList.add(new NicknameData(nickName));
}
private void addEmail(int type, String data, String label, boolean isPrimary) {
if (mEmailList == null) {
mEmailList = new ArrayList<EmailData>();
}
mEmailList.add(new EmailData(data, type, label, isPrimary));
}
private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) {
if (mPostalList == null) {
mPostalList = new ArrayList<PostalData>(0);
}
mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary,
mVCardType));
}
/**
* Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or
* {@link #handleTitleValue(String)}.
*/
private void addNewOrganization(final String organizationName, final String departmentName,
final String titleName, final String phoneticName, int type, final boolean isPrimary) {
if (mOrganizationList == null) {
mOrganizationList = new ArrayList<OrganizationData>();
}
mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName,
phoneticName, type, isPrimary));
}
private static final List<String> sEmptyList = Collections
.unmodifiableList(new ArrayList<String>(0));
private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
if (sortAsCollection != null && sortAsCollection.size() != 0) {
if (sortAsCollection.size() > 1) {
Log.w(LOG_TAG,
"Incorrect multiple SORT_AS parameters detected: "
+ Arrays.toString(sortAsCollection.toArray()));
}
final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
.iterator().next(), mVCardType);
final StringBuilder builder = new StringBuilder();
for (final String elem : sortNames) {
builder.append(elem);
}
return builder.toString();
} else {
return null;
}
}
/**
* Set "ORG" related values to the appropriate data. If there's more than
* one {@link OrganizationData} objects, this input data are attached to the
* last one which does not have valid values (not including empty but only
* null). If there's no {@link OrganizationData} object, a new
* {@link OrganizationData} is created, whose title is set to null.
*/
private void handleOrgValue(final int type, List<String> orgList,
Map<String, Collection<String>> paramMap, boolean isPrimary) {
final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
if (orgList == null) {
orgList = sEmptyList;
}
final String organizationName;
final String departmentName;
final int size = orgList.size();
switch (size) {
case 0: {
organizationName = "";
departmentName = null;
break;
}
case 1: {
organizationName = orgList.get(0);
departmentName = null;
break;
}
default: { // More than 1.
organizationName = orgList.get(0);
// We're not sure which is the correct string for department.
// In order to keep all the data, concatinate the rest of elements.
StringBuilder builder = new StringBuilder();
for (int i = 1; i < size; i++) {
if (i > 1) {
builder.append(' ');
}
builder.append(orgList.get(i));
}
departmentName = builder.toString();
}
}
if (mOrganizationList == null) {
// Create new first organization entry, with "null" title which may be
// added via handleTitleValue().
addNewOrganization(organizationName, departmentName, null, phoneticName, type,
isPrimary);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
// Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
// e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
if (organizationData.mOrganizationName == null
&& organizationData.mDepartmentName == null) {
// Probably the "TITLE" property comes before the "ORG" property via
// handleTitleLine().
organizationData.mOrganizationName = organizationName;
organizationData.mDepartmentName = departmentName;
organizationData.mIsPrimary = isPrimary;
return;
}
}
// No OrganizatioData is available. Create another one, with "null" title, which may be
// added via handleTitleValue().
addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
}
/**
* Set "title" value to the appropriate data. If there's more than one
* OrganizationData objects, this input is attached to the last one which
* does not have valid title value (not including empty but only null). If
* there's no OrganizationData object, a new OrganizationData is created,
* whose company name is set to null.
*/
private void handleTitleValue(final String title) {
if (mOrganizationList == null) {
// Create new first organization entry, with "null" other info, which may be
// added via handleOrgValue().
addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
return;
}
for (OrganizationData organizationData : mOrganizationList) {
if (organizationData.mTitle == null) {
organizationData.mTitle = title;
return;
}
}
// No Organization is available. Create another one, with "null" other info, which may be
// added via handleOrgValue().
addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
}
private void addIm(int protocol, String customProtocol, String propValue, int type,
boolean isPrimary) {
if (mImList == null) {
mImList = new ArrayList<ImData>();
}
mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary));
}
private void addNote(final String note) {
if (mNoteList == null) {
mNoteList = new ArrayList<NoteData>(1);
}
mNoteList.add(new NoteData(note));
}
private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
if (mPhotoList == null) {
mPhotoList = new ArrayList<PhotoData>(1);
}
final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary);
mPhotoList.add(photoData);
}
/**
* Tries to extract paramMap, constructs SORT-AS parameter values, and store
* them in appropriate phonetic name variables. This method does not care
* the vCard version. Even when we have SORT-AS parameters in invalid
* versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't
* drop meaningful information. If we had this parameter in the N field of
* vCard 3.0, and the contact data also have SORT-STRING, we will prefer
* SORT-STRING, since it is regitimate property to be understood.
*/
private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
if (VCardConfig.isVersion30(mVCardType)
&& !(TextUtils.isEmpty(mNameData.mPhoneticFamily)
&& TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
.isEmpty(mNameData.mPhoneticGiven))) {
return;
}
final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
if (sortAsCollection != null && sortAsCollection.size() != 0) {
if (sortAsCollection.size() > 1) {
Log.w(LOG_TAG,
"Incorrect multiple SORT_AS parameters detected: "
+ Arrays.toString(sortAsCollection.toArray()));
}
final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
.iterator().next(), mVCardType);
int size = sortNames.size();
if (size > 3) {
size = 3;
}
switch (size) {
case 3:
mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$
case 2:
mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$
default:
mNameData.mPhoneticFamily = sortNames.get(0);
break;
}
}
}
@SuppressWarnings("fallthrough")
private void handleNProperty(final List<String> paramValues,
Map<String, Collection<String>> paramMap) {
// in vCard 4.0, SORT-AS parameter is available.
tryHandleSortAsName(paramMap);
// Family, Given, Middle, Prefix, Suffix. (1 - 5)
int size;
if (paramValues == null || (size = paramValues.size()) < 1) {
return;
}
if (size > 5) {
size = 5;
}
switch (size) {
// Fall-through.
case 5:
mNameData.mSuffix = paramValues.get(4);
case 4:
mNameData.mPrefix = paramValues.get(3);
case 3:
mNameData.mMiddle = paramValues.get(2);
case 2:
mNameData.mGiven = paramValues.get(1);
default:
mNameData.mFamily = paramValues.get(0);
}
}
/**
* Note: Some Japanese mobile phones use this field for phonetic name, since
* vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the
* field has some ';'s in it. Assume the ';' means the same meaning in N
* property
*/
@SuppressWarnings("fallthrough")
private void handlePhoneticNameFromSound(List<String> elems) {
if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily)
&& TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
.isEmpty(mNameData.mPhoneticGiven))) {
// This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
// Ignore "SOUND;X-IRMC-N".
return;
}
int size;
if (elems == null || (size = elems.size()) < 1) {
return;
}
// Assume that the order is "Family, Given, Middle".
// This is not from specification but mere assumption. Some Japanese
// phones use this order.
if (size > 3) {
size = 3;
}
if (elems.get(0).length() > 0) {
boolean onlyFirstElemIsNonEmpty = true;
for (int i = 1; i < size; i++) {
if (elems.get(i).length() > 0) {
onlyFirstElemIsNonEmpty = false;
break;
}
}
if (onlyFirstElemIsNonEmpty) {
final String[] namesArray = elems.get(0).split(" ");
final int nameArrayLength = namesArray.length;
if (nameArrayLength == 3) {
// Assume the string is "Family Middle Given".
mNameData.mPhoneticFamily = namesArray[0];
mNameData.mPhoneticMiddle = namesArray[1];
mNameData.mPhoneticGiven = namesArray[2];
} else if (nameArrayLength == 2) {
// Assume the string is "Family Given" based on the Japanese mobile
// phones' preference.
mNameData.mPhoneticFamily = namesArray[0];
mNameData.mPhoneticGiven = namesArray[1];
} else {
mNameData.mPhoneticGiven = elems.get(0);
}
return;
}
}
switch (size) {
// fallthrough
case 3:
mNameData.mPhoneticMiddle = elems.get(2);
case 2:
mNameData.mPhoneticGiven = elems.get(1);
default:
mNameData.mPhoneticFamily = elems.get(0);
}
}
public void addProperty(final VCardProperty property) {
final String propertyName = property.getName();
final Map<String, Collection<String>> paramMap = property.getParameterMap();
final List<String> propertyValueList = property.getValueList();
byte[] propertyBytes = property.getByteValue();
if ((propertyValueList == null || propertyValueList.size() == 0)
&& propertyBytes == null) {
return;
}
final String propValue = (propertyValueList != null
? listToString(propertyValueList).trim()
: null);
if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) {
// vCard version. Ignore this.
} else if (propertyName.equals(VCardConstants.PROPERTY_FN)) {
mNameData.mFormatted = propValue;
} else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) {
// Only in vCard 3.0. Use this if FN doesn't exist though it is
// required in vCard 3.0.
if (TextUtils.isEmpty(mNameData.mFormatted)) {
mNameData.mFormatted = propValue;
}
} else if (propertyName.equals(VCardConstants.PROPERTY_N)) {
handleNProperty(propertyValueList, paramMap);
} else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
mNameData.mSortString = propValue;
} else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME)
|| propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
addNickName(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) {
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null
&& typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
// As of 2009-10-08, Parser side does not split a property value into separated
// values using ';' (in other words, propValueList.size() == 1),
// which is correct behavior from the view of vCard 2.1.
// But we want it to be separated, so do the separation here.
final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue,
mVCardType);
handlePhoneticNameFromSound(phoneticNameList);
} else {
// Ignore this field since Android cannot understand what it is.
}
} else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) {
boolean valuesAreAllEmpty = true;
for (String value : propertyValueList) {
if (!TextUtils.isEmpty(value)) {
valuesAreAllEmpty = false;
break;
}
}
if (valuesAreAllEmpty) {
return;
}
int type = -1;
String label = null;
boolean isPrimary = false;
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (final String typeStringOrg : typeCollection) {
final String typeStringUpperCase = typeStringOrg.toUpperCase();
if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
type = StructuredPostal.TYPE_HOME;
label = null;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)
|| typeStringUpperCase
.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
// "COMPANY" seems emitted by Windows Mobile, which is not
// specifically supported by vCard 2.1. We assume this is same
// as "WORK".
type = StructuredPostal.TYPE_WORK;
label = null;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL)
|| typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM)
|| typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
// We do not have any appropriate way to store this information.
} else if (type < 0) { // If no other type is specified before.
type = StructuredPostal.TYPE_CUSTOM;
if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
label = typeStringOrg.substring(2);
} else {
label = typeStringOrg;
}
}
}
}
// We use "HOME" as default
if (type < 0) {
type = StructuredPostal.TYPE_HOME;
}
addPostal(type, propertyValueList, label, isPrimary);
} else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) {
int type = -1;
String label = null;
boolean isPrimary = false;
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (final String typeStringOrg : typeCollection) {
final String typeStringUpperCase = typeStringOrg.toUpperCase();
if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
type = Email.TYPE_HOME;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
type = Email.TYPE_WORK;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
type = Email.TYPE_MOBILE;
} else if (type < 0) { // If no other type is specified before
if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
label = typeStringOrg.substring(2);
} else {
label = typeStringOrg;
}
type = Email.TYPE_CUSTOM;
}
}
}
if (type < 0) {
type = Email.TYPE_OTHER;
}
addEmail(type, propValue, label, isPrimary);
} else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) {
// vCard specification does not specify other types.
final int type = Organization.TYPE_WORK;
boolean isPrimary = false;
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
}
}
}
handleOrgValue(type, propertyValueList, paramMap, isPrimary);
} else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) {
handleTitleValue(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) {
// This conflicts with TITLE. Ignore for now...
// handleTitleValue(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO)
|| propertyName.equals(VCardConstants.PROPERTY_LOGO)) {
Collection<String> paramMapValue = paramMap.get("VALUE");
if (paramMapValue != null && paramMapValue.contains("URL")) {
// Currently we do not have appropriate example for testing this case.
} else {
final Collection<String> typeCollection = paramMap.get("TYPE");
String formatName = null;
boolean isPrimary = false;
if (typeCollection != null) {
for (String typeValue : typeCollection) {
if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
isPrimary = true;
} else if (formatName == null) {
formatName = typeValue;
}
}
}
addPhotoBytes(formatName, propertyBytes, isPrimary);
}
} else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) {
String phoneNumber = null;
boolean isSip = false;
if (VCardConfig.isVersion40(mVCardType)) {
// Given propValue is in URI format, not in phone number format used until
// vCard 3.0.
if (propValue.startsWith("sip:")) {
isSip = true;
} else if (propValue.startsWith("tel:")) {
phoneNumber = propValue.substring(4);
} else {
// We don't know appropriate way to handle the other schemas. Also,
// we may still have non-URI phone number. To keep given data as much as
// we can, just save original value here.
phoneNumber = propValue;
}
} else {
phoneNumber = propValue;
}
if (isSip) {
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
handleSipCase(propValue, typeCollection);
} else {
if (propValue.length() == 0) {
return;
}
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection,
phoneNumber);
final int type;
final String label;
if (typeObject instanceof Integer) {
type = (Integer) typeObject;
label = null;
} else {
type = Phone.TYPE_CUSTOM;
label = typeObject.toString();
}
final boolean isPrimary;
if (typeCollection != null &&
typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else {
isPrimary = false;
}
addPhone(type, phoneNumber, label, isPrimary);
}
} else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
// The phone number available via Skype.
Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
final int type = Phone.TYPE_OTHER;
final boolean isPrimary;
if (typeCollection != null
&& typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else {
isPrimary = false;
}
addPhone(type, propValue, null, isPrimary);
} else if (sImMap.containsKey(propertyName)) {
final int protocol = sImMap.get(propertyName);
boolean isPrimary = false;
int type = -1;
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
if (typeCollection != null) {
for (String typeString : typeCollection) {
if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (type < 0) {
if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
type = Im.TYPE_HOME;
} else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
type = Im.TYPE_WORK;
}
}
}
}
if (type < 0) {
type = Im.TYPE_HOME;
}
addIm(protocol, null, propValue, type, isPrimary);
} else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) {
addNote(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_URL)) {
if (mWebsiteList == null) {
mWebsiteList = new ArrayList<WebsiteData>(1);
}
mWebsiteList.add(new WebsiteData(propValue));
} else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) {
mBirthday = new BirthdayData(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
mAnniversary = new AnniversaryData(propValue);
} else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
mNameData.mPhoneticGiven = propValue;
} else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
mNameData.mPhoneticMiddle = propValue;
} else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
mNameData.mPhoneticFamily = propValue;
} else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) {
// See also RFC 4770 (for vCard 3.0)
if (propValue.startsWith("sip:")) {
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
handleSipCase(propValue, typeCollection);
}
} else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) {
if (!TextUtils.isEmpty(propValue)) {
final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
handleSipCase(propValue, typeCollection);
}
} else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue,
mVCardType);
handleAndroidCustomProperty(customPropertyList);
} else if (propertyName.toUpperCase().startsWith("X-")) {
// Catch all for X- properties. The caller can decide what to do with these.
if (mUnknownXData == null) {
mUnknownXData = new ArrayList<Pair<String, String>>();
}
mUnknownXData.add(new Pair<String, String>(propertyName, propValue));
} else {
}
// Be careful when adding some logic here, as some blocks above may use "return".
}
/**
* @param propValue may contain "sip:" at the beginning.
* @param typeCollection
*/
private void handleSipCase(String propValue, Collection<String> typeCollection) {
if (TextUtils.isEmpty(propValue)) {
return;
}
if (propValue.startsWith("sip:")) {
propValue = propValue.substring(4);
if (propValue.length() == 0) {
return;
}
}
int type = -1;
String label = null;
boolean isPrimary = false;
if (typeCollection != null) {
for (final String typeStringOrg : typeCollection) {
final String typeStringUpperCase = typeStringOrg.toUpperCase();
if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
isPrimary = true;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
type = SipAddress.TYPE_HOME;
} else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
type = SipAddress.TYPE_WORK;
} else if (type < 0) { // If no other type is specified before
if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
label = typeStringOrg.substring(2);
} else {
label = typeStringOrg;
}
type = SipAddress.TYPE_CUSTOM;
}
}
}
if (type < 0) {
type = SipAddress.TYPE_OTHER;
}
addSip(propValue, type, label, isPrimary);
}
public void addChild(VCardEntry child) {
if (mChildren == null) {
mChildren = new ArrayList<VCardEntry>();
}
mChildren.add(child);
}
private void handleAndroidCustomProperty(final List<String> customPropertyList) {
if (mAndroidCustomDataList == null) {
mAndroidCustomDataList = new ArrayList<AndroidCustomData>();
}
mAndroidCustomDataList
.add(AndroidCustomData.constructAndroidCustomData(customPropertyList));
}
/**
* Construct the display name. The constructed data must not be null.
*/
private String constructDisplayName() {
String displayName = null;
// FullName (created via "FN" or "NAME" field) is prefered.
if (!TextUtils.isEmpty(mNameData.mFormatted)) {
displayName = mNameData.mFormatted;
} else if (!mNameData.emptyStructuredName()) {
displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily,
mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix);
} else if (!mNameData.emptyPhoneticStructuredName()) {
displayName = VCardUtils.constructNameFromElements(mVCardType,
mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven);
} else if (mEmailList != null && mEmailList.size() > 0) {
displayName = mEmailList.get(0).mAddress;
} else if (mPhoneList != null && mPhoneList.size() > 0) {
displayName = mPhoneList.get(0).mNumber;
} else if (mPostalList != null && mPostalList.size() > 0) {
displayName = mPostalList.get(0).getFormattedAddress(mVCardType);
} else if (mOrganizationList != null && mOrganizationList.size() > 0) {
displayName = mOrganizationList.get(0).getFormattedString();
}
if (displayName == null) {
displayName = "";
}
return displayName;
}
/**
* Consolidate several fielsds (like mName) using name candidates,
*/
public void consolidateFields() {
mNameData.displayName = constructDisplayName();
}
/**
* @return true when this object has nothing meaningful for Android's
* Contacts, and thus is "ignorable" for Android's Contacts. This
* does not mean an original vCard is really empty. Even when the
* original vCard has some fields, this may ignore it if those
* fields cannot be transcoded into Android's Contacts
* representation.
*/
public boolean isIgnorable() {
IsIgnorableIterator iterator = new IsIgnorableIterator();
iterateAllData(iterator);
return iterator.getResult();
}
/**
* Constructs the list of insert operation for this object. When the
* operationList argument is null, this method creates a new ArrayList and
* return it. The returned object is filled with new insert operations for
* this object. When operationList argument is not null, this method appends
* those new operations into the object instead of creating a new ArrayList.
*
* @param resolver {@link ContentResolver} object to be used in this method.
* @param operationList object to be filled. You can use this argument to
* concatinate operation lists. If null, this method creates a
* new array object.
* @return If operationList argument is null, new object with new insert
* operations. If it is not null, the operationList object with
* operations inserted by this method.
*/
public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver,
ArrayList<ContentProviderOperation> operationList) {
if (operationList == null) {
operationList = new ArrayList<ContentProviderOperation>();
}
if (isIgnorable()) {
return operationList;
}
final int backReferenceIndex = operationList.size();
// After applying the batch the first result's Uri is returned so it is important that
// the RawContact is the first operation that gets inserted into the list.
ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(RawContacts.CONTENT_URI);
if (mAccount != null) {
builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
} else {
builder.withValue(RawContacts.ACCOUNT_NAME, null);
builder.withValue(RawContacts.ACCOUNT_TYPE, null);
}
// contacts favorites
if (getStarred()) {
builder.withValue(RawContacts.STARRED, 1);
}
operationList.add(builder.build());
int start = operationList.size();
iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex));
int end = operationList.size();
return operationList;
}
public static VCardEntry buildFromResolver(ContentResolver resolver) {
return buildFromResolver(resolver, Contacts.CONTENT_URI);
}
public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
return null;
}
private String listToString(List<String> list) {
final int size = list.size();
if (size > 1) {
StringBuilder builder = new StringBuilder();
int i = 0;
for (String type : list) {
builder.append(type);
if (i < size - 1) {
builder.append(";");
}
}
return builder.toString();
} else if (size == 1) {
return list.get(0);
} else {
return "";
}
}
public final NameData getNameData() {
return mNameData;
}
public final List<NicknameData> getNickNameList() {
return mNicknameList;
}
public final String getBirthday() {
return mBirthday != null ? mBirthday.mBirthday : null;
}
public final List<NoteData> getNotes() {
return mNoteList;
}
public final List<PhoneData> getPhoneList() {
return mPhoneList;
}
public final List<EmailData> getEmailList() {
return mEmailList;
}
public final List<PostalData> getPostalList() {
return mPostalList;
}
public final List<OrganizationData> getOrganizationList() {
return mOrganizationList;
}
public final List<ImData> getImList() {
return mImList;
}
public final List<PhotoData> getPhotoList() {
return mPhotoList;
}
public final List<WebsiteData> getWebsiteList() {
return mWebsiteList;
}
/**
* @hide this interface may be changed for better support of vCard 4.0 (UID)
*/
public final List<VCardEntry> getChildlen() {
return mChildren;
}
public String getDisplayName() {
if (mNameData.displayName == null) {
mNameData.displayName = constructDisplayName();
}
return mNameData.displayName;
}
public List<Pair<String, String>> getUnknownXData() {
return mUnknownXData;
}
}