/* * 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 sImMap = new HashMap(); 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); } 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN public AndroidCustomData(String mimeType, List dataList) { mMimeType = mimeType; mDataList = dataList; } public static AndroidCustomData constructAndroidCustomData(List list) { String mimeType; List 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 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 getDataList() { return mDataList; } } private final NameData mNameData = new NameData(); private List mPhoneList; private List mEmailList; private List mPostalList; private List mOrganizationList; private List mImList; private List mPhotoList; private List mWebsiteList; private List mSipList; private List mNicknameList; private List mNoteList; private List mAndroidCustomDataList; private BirthdayData mBirthday; private AnniversaryData mAnniversary; private List> 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 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 mOperationList; private final int mBackReferenceIndex; public InsertOperationConstrutor(List 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 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(); } 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 (('0' <= ch && ch <= '9') || (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(); } mSipList.add(new SipData(sipData, type, label, isPrimary)); } private void addNickName(final String nickName) { if (mNicknameList == null) { mNicknameList = new ArrayList(); } mNicknameList.add(new NicknameData(nickName)); } private void addEmail(int type, String data, String label, boolean isPrimary) { if (mEmailList == null) { mEmailList = new ArrayList(); } mEmailList.add(new EmailData(data, type, label, isPrimary)); } private void addPostal(int type, List propValueList, String label, boolean isPrimary) { if (mPostalList == null) { mPostalList = new ArrayList(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(); } mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName, phoneticName, type, isPrimary)); } private static final List sEmptyList = Collections .unmodifiableList(new ArrayList(0)); private String buildSinglePhoneticNameFromSortAsParam(Map> paramMap) { final Collection 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 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 orgList, Map> 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(); } mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary)); } private void addNote(final String note) { if (mNoteList == null) { mNoteList = new ArrayList(1); } mNoteList.add(new NoteData(note)); } private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { if (mPhotoList == null) { mPhotoList = new ArrayList(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> paramMap) { if (VCardConfig.isVersion30(mVCardType) && !(TextUtils.isEmpty(mNameData.mPhoneticFamily) && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils .isEmpty(mNameData.mPhoneticGiven))) { return; } final Collection 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 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 paramValues, Map> 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 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> paramMap = property.getParameterMap(); final List 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 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 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 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 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 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 paramMapValue = paramMap.get("VALUE"); if (paramMapValue != null && paramMapValue.contains("URL")) { // Currently we do not have appropriate example for testing this case. } else { final Collection 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 typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); handleSipCase(propValue, typeCollection); } else { if (propValue.length() == 0) { return; } final Collection 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 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 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(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 typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); handleSipCase(propValue, typeCollection); } } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) { if (!TextUtils.isEmpty(propValue)) { final Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); handleSipCase(propValue, typeCollection); } } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { final List 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>(); } mUnknownXData.add(new Pair(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 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(); } mChildren.add(child); } private void handleAndroidCustomProperty(final List customPropertyList) { if (mAndroidCustomDataList == null) { mAndroidCustomDataList = new ArrayList(); } 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 constructInsertOperations(ContentResolver resolver, ArrayList operationList) { if (operationList == null) { operationList = new ArrayList(); } 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); } 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 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 getNickNameList() { return mNicknameList; } public final String getBirthday() { return mBirthday != null ? mBirthday.mBirthday : null; } public final List getNotes() { return mNoteList; } public final List getPhoneList() { return mPhoneList; } public final List getEmailList() { return mEmailList; } public final List getPostalList() { return mPostalList; } public final List getOrganizationList() { return mOrganizationList; } public final List getImList() { return mImList; } public final List getPhotoList() { return mPhotoList; } public final List getWebsiteList() { return mWebsiteList; } /** * @hide this interface may be changed for better support of vCard 4.0 (UID) */ public final List getChildlen() { return mChildren; } public String getDisplayName() { if (mNameData.displayName == null) { mNameData.displayName = constructDisplayName(); } return mNameData.displayName; } public List> getUnknownXData() { return mUnknownXData; } }