| /* |
| * 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; |
| } |
| // {@link ContactsContract} has a {@link StructuredPostal.TYPE_OTHER}, so |
| // if the custom type is "other", map it from {@code TYPE_CUSTOM} to |
| // {@code TYPE_OTHER}. |
| if (VCardConstants.PARAM_ADR_EXTRA_TYPE_OTHER.equals(label.toUpperCase())) { |
| type = StructuredPostal.TYPE_OTHER; |
| label = null; |
| } |
| } |
| } |
| } |
| // 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; |
| } |
| } |