com.silentcircle.vcard.VCardEntry.java Source code

Java tutorial

Introduction

Here is the source code for com.silentcircle.vcard.VCardEntry.java

Source

/*
Copyright (C) 2016, Silent Circle, LLC.  All rights reserved.
    
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Any redistribution, use, or modification is done solely for personal
  benefit and not for any commercial purpose or for monetary gain
* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Silent Circle nor the
  names of its contributors may be used to endorse or promote products
  derived from this software without specific prior written permission.
    
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/*
 * This  implementation is an edited version of original Android sources.
 */

/*
 * 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.silentcircle.vcard;

import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.net.Uri;
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.Data;
import android.provider.ContactsContract.RawContacts;
import android.support.v4.util.Pair;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;

import com.silentcircle.vcard.VCardUtils.PhoneNumberUtilsPort;

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()}.
 */
@SuppressLint("DefaultLocale")
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_SILENT, Im.PROTOCOL_CUSTOM);
        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 interface EntryElement {
        // Also need to inherit toString(), equals().
        EntryLabel getEntryLabel();

        void constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex);

        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 com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.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 com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.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 com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Im#PROTOCOL}. e.g. {@link com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.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 {
        void onIterationStarted();

        void onIterationEnded();

        /**
         * Called when there are one or more {@link EntryElement} instances associated with {@link EntryLabel}.
         */
        void onElementGroupStarted(EntryLabel label);

        /**
         * Called after all {@link EntryElement} instances for {@link EntryLabel} provided on
         * {@link #onElementGroupStarted(EntryLabel)} being processed by {@link #onElement(EntryElement)}
         */
        void onElementGroupEnded();

        /**
         * @return should be true when child wants to continue the operation. False otherwise.
         */
        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 (('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<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, java.util.List, java.util.Map, boolean)} or {@link #handleTitleValue(String)}.
     */
    private void addNewOrganization(final String organizationName, final String departmentName,
            final String titleName, final String phoneticName, int type, final boolean isPrimary) {
        if (mOrganizationList == null) {
            mOrganizationList = new ArrayList<OrganizationData>();
        }
        mOrganizationList.add(
                new OrganizationData(organizationName, departmentName, titleName, phoneticName, type, isPrimary));
    }

    private static final List<String> sEmptyList = Collections.unmodifiableList(new ArrayList<String>(0));

    private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
        final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
        if (sortAsCollection != null && sortAsCollection.size() != 0) {
            if (sortAsCollection.size() > 1) {
                Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: "
                        + Arrays.toString(sortAsCollection.toArray()));
            }
            final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection.iterator().next(),
                    mVCardType);
            final StringBuilder builder = new StringBuilder();
            for (final String elem : sortNames) {
                builder.append(elem);
            }
            return builder.toString();
        } else {
            return null;
        }
    }

    /**
     * Set "ORG" related values to the appropriate data. If there's more than one {@link OrganizationData} objects, this input
     * data are attached to the last one which does not have valid values (not including empty but only null). If there's no
     * {@link OrganizationData} object, a new {@link OrganizationData} is created, whose title is set to null.
     */
    private void handleOrgValue(final int type, List<String> orgList, Map<String, Collection<String>> paramMap,
            boolean isPrimary) {
        final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
        if (orgList == null) {
            orgList = sEmptyList;
        }
        final String organizationName;
        final String departmentName;
        final int size = orgList.size();
        switch (size) {
        case 0: {
            organizationName = "";
            departmentName = null;
            break;
        }
        case 1: {
            organizationName = orgList.get(0);
            departmentName = null;
            break;
        }
        default: { // More than 1.
            organizationName = orgList.get(0);
            // We're not sure which is the correct string for department.
            // In order to keep all the data, concatinate the rest of elements.
            StringBuilder builder = new StringBuilder();
            for (int i = 1; i < size; i++) {
                if (i > 1) {
                    builder.append(' ');
                }
                builder.append(orgList.get(i));
            }
            departmentName = builder.toString();
        }
        }
        if (mOrganizationList == null) {
            // Create new first organization entry, with "null" title which may be
            // added via handleTitleValue().
            addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
            return;
        }
        for (OrganizationData organizationData : mOrganizationList) {
            // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
            // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
            if (organizationData.mOrganizationName == null && organizationData.mDepartmentName == null) {
                // Probably the "TITLE" property comes before the "ORG" property via
                // handleTitleLine().
                organizationData.mOrganizationName = organizationName;
                organizationData.mDepartmentName = departmentName;
                organizationData.mIsPrimary = isPrimary;
                return;
            }
        }
        // No OrganizatioData is available. Create another one, with "null" title, which may be
        // added via handleTitleValue().
        addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
    }

    /**
     * Set "title" value to the appropriate data. If there's more than one OrganizationData objects, this input is attached to the
     * last one which does not have valid title value (not including empty but only null). If there's no OrganizationData object,
     * a new OrganizationData is created, whose company name is set to null.
     */
    private void handleTitleValue(final String title) {
        if (mOrganizationList == null) {
            // Create new first organization entry, with "null" other info, which may be
            // added via handleOrgValue().
            addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
            return;
        }
        for (OrganizationData organizationData : mOrganizationList) {
            if (organizationData.mTitle == null) {
                organizationData.mTitle = title;
                return;
            }
        }
        // No Organization is available. Create another one, with "null" other info, which may be
        // added via handleOrgValue().
        addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
    }

    private void addIm(int protocol, String customProtocol, String propValue, int type, boolean isPrimary) {
        if (mImList == null) {
            mImList = new ArrayList<ImData>();
        }
        mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary));
    }

    private void addNote(final String note) {
        if (mNoteList == null) {
            mNoteList = new ArrayList<NoteData>(1);
        }
        mNoteList.add(new NoteData(note));
    }

    private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
        if (mPhotoList == null) {
            mPhotoList = new ArrayList<PhotoData>(1);
        }
        final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary);
        mPhotoList.add(photoData);
    }

    /**
     * Tries to extract paramMap, constructs SORT-AS parameter values, and store them in appropriate phonetic name variables. This
     * method does not care the vCard version. Even when we have SORT-AS parameters in invalid versions (i.e. 2.1 and 3.0), we
     * scilently accept them so that we won't drop meaningful information. If we had this parameter in the N field of vCard 3.0,
     * and the contact data also have SORT-STRING, we will prefer SORT-STRING, since it is regitimate property to be understood.
     */
    private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
        if (VCardConfig.isVersion30(mVCardType) && !(TextUtils.isEmpty(mNameData.mPhoneticFamily)
                && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils.isEmpty(mNameData.mPhoneticGiven))) {
            return;
        }

        final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
        if (sortAsCollection != null && sortAsCollection.size() != 0) {
            if (sortAsCollection.size() > 1) {
                Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: "
                        + Arrays.toString(sortAsCollection.toArray()));
            }
            final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection.iterator().next(),
                    mVCardType);
            int size = sortNames.size();
            if (size > 3) {
                size = 3;
            }
            switch (size) {
            case 3:
                mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$
            case 2:
                mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$
            default:
                mNameData.mPhoneticFamily = sortNames.get(0);
                break;
            }
        }
    }

    @SuppressWarnings("fallthrough")
    private void handleNProperty(final List<String> paramValues, Map<String, Collection<String>> paramMap) {
        // in vCard 4.0, SORT-AS parameter is available.
        tryHandleSortAsName(paramMap);

        // Family, Given, Middle, Prefix, Suffix. (1 - 5)
        int size;
        if (paramValues == null || (size = paramValues.size()) < 1) {
            return;
        }
        if (size > 5) {
            size = 5;
        }

        switch (size) {
        // Fall-through.
        case 5:
            mNameData.mSuffix = paramValues.get(4);
        case 4:
            mNameData.mPrefix = paramValues.get(3);
        case 3:
            mNameData.mMiddle = paramValues.get(2);
        case 2:
            mNameData.mGiven = paramValues.get(1);
        default:
            mNameData.mFamily = paramValues.get(0);
        }
    }

    /**
     * Note: Some Japanese mobile phones use this field for phonetic name, since vCard 2.1 does not have "SORT-STRING" type. Also,
     * in some cases, the field has some ';'s in it. Assume the ';' means the same meaning in N property
     */
    @SuppressWarnings("fallthrough")
    private void handlePhoneticNameFromSound(List<String> elems) {
        if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily) && TextUtils.isEmpty(mNameData.mPhoneticMiddle)
                && TextUtils.isEmpty(mNameData.mPhoneticGiven))) {
            // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
            // Ignore "SOUND;X-IRMC-N".
            return;
        }

        int size;
        if (elems == null || (size = elems.size()) < 1) {
            return;
        }

        // Assume that the order is "Family, Given, Middle".
        // This is not from specification but mere assumption. Some Japanese
        // phones use this order.
        if (size > 3) {
            size = 3;
        }

        if (elems.get(0).length() > 0) {
            boolean onlyFirstElemIsNonEmpty = true;
            for (int i = 1; i < size; i++) {
                if (elems.get(i).length() > 0) {
                    onlyFirstElemIsNonEmpty = false;
                    break;
                }
            }
            if (onlyFirstElemIsNonEmpty) {
                final String[] namesArray = elems.get(0).split(" ");
                final int nameArrayLength = namesArray.length;
                if (nameArrayLength == 3) {
                    // Assume the string is "Family Middle Given".
                    mNameData.mPhoneticFamily = namesArray[0];
                    mNameData.mPhoneticMiddle = namesArray[1];
                    mNameData.mPhoneticGiven = namesArray[2];
                } else if (nameArrayLength == 2) {
                    // Assume the string is "Family Given" based on the Japanese mobile
                    // phones' preference.
                    mNameData.mPhoneticFamily = namesArray[0];
                    mNameData.mPhoneticGiven = namesArray[1];
                } else {
                    mNameData.mPhoneticGiven = elems.get(0);
                }
                return;
            }
        }

        switch (size) {
        // fallthrough
        case 3:
            mNameData.mPhoneticMiddle = elems.get(2);
        case 2:
            mNameData.mPhoneticGiven = elems.get(1);
        default:
            mNameData.mPhoneticFamily = elems.get(0);
        }
    }

    public void addProperty(final VCardProperty property) {
        final String propertyName = property.getName();
        final Map<String, Collection<String>> paramMap = property.getParameterMap();
        final List<String> propertyValueList = property.getValueList();
        byte[] propertyBytes = property.getByteValue();

        if ((propertyValueList == null || propertyValueList.size() == 0) && propertyBytes == null) {
            return;
        }
        final String propValue = (propertyValueList != null ? listToString(propertyValueList).trim() : null);

        if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) {
            // vCard version. Ignore this.
        } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) {
            mNameData.mFormatted = propValue;
        } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) {
            // Only in vCard 3.0. Use this if FN doesn't exist though it is
            // required in vCard 3.0.
            if (TextUtils.isEmpty(mNameData.mFormatted)) {
                mNameData.mFormatted = propValue;
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_N)) {
            handleNProperty(propertyValueList, paramMap);
        } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
            mNameData.mSortString = propValue;
        } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME)
                || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
            addNickName(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) {
            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
                // As of 2009-10-08, Parser side does not split a property value into separated
                // values using ';' (in other words, propValueList.size() == 1),
                // which is correct behavior from the view of vCard 2.1.
                // But we want it to be separated, so do the separation here.
                final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue, mVCardType);
                handlePhoneticNameFromSound(phoneticNameList);
            } else {
                // Ignore this field since Android cannot understand what it is.
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) {
            boolean valuesAreAllEmpty = true;
            for (String value : propertyValueList) {
                if (!TextUtils.isEmpty(value)) {
                    valuesAreAllEmpty = false;
                    break;
                }
            }
            if (valuesAreAllEmpty) {
                return;
            }

            int type = -1;
            String label = null;
            boolean isPrimary = false;
            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            if (typeCollection != null) {
                for (final String typeStringOrg : typeCollection) {
                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
                        isPrimary = true;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
                        type = StructuredPostal.TYPE_HOME;
                        label = null;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)
                            || typeStringUpperCase.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
                        // "COMPANY" seems emitted by Windows Mobile, which is not
                        // specifically supported by vCard 2.1. We assume this is same
                        // as "WORK".
                        type = StructuredPostal.TYPE_WORK;
                        label = null;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL)
                            || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM)
                            || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
                        // We do not have any appropriate way to store this information.
                    } else if (type < 0) { // If no other type is specified before.
                        type = StructuredPostal.TYPE_CUSTOM;
                        if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
                            label = typeStringOrg.substring(2);
                        } else {
                            label = typeStringOrg;
                        }
                    }
                }
            }
            // We use "HOME" as default
            if (type < 0) {
                type = StructuredPostal.TYPE_HOME;
            }

            addPostal(type, propertyValueList, label, isPrimary);
        } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) {
            int type = -1;
            String label = null;
            boolean isPrimary = false;
            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            if (typeCollection != null) {
                for (final String typeStringOrg : typeCollection) {
                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
                        isPrimary = true;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
                        type = Email.TYPE_HOME;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
                        type = Email.TYPE_WORK;
                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
                        type = Email.TYPE_MOBILE;
                    } else if (type < 0) { // If no other type is specified before
                        if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
                            label = typeStringOrg.substring(2);
                        } else {
                            label = typeStringOrg;
                        }
                        type = Email.TYPE_CUSTOM;
                    }
                }
            }
            if (type < 0) {
                type = Email.TYPE_OTHER;
            }
            addEmail(type, propValue, label, isPrimary);
        } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) {
            // vCard specification does not specify other types.
            final int type = Organization.TYPE_WORK;
            boolean isPrimary = false;
            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            if (typeCollection != null) {
                for (String typeString : typeCollection) {
                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
                        isPrimary = true;
                    }
                }
            }
            handleOrgValue(type, propertyValueList, paramMap, isPrimary);
        } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) {
            handleTitleValue(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) {
            // This conflicts with TITLE. Ignore for now...
            // handleTitleValue(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO)
                || propertyName.equals(VCardConstants.PROPERTY_LOGO)) {
            Collection<String> paramMapValue = paramMap.get("VALUE");
            if (paramMapValue != null && paramMapValue.contains("URL")) {
                // Currently we do not have appropriate example for testing this case.
            } else {
                final Collection<String> typeCollection = paramMap.get("TYPE");
                String formatName = null;
                boolean isPrimary = false;
                if (typeCollection != null) {
                    for (String typeValue : typeCollection) {
                        if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
                            isPrimary = true;
                        } else if (formatName == null) {
                            formatName = typeValue;
                        }
                    }
                }
                addPhotoBytes(formatName, propertyBytes, isPrimary);
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) {
            String phoneNumber = null;
            boolean isSip = false;
            if (VCardConfig.isVersion40(mVCardType)) {
                // Given propValue is in URI format, not in phone number format used until
                // vCard 3.0.
                if (propValue.startsWith("sip:")) {
                    isSip = true;
                } else if (propValue.startsWith("tel:")) {
                    phoneNumber = propValue.substring(4);
                } else {
                    // We don't know appropriate way to handle the other schemas. Also,
                    // we may still have non-URI phone number. To keep given data as much as
                    // we can, just save original value here.
                    phoneNumber = propValue;
                }
            } else {
                phoneNumber = propValue;
            }

            if (isSip) {
                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
                handleSipCase(propValue, typeCollection);
            } else {
                if (propValue.length() == 0) {
                    return;
                }

                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
                final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection, phoneNumber);
                final int type;
                final String label;
                if (typeObject instanceof Integer) {
                    type = (Integer) typeObject;
                    label = null;
                } else {
                    type = Phone.TYPE_CUSTOM;
                    if (typeObject != null)
                        label = typeObject.toString();
                    else {
                        label = null;
                        Log.i("VCardEntry", "No type object for phone number: " + phoneNumber
                                + "\ntype collection: " + typeCollection);
                    }
                }

                final boolean isPrimary;
                isPrimary = typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF);

                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_CUSTOM;
            final boolean isPrimary;
            isPrimary = typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF);
            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 mUuid) 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 android.content.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);
        // }
        builder.withValue(RawContacts.DISPLAY_NAME_PRIMARY, 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, RawContacts.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;
    }

    public final List<SipData> getSipList() {
        return mSipList;
    }

    /**
     * @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;
    }
}