/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.vcard;

import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;

import android.accounts.Account;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Represents one vCard entry, which should start with "BEGIN:VCARD" and end
 * with "END:VCARD". This class is for bridging between real vCard data and
 * Android's {@link ContactsContract}, which means some aspects of vCard are
 * dropped before this object being constructed. Raw vCard data should be first
 * supplied with {@link #addProperty(VCardProperty)}. After supplying all data,
 * user should call {@link #consolidateFields()} to prepare some additional
 * information which is constructable from supplied raw data. TODO: preserve raw
 * data using {@link VCardProperty}. If it may just waste memory, this at least
 * should contain them when it cannot convert vCard as a string to Android's
 * Contacts representation. Those raw properties should _not_ be used for
 * {@link #isIgnorable()}.
 */
public class VCardEntry {
    private static final String LOG_TAG = VCardConstants.LOG_TAG;

    private static final int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;

    private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();

    static {
        sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
        sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
        sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
        sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
        sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
        sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
        sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
        sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
                Im.PROTOCOL_GOOGLE_TALK);
    }

    public enum EntryLabel {
        NAME,
        PHONE,
        EMAIL,
        POSTAL_ADDRESS,
        ORGANIZATION,
        IM,
        PHOTO,
        WEBSITE,
        SIP,
        NICKNAME,
        NOTE,
        BIRTHDAY,
        ANNIVERSARY,
        ANDROID_CUSTOM
    }

    public static interface EntryElement {
        // Also need to inherit toString(), equals().
        public EntryLabel getEntryLabel();

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

        public boolean isEmpty();
    }

    // TODO: vCard 4.0 logically has multiple formatted names and we need to
    // select the most preferable one using PREF parameter.
    //
    // e.g. (based on rev.13)
    // FN;PREF=1:John M. Doe
    // FN;PREF=2:John Doe
    // FN;PREF=3;John
    public static class NameData implements EntryElement {
        private String mFamily;
        private String mGiven;
        private String mMiddle;
        private String mPrefix;
        private String mSuffix;

        // Used only when no family nor given name is found.
        private String mFormatted;

        private String mPhoneticFamily;
        private String mPhoneticGiven;
        private String mPhoneticMiddle;

        // For "SORT-STRING" in vCard 3.0.
        private String mSortString;

        /**
         * Not in vCard but for {@link StructuredName#DISPLAY_NAME}. This field
         * is constructed by VCardEntry on demand. Consider using
         * {@link VCardEntry#getDisplayName()}.
         */
        // This field should reflect the other Elem fields like Email,
        // PostalAddress, etc., while
        // This is static class which cannot see other data. Thus we ask
        // VCardEntry to populate it.
        public String displayName;

        public boolean emptyStructuredName() {
            return TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mGiven)
                    && TextUtils.isEmpty(mMiddle) && TextUtils.isEmpty(mPrefix)
                    && TextUtils.isEmpty(mSuffix);
        }

        public boolean emptyPhoneticStructuredName() {
            return TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticGiven)
                    && TextUtils.isEmpty(mPhoneticMiddle);
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);

            if (!TextUtils.isEmpty(mGiven)) {
                builder.withValue(StructuredName.GIVEN_NAME, mGiven);
            }
            if (!TextUtils.isEmpty(mFamily)) {
                builder.withValue(StructuredName.FAMILY_NAME, mFamily);
            }
            if (!TextUtils.isEmpty(mMiddle)) {
                builder.withValue(StructuredName.MIDDLE_NAME, mMiddle);
            }
            if (!TextUtils.isEmpty(mPrefix)) {
                builder.withValue(StructuredName.PREFIX, mPrefix);
            }
            if (!TextUtils.isEmpty(mSuffix)) {
                builder.withValue(StructuredName.SUFFIX, mSuffix);
            }

            boolean phoneticNameSpecified = false;

            if (!TextUtils.isEmpty(mPhoneticGiven)) {
                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGiven);
                phoneticNameSpecified = true;
            }
            if (!TextUtils.isEmpty(mPhoneticFamily)) {
                builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamily);
                phoneticNameSpecified = true;
            }
            if (!TextUtils.isEmpty(mPhoneticMiddle)) {
                builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddle);
                phoneticNameSpecified = true;
            }

            // SORT-STRING is used only when phonetic names aren't specified in
            // the original vCard.
            if (!phoneticNameSpecified) {
                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mSortString);
            }

            builder.withValue(StructuredName.DISPLAY_NAME, displayName);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return (TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mMiddle)
                    && TextUtils.isEmpty(mGiven) && TextUtils.isEmpty(mPrefix)
                    && TextUtils.isEmpty(mSuffix) && TextUtils.isEmpty(mFormatted)
                    && TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticMiddle)
                    && TextUtils.isEmpty(mPhoneticGiven) && TextUtils.isEmpty(mSortString));
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof NameData)) {
                return false;
            }
            NameData nameData = (NameData) obj;

            return (TextUtils.equals(mFamily, nameData.mFamily)
                    && TextUtils.equals(mMiddle, nameData.mMiddle)
                    && TextUtils.equals(mGiven, nameData.mGiven)
                    && TextUtils.equals(mPrefix, nameData.mPrefix)
                    && TextUtils.equals(mSuffix, nameData.mSuffix)
                    && TextUtils.equals(mFormatted, nameData.mFormatted)
                    && TextUtils.equals(mPhoneticFamily, nameData.mPhoneticFamily)
                    && TextUtils.equals(mPhoneticMiddle, nameData.mPhoneticMiddle)
                    && TextUtils.equals(mPhoneticGiven, nameData.mPhoneticGiven)
                    && TextUtils.equals(mSortString, nameData.mSortString));
        }

        @Override
        public int hashCode() {
            final String[] hashTargets = new String[] {mFamily, mMiddle, mGiven, mPrefix, mSuffix,
                    mFormatted, mPhoneticFamily, mPhoneticMiddle,
                    mPhoneticGiven, mSortString};
            int hash = 0;
            for (String hashTarget : hashTargets) {
                hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
            }
            return hash;
        }

        @Override
        public String toString() {
            return String.format("family: %s, given: %s, middle: %s, prefix: %s, suffix: %s",
                    mFamily, mGiven, mMiddle, mPrefix, mSuffix);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.NAME;
        }

        public String getFamily() {
            return mFamily;
        }

        public String getMiddle() {
            return mMiddle;
        }

        public String getGiven() {
            return mGiven;
        }

        public String getPrefix() {
            return mPrefix;
        }

        public String getSuffix() {
            return mSuffix;
        }

        public String getFormatted() {
            return mFormatted;
        }

        public String getSortString() {
            return mSortString;
        }

        /** @hide Just for testing. */
        public void setFamily(String family) { mFamily = family; }
        /** @hide Just for testing. */
        public void setMiddle(String middle) { mMiddle = middle; }
        /** @hide Just for testing. */
        public void setGiven(String given) { mGiven = given; }
        /** @hide Just for testing. */
        public void setPrefix(String prefix) { mPrefix = prefix; }
        /** @hide Just for testing. */
        public void setSuffix(String suffix) { mSuffix = suffix; }
    }

    public static class PhoneData implements EntryElement {
        private final String mNumber;
        private final int mType;
        private final String mLabel;

        // isPrimary is (not final but) changable, only when there's no
        // appropriate one existing
        // in the original VCard.
        private boolean mIsPrimary;

        public PhoneData(String data, int type, String label, boolean isPrimary) {
            mNumber = data;
            mType = type;
            mLabel = label;
            mIsPrimary = isPrimary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Phone.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);

            builder.withValue(Phone.TYPE, mType);
            if (mType == Phone.TYPE_CUSTOM) {
                builder.withValue(Phone.LABEL, mLabel);
            }
            builder.withValue(Phone.NUMBER, mNumber);
            if (mIsPrimary) {
                builder.withValue(Phone.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mNumber);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof PhoneData)) {
                return false;
            }
            PhoneData phoneData = (PhoneData) obj;
            return (mType == phoneData.mType
                    && TextUtils.equals(mNumber, phoneData.mNumber)
                    && TextUtils.equals(mLabel, phoneData.mLabel)
                    && (mIsPrimary == phoneData.mIsPrimary));
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + (mNumber != null ? mNumber.hashCode() : 0);
            hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            return hash;
        }

        @Override
        public String toString() {
            return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mNumber,
                    mLabel, mIsPrimary);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.PHONE;
        }

        public String getNumber() {
            return mNumber;
        }

        public int getType() {
            return mType;
        }

        public String getLabel() {
            return mLabel;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class EmailData implements EntryElement {
        private final String mAddress;
        private final int mType;
        // Used only when TYPE is TYPE_CUSTOM.
        private final String mLabel;
        private final boolean mIsPrimary;

        public EmailData(String data, int type, String label, boolean isPrimary) {
            mType = type;
            mAddress = data;
            mLabel = label;
            mIsPrimary = isPrimary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Email.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);

            builder.withValue(Email.TYPE, mType);
            if (mType == Email.TYPE_CUSTOM) {
                builder.withValue(Email.LABEL, mLabel);
            }
            builder.withValue(Email.DATA, mAddress);
            if (mIsPrimary) {
                builder.withValue(Data.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mAddress);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof EmailData)) {
                return false;
            }
            EmailData emailData = (EmailData) obj;
            return (mType == emailData.mType
                    && TextUtils.equals(mAddress, emailData.mAddress)
                    && TextUtils.equals(mLabel, emailData.mLabel)
                    && (mIsPrimary == emailData.mIsPrimary));
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
            hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            return hash;
        }

        @Override
        public String toString() {
            return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mAddress,
                    mLabel, mIsPrimary);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.EMAIL;
        }

        public String getAddress() {
            return mAddress;
        }

        public int getType() {
            return mType;
        }

        public String getLabel() {
            return mLabel;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class PostalData implements EntryElement {
        // Determined by vCard specification.
        // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
        private static final int ADDR_MAX_DATA_SIZE = 7;
        private final String mPobox;
        private final String mExtendedAddress;
        private final String mStreet;
        private final String mLocalty;
        private final String mRegion;
        private final String mPostalCode;
        private final String mCountry;
        private final int mType;
        private final String mLabel;
        private boolean mIsPrimary;

        /** We keep this for {@link StructuredPostal#FORMATTED_ADDRESS} */
        // TODO: need better way to construct formatted address.
        private int mVCardType;

        public PostalData(String pobox, String extendedAddress, String street, String localty,
                String region, String postalCode, String country, int type, String label,
                boolean isPrimary, int vcardType) {
            mType = type;
            mPobox = pobox;
            mExtendedAddress = extendedAddress;
            mStreet = street;
            mLocalty = localty;
            mRegion = region;
            mPostalCode = postalCode;
            mCountry = country;
            mLabel = label;
            mIsPrimary = isPrimary;
            mVCardType = vcardType;
        }

        /**
         * Accepts raw propertyValueList in vCard and constructs PostalData.
         */
        public static PostalData constructPostalData(final List<String> propValueList,
                final int type, final String label, boolean isPrimary, int vcardType) {
            final String[] dataArray = new String[ADDR_MAX_DATA_SIZE];

            int size = propValueList.size();
            if (size > ADDR_MAX_DATA_SIZE) {
                size = ADDR_MAX_DATA_SIZE;
            }

            // adr-value = 0*6(text-value ";") text-value
            // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name
            //
            // Use Iterator assuming List may be LinkedList, though actually it is
            // always ArrayList in the current implementation.
            int i = 0;
            for (String addressElement : propValueList) {
                dataArray[i] = addressElement;
                if (++i >= size) {
                    break;
                }
            }
            while (i < ADDR_MAX_DATA_SIZE) {
                dataArray[i++] = null;
            }

            return new PostalData(dataArray[0], dataArray[1], dataArray[2], dataArray[3],
                    dataArray[4], dataArray[5], dataArray[6], type, label, isPrimary, vcardType);
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);

            builder.withValue(StructuredPostal.TYPE, mType);
            if (mType == StructuredPostal.TYPE_CUSTOM) {
                builder.withValue(StructuredPostal.LABEL, mLabel);
            }

            final String streetString;
            if (TextUtils.isEmpty(mStreet)) {
                if (TextUtils.isEmpty(mExtendedAddress)) {
                    streetString = null;
                } else {
                    streetString = mExtendedAddress;
                }
            } else {
                if (TextUtils.isEmpty(mExtendedAddress)) {
                    streetString = mStreet;
                } else {
                    streetString = mStreet + " " + mExtendedAddress;
                }
            }
            builder.withValue(StructuredPostal.POBOX, mPobox);
            builder.withValue(StructuredPostal.STREET, streetString);
            builder.withValue(StructuredPostal.CITY, mLocalty);
            builder.withValue(StructuredPostal.REGION, mRegion);
            builder.withValue(StructuredPostal.POSTCODE, mPostalCode);
            builder.withValue(StructuredPostal.COUNTRY, mCountry);

            builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress(mVCardType));
            if (mIsPrimary) {
                builder.withValue(Data.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        public String getFormattedAddress(final int vcardType) {
            StringBuilder builder = new StringBuilder();
            boolean empty = true;
            final String[] dataArray = new String[] {
                    mPobox, mExtendedAddress, mStreet, mLocalty, mRegion, mPostalCode, mCountry
            };
            if (VCardConfig.isJapaneseDevice(vcardType)) {
                // In Japan, the order is reversed.
                for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
                    String addressPart = dataArray[i];
                    if (!TextUtils.isEmpty(addressPart)) {
                        if (!empty) {
                            builder.append(' ');
                        } else {
                            empty = false;
                        }
                        builder.append(addressPart);
                    }
                }
            } else {
                for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
                    String addressPart = dataArray[i];
                    if (!TextUtils.isEmpty(addressPart)) {
                        if (!empty) {
                            builder.append(' ');
                        } else {
                            empty = false;
                        }
                        builder.append(addressPart);
                    }
                }
            }

            return builder.toString().trim();
        }

        @Override
        public boolean isEmpty() {
            return (TextUtils.isEmpty(mPobox)
                    && TextUtils.isEmpty(mExtendedAddress)
                    && TextUtils.isEmpty(mStreet)
                    && TextUtils.isEmpty(mLocalty)
                    && TextUtils.isEmpty(mRegion)
                    && TextUtils.isEmpty(mPostalCode)
                    && TextUtils.isEmpty(mCountry));
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof PostalData)) {
                return false;
            }
            final PostalData postalData = (PostalData) obj;
            return (mType == postalData.mType)
                    && (mType == StructuredPostal.TYPE_CUSTOM ? TextUtils.equals(mLabel,
                            postalData.mLabel) : true)
                    && (mIsPrimary == postalData.mIsPrimary)
                    && TextUtils.equals(mPobox, postalData.mPobox)
                    && TextUtils.equals(mExtendedAddress, postalData.mExtendedAddress)
                    && TextUtils.equals(mStreet, postalData.mStreet)
                    && TextUtils.equals(mLocalty, postalData.mLocalty)
                    && TextUtils.equals(mRegion, postalData.mRegion)
                    && TextUtils.equals(mPostalCode, postalData.mPostalCode)
                    && TextUtils.equals(mCountry, postalData.mCountry);
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);

            final String[] hashTargets = new String[] {mPobox, mExtendedAddress, mStreet,
                    mLocalty, mRegion, mPostalCode, mCountry};
            for (String hashTarget : hashTargets) {
                hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
            }
            return hash;
        }

        @Override
        public String toString() {
            return String.format("type: %d, label: %s, isPrimary: %s, pobox: %s, "
                    + "extendedAddress: %s, street: %s, localty: %s, region: %s, postalCode %s, "
                    + "country: %s", mType, mLabel, mIsPrimary, mPobox, mExtendedAddress, mStreet,
                    mLocalty, mRegion, mPostalCode, mCountry);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.POSTAL_ADDRESS;
        }

        public String getPobox() {
            return mPobox;
        }

        public String getExtendedAddress() {
            return mExtendedAddress;
        }

        public String getStreet() {
            return mStreet;
        }

        public String getLocalty() {
            return mLocalty;
        }

        public String getRegion() {
            return mRegion;
        }

        public String getPostalCode() {
            return mPostalCode;
        }

        public String getCountry() {
            return mCountry;
        }

        public int getType() {
            return mType;
        }

        public String getLabel() {
            return mLabel;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class OrganizationData implements EntryElement {
        // non-final is Intentional: we may change the values since this info is separated into
        // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in different
        // timing.
        private String mOrganizationName;
        private String mDepartmentName;
        private String mTitle;
        private final String mPhoneticName; // We won't have this in "TITLE" property.
        private final int mType;
        private boolean mIsPrimary;

        public OrganizationData(final String organizationName, final String departmentName,
                final String titleName, final String phoneticName, int type,
                final boolean isPrimary) {
            mType = type;
            mOrganizationName = organizationName;
            mDepartmentName = departmentName;
            mTitle = titleName;
            mPhoneticName = phoneticName;
            mIsPrimary = isPrimary;
        }

        public String getFormattedString() {
            final StringBuilder builder = new StringBuilder();
            if (!TextUtils.isEmpty(mOrganizationName)) {
                builder.append(mOrganizationName);
            }

            if (!TextUtils.isEmpty(mDepartmentName)) {
                if (builder.length() > 0) {
                    builder.append(", ");
                }
                builder.append(mDepartmentName);
            }

            if (!TextUtils.isEmpty(mTitle)) {
                if (builder.length() > 0) {
                    builder.append(", ");
                }
                builder.append(mTitle);
            }

            return builder.toString();
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Organization.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
            builder.withValue(Organization.TYPE, mType);
            if (mOrganizationName != null) {
                builder.withValue(Organization.COMPANY, mOrganizationName);
            }
            if (mDepartmentName != null) {
                builder.withValue(Organization.DEPARTMENT, mDepartmentName);
            }
            if (mTitle != null) {
                builder.withValue(Organization.TITLE, mTitle);
            }
            if (mPhoneticName != null) {
                builder.withValue(Organization.PHONETIC_NAME, mPhoneticName);
            }
            if (mIsPrimary) {
                builder.withValue(Organization.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mOrganizationName) && TextUtils.isEmpty(mDepartmentName)
                    && TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mPhoneticName);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof OrganizationData)) {
                return false;
            }
            OrganizationData organization = (OrganizationData) obj;
            return (mType == organization.mType
                    && TextUtils.equals(mOrganizationName, organization.mOrganizationName)
                    && TextUtils.equals(mDepartmentName, organization.mDepartmentName)
                    && TextUtils.equals(mTitle, organization.mTitle)
                    && (mIsPrimary == organization.mIsPrimary));
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + (mOrganizationName != null ? mOrganizationName.hashCode() : 0);
            hash = hash * 31 + (mDepartmentName != null ? mDepartmentName.hashCode() : 0);
            hash = hash * 31 + (mTitle != null ? mTitle.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            return hash;
        }

        @Override
        public String toString() {
            return String.format(
                    "type: %d, organization: %s, department: %s, title: %s, isPrimary: %s", mType,
                    mOrganizationName, mDepartmentName, mTitle, mIsPrimary);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.ORGANIZATION;
        }

        public String getOrganizationName() {
            return mOrganizationName;
        }

        public String getDepartmentName() {
            return mDepartmentName;
        }

        public String getTitle() {
            return mTitle;
        }

        public String getPhoneticName() {
            return mPhoneticName;
        }

        public int getType() {
            return mType;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class ImData implements EntryElement {
        private final String mAddress;
        private final int mProtocol;
        private final String mCustomProtocol;
        private final int mType;
        private final boolean mIsPrimary;

        public ImData(final int protocol, final String customProtocol, final String address,
                final int type, final boolean isPrimary) {
            mProtocol = protocol;
            mCustomProtocol = customProtocol;
            mType = type;
            mAddress = address;
            mIsPrimary = isPrimary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Im.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
            builder.withValue(Im.TYPE, mType);
            builder.withValue(Im.PROTOCOL, mProtocol);
            builder.withValue(Im.DATA, mAddress);
            if (mProtocol == Im.PROTOCOL_CUSTOM) {
                builder.withValue(Im.CUSTOM_PROTOCOL, mCustomProtocol);
            }
            if (mIsPrimary) {
                builder.withValue(Data.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mAddress);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof ImData)) {
                return false;
            }
            ImData imData = (ImData) obj;
            return (mType == imData.mType
                    && mProtocol == imData.mProtocol
                    && TextUtils.equals(mCustomProtocol, imData.mCustomProtocol)
                    && TextUtils.equals(mAddress, imData.mAddress)
                    && (mIsPrimary == imData.mIsPrimary));
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + mProtocol;
            hash = hash * 31 + (mCustomProtocol != null ? mCustomProtocol.hashCode() : 0);
            hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            return hash;
        }

        @Override
        public String toString() {
            return String.format(
                    "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", mType,
                    mProtocol, mCustomProtocol, mAddress, mIsPrimary);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.IM;
        }

        public String getAddress() {
            return mAddress;
        }

        /**
         * One of the value available for {@link Im#PROTOCOL}. e.g.
         * {@link Im#PROTOCOL_GOOGLE_TALK}
         */
        public int getProtocol() {
            return mProtocol;
        }

        public String getCustomProtocol() {
            return mCustomProtocol;
        }

        public int getType() {
            return mType;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class PhotoData implements EntryElement {
        // private static final String FORMAT_FLASH = "SWF";

        // used when type is not defined in ContactsContract.
        private final String mFormat;
        private final boolean mIsPrimary;

        private final byte[] mBytes;

        private Integer mHashCode = null;

        public PhotoData(String format, byte[] photoBytes, boolean isPrimary) {
            mFormat = format;
            mBytes = photoBytes;
            mIsPrimary = isPrimary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
            builder.withValue(Photo.PHOTO, mBytes);
            if (mIsPrimary) {
                builder.withValue(Photo.IS_PRIMARY, 1);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return mBytes == null || mBytes.length == 0;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof PhotoData)) {
                return false;
            }
            PhotoData photoData = (PhotoData) obj;
            return (TextUtils.equals(mFormat, photoData.mFormat)
                    && Arrays.equals(mBytes, photoData.mBytes)
                    && (mIsPrimary == photoData.mIsPrimary));
        }

        @Override
        public int hashCode() {
            if (mHashCode != null) {
                return mHashCode;
            }

            int hash = mFormat != null ? mFormat.hashCode() : 0;
            hash = hash * 31;
            if (mBytes != null) {
                for (byte b : mBytes) {
                    hash += b;
                }
            }

            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            mHashCode = hash;
            return hash;
        }

        @Override
        public String toString() {
            return String.format("format: %s: size: %d, isPrimary: %s", mFormat, mBytes.length,
                    mIsPrimary);
        }

        @Override
        public final EntryLabel getEntryLabel() {
            return EntryLabel.PHOTO;
        }

        public String getFormat() {
            return mFormat;
        }

        public byte[] getBytes() {
            return mBytes;
        }

        public boolean isPrimary() {
            return mIsPrimary;
        }
    }

    public static class NicknameData implements EntryElement {
        private final String mNickname;

        public NicknameData(String nickname) {
            mNickname = nickname;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
            builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
            builder.withValue(Nickname.NAME, mNickname);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mNickname);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof NicknameData)) {
                return false;
            }
            NicknameData nicknameData = (NicknameData) obj;
            return TextUtils.equals(mNickname, nicknameData.mNickname);
        }

        @Override
        public int hashCode() {
            return mNickname != null ? mNickname.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "nickname: " + mNickname;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.NICKNAME;
        }

        public String getNickname() {
            return mNickname;
        }
    }

    public static class NoteData implements EntryElement {
        public final String mNote;

        public NoteData(String note) {
            mNote = note;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
            builder.withValue(Note.NOTE, mNote);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mNote);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof NoteData)) {
                return false;
            }
            NoteData noteData = (NoteData) obj;
            return TextUtils.equals(mNote, noteData.mNote);
        }

        @Override
        public int hashCode() {
            return mNote != null ? mNote.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "note: " + mNote;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.NOTE;
        }

        public String getNote() {
            return mNote;
        }
    }

    public static class WebsiteData implements EntryElement {
        private final String mWebsite;

        public WebsiteData(String website) {
            mWebsite = website;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
            builder.withValue(Website.URL, mWebsite);
            // There's no information about the type of URL in vCard.
            // We use TYPE_HOMEPAGE for safety.
            builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mWebsite);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof WebsiteData)) {
                return false;
            }
            WebsiteData websiteData = (WebsiteData) obj;
            return TextUtils.equals(mWebsite, websiteData.mWebsite);
        }

        @Override
        public int hashCode() {
            return mWebsite != null ? mWebsite.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "website: " + mWebsite;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.WEBSITE;
        }

        public String getWebsite() {
            return mWebsite;
        }
    }

    public static class BirthdayData implements EntryElement {
        private final String mBirthday;

        public BirthdayData(String birthday) {
            mBirthday = birthday;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
            builder.withValue(Event.START_DATE, mBirthday);
            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mBirthday);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof BirthdayData)) {
                return false;
            }
            BirthdayData birthdayData = (BirthdayData) obj;
            return TextUtils.equals(mBirthday, birthdayData.mBirthday);
        }

        @Override
        public int hashCode() {
            return mBirthday != null ? mBirthday.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "birthday: " + mBirthday;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.BIRTHDAY;
        }

        public String getBirthday() {
            return mBirthday;
        }
    }

    public static class AnniversaryData implements EntryElement {
        private final String mAnniversary;

        public AnniversaryData(String anniversary) {
            mAnniversary = anniversary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
            builder.withValue(Event.START_DATE, mAnniversary);
            builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mAnniversary);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof AnniversaryData)) {
                return false;
            }
            AnniversaryData anniversaryData = (AnniversaryData) obj;
            return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary);
        }

        @Override
        public int hashCode() {
            return mAnniversary != null ? mAnniversary.hashCode() : 0;
        }

        @Override
        public String toString() {
            return "anniversary: " + mAnniversary;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.ANNIVERSARY;
        }

        public String getAnniversary() { return mAnniversary; }
    }

    public static class SipData implements EntryElement {
        /**
         * Note that schema part ("sip:") is automatically removed. e.g.
         * "sip:username:password@host:port" becomes
         * "username:password@host:port"
         */
        private final String mAddress;
        private final int mType;
        private final String mLabel;
        private final boolean mIsPrimary;

        public SipData(String rawSip, int type, String label, boolean isPrimary) {
            if (rawSip.startsWith("sip:")) {
                mAddress = rawSip.substring(4);
            } else {
                mAddress = rawSip;
            }
            mType = type;
            mLabel = label;
            mIsPrimary = isPrimary;
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
            builder.withValue(SipAddress.SIP_ADDRESS, mAddress);
            builder.withValue(SipAddress.TYPE, mType);
            if (mType == SipAddress.TYPE_CUSTOM) {
                builder.withValue(SipAddress.LABEL, mLabel);
            }
            if (mIsPrimary) {
                builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary);
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mAddress);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof SipData)) {
                return false;
            }
            SipData sipData = (SipData) obj;
            return (mType == sipData.mType
                    && TextUtils.equals(mLabel, sipData.mLabel)
                    && TextUtils.equals(mAddress, sipData.mAddress)
                    && (mIsPrimary == sipData.mIsPrimary));
        }

        @Override
        public int hashCode() {
            int hash = mType;
            hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
            hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
            return hash;
        }

        @Override
        public String toString() {
            return "sip: " + mAddress;
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.SIP;
        }

        /**
         * @return Address part of the sip data. The schema ("sip:") isn't contained here.
         */
        public String getAddress() { return mAddress; }
        public int getType() { return mType; }
        public String getLabel() { return mLabel; }
    }

    /**
     * Some Contacts data in Android cannot be converted to vCard
     * representation. VCardEntry preserves those data using this class.
     */
    public static class AndroidCustomData implements EntryElement {
        private final String mMimeType;

        private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN

        public AndroidCustomData(String mimeType, List<String> dataList) {
            mMimeType = mimeType;
            mDataList = dataList;
        }

        public static AndroidCustomData constructAndroidCustomData(List<String> list) {
            String mimeType;
            List<String> dataList;

            if (list == null) {
                mimeType = null;
                dataList = null;
            } else if (list.size() < 2) {
                mimeType = list.get(0);
                dataList = null;
            } else {
                final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size()
                        : VCardConstants.MAX_DATA_COLUMN + 1;
                mimeType = list.get(0);
                dataList = list.subList(1, max);
            }

            return new AndroidCustomData(mimeType, dataList);
        }

        @Override
        public void constructInsertOperation(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            final ContentProviderOperation.Builder builder = ContentProviderOperation
                    .newInsert(Data.CONTENT_URI);
            builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex);
            builder.withValue(Data.MIMETYPE, mMimeType);
            for (int i = 0; i < mDataList.size(); i++) {
                String value = mDataList.get(i);
                if (!TextUtils.isEmpty(value)) {
                    // 1-origin
                    builder.withValue("data" + (i + 1), value);
                }
            }
            operationList.add(builder.build());
        }

        @Override
        public boolean isEmpty() {
            return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof AndroidCustomData)) {
                return false;
            }
            AndroidCustomData data = (AndroidCustomData) obj;
            if (!TextUtils.equals(mMimeType, data.mMimeType)) {
                return false;
            }
            if (mDataList == null) {
                return data.mDataList == null;
            } else {
                final int size = mDataList.size();
                if (size != data.mDataList.size()) {
                    return false;
                }
                for (int i = 0; i < size; i++) {
                    if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) {
                        return false;
                    }
                }
                return true;
            }
        }

        @Override
        public int hashCode() {
            int hash = mMimeType != null ? mMimeType.hashCode() : 0;
            if (mDataList != null) {
                for (String data : mDataList) {
                    hash = hash * 31 + (data != null ? data.hashCode() : 0);
                }
            }
            return hash;
        }

        @Override
        public String toString() {
            final StringBuilder builder = new StringBuilder();
            builder.append("android-custom: " + mMimeType + ", data: ");
            builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray()));
            return builder.toString();
        }

        @Override
        public EntryLabel getEntryLabel() {
            return EntryLabel.ANDROID_CUSTOM;
        }

        public String getMimeType() { return mMimeType; }
        public List<String> getDataList() { return mDataList; }
    }

    private final NameData mNameData = new NameData();
    private List<PhoneData> mPhoneList;
    private List<EmailData> mEmailList;
    private List<PostalData> mPostalList;
    private List<OrganizationData> mOrganizationList;
    private List<ImData> mImList;
    private List<PhotoData> mPhotoList;
    private List<WebsiteData> mWebsiteList;
    private List<SipData> mSipList;
    private List<NicknameData> mNicknameList;
    private List<NoteData> mNoteList;
    private List<AndroidCustomData> mAndroidCustomDataList;
    private BirthdayData mBirthday;
    private AnniversaryData mAnniversary;

    /**
     * Inner iterator interface.
     */
    public interface EntryElementIterator {
        public void onIterationStarted();

        public void onIterationEnded();

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

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

        /**
         * @return should be true when child wants to continue the operation.
         *         False otherwise.
         */
        public boolean onElement(EntryElement elem);
    }

    public final void iterateAllData(EntryElementIterator iterator) {
        iterator.onIterationStarted();
        iterator.onElementGroupStarted(mNameData.getEntryLabel());
        iterator.onElement(mNameData);
        iterator.onElementGroupEnded();

        iterateOneList(mPhoneList, iterator);
        iterateOneList(mEmailList, iterator);
        iterateOneList(mPostalList, iterator);
        iterateOneList(mOrganizationList, iterator);
        iterateOneList(mImList, iterator);
        iterateOneList(mPhotoList, iterator);
        iterateOneList(mWebsiteList, iterator);
        iterateOneList(mSipList, iterator);
        iterateOneList(mNicknameList, iterator);
        iterateOneList(mNoteList, iterator);
        iterateOneList(mAndroidCustomDataList, iterator);

        if (mBirthday != null) {
            iterator.onElementGroupStarted(mBirthday.getEntryLabel());
            iterator.onElement(mBirthday);
            iterator.onElementGroupEnded();
        }
        if (mAnniversary != null) {
            iterator.onElementGroupStarted(mAnniversary.getEntryLabel());
            iterator.onElement(mAnniversary);
            iterator.onElementGroupEnded();
        }
        iterator.onIterationEnded();
    }

    private void iterateOneList(List<? extends EntryElement> elemList,
            EntryElementIterator iterator) {
        if (elemList != null && elemList.size() > 0) {
            iterator.onElementGroupStarted(elemList.get(0).getEntryLabel());
            for (EntryElement elem : elemList) {
                iterator.onElement(elem);
            }
            iterator.onElementGroupEnded();
        }
    }

    private class IsIgnorableIterator implements EntryElementIterator {
        private boolean mEmpty = true;

        @Override
        public void onIterationStarted() {
        }

        @Override
        public void onIterationEnded() {
        }

        @Override
        public void onElementGroupStarted(EntryLabel label) {
        }

        @Override
        public void onElementGroupEnded() {
        }

        @Override
        public boolean onElement(EntryElement elem) {
            if (!elem.isEmpty()) {
                mEmpty = false;
                // exit now
                return false;
            } else {
                return true;
            }
        }

        public boolean getResult() {
            return mEmpty;
        }
    }

    private class ToStringIterator implements EntryElementIterator {
        private StringBuilder mBuilder;

        private boolean mFirstElement;

        @Override
        public void onIterationStarted() {
            mBuilder = new StringBuilder();
            mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n");
        }

        @Override
        public void onElementGroupStarted(EntryLabel label) {
            mBuilder.append(label.toString() + ": ");
            mFirstElement = true;
        }

        @Override
        public boolean onElement(EntryElement elem) {
            if (!mFirstElement) {
                mBuilder.append(", ");
                mFirstElement = false;
            }
            mBuilder.append("[").append(elem.toString()).append("]");
            return true;
        }

        @Override
        public void onElementGroupEnded() {
            mBuilder.append("\n");
        }

        @Override
        public void onIterationEnded() {
            mBuilder.append("]]\n");
        }

        @Override
        public String toString() {
            return mBuilder.toString();
        }
    }

    private class InsertOperationConstrutor implements EntryElementIterator {
        private final List<ContentProviderOperation> mOperationList;

        private final int mBackReferenceIndex;

        public InsertOperationConstrutor(List<ContentProviderOperation> operationList,
                int backReferenceIndex) {
            mOperationList = operationList;
            mBackReferenceIndex = backReferenceIndex;
        }

        @Override
        public void onIterationStarted() {
        }

        @Override
        public void onIterationEnded() {
        }

        @Override
        public void onElementGroupStarted(EntryLabel label) {
        }

        @Override
        public void onElementGroupEnded() {
        }

        @Override
        public boolean onElement(EntryElement elem) {
            if (!elem.isEmpty()) {
                elem.constructInsertOperation(mOperationList, mBackReferenceIndex);
            }
            return true;
        }
    }

    private final int mVCardType;
    private final Account mAccount;

    private List<VCardEntry> mChildren;

    @Override
    public String toString() {
        ToStringIterator iterator = new ToStringIterator();
        iterateAllData(iterator);
        return iterator.toString();
    }

    public VCardEntry() {
        this(VCardConfig.VCARD_TYPE_V21_GENERIC);
    }

    public VCardEntry(int vcardType) {
        this(vcardType, null);
    }

    public VCardEntry(int vcardType, Account account) {
        mVCardType = vcardType;
        mAccount = account;
    }

    private void addPhone(int type, String data, String label, boolean isPrimary) {
        if (mPhoneList == null) {
            mPhoneList = new ArrayList<PhoneData>();
        }
        final StringBuilder builder = new StringBuilder();
        final String trimmed = data.trim();
        final String formattedNumber;
        if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
            formattedNumber = trimmed;
        } else {
            // TODO: from the view of vCard spec these auto conversions should be removed.
            // Note that some other codes (like the phone number formatter) or modules expect this
            // auto conversion (bug 5178723), so just omitting this code won't be preferable enough
            // (bug 4177894)
            boolean hasPauseOrWait = false;
            final int length = trimmed.length();
            for (int i = 0; i < length; i++) {
                char ch = trimmed.charAt(i);
                // See RFC 3601 and docs for PhoneNumberUtils for more info.
                if (ch == 'p' || ch == 'P') {
                    builder.append(PhoneNumberUtils.PAUSE);
                    hasPauseOrWait = true;
                } else if (ch == 'w' || ch == 'W') {
                    builder.append(PhoneNumberUtils.WAIT);
                    hasPauseOrWait = true;
                } else if (('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, List, Map, boolean)} or
     * {@link #handleTitleValue(String)}.
     */
    private void addNewOrganization(final String organizationName, final String departmentName,
            final String titleName, final String phoneticName, int type, final boolean isPrimary) {
        if (mOrganizationList == null) {
            mOrganizationList = new ArrayList<OrganizationData>();
        }
        mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName,
                phoneticName, type, isPrimary));
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
                final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection,
                        phoneNumber);
                final int type;
                final String label;
                if (typeObject instanceof Integer) {
                    type = (Integer) typeObject;
                    label = null;
                } else {
                    type = Phone.TYPE_CUSTOM;
                    label = typeObject.toString();
                }

                final boolean isPrimary;
                if (typeCollection != null &&
                        typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
                    isPrimary = true;
                } else {
                    isPrimary = false;
                }

                addPhone(type, phoneNumber, label, isPrimary);
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
            // The phone number available via Skype.
            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            final int type = Phone.TYPE_OTHER;
            final boolean isPrimary;
            if (typeCollection != null
                    && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
                isPrimary = true;
            } else {
                isPrimary = false;
            }
            addPhone(type, propValue, null, isPrimary);
        } else if (sImMap.containsKey(propertyName)) {
            final int protocol = sImMap.get(propertyName);
            boolean isPrimary = false;
            int type = -1;
            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
            if (typeCollection != null) {
                for (String typeString : typeCollection) {
                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
                        isPrimary = true;
                    } else if (type < 0) {
                        if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
                            type = Im.TYPE_HOME;
                        } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
                            type = Im.TYPE_WORK;
                        }
                    }
                }
            }
            if (type < 0) {
                type = Im.TYPE_HOME;
            }
            addIm(protocol, null, propValue, type, isPrimary);
        } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) {
            addNote(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) {
            if (mWebsiteList == null) {
                mWebsiteList = new ArrayList<WebsiteData>(1);
            }
            mWebsiteList.add(new WebsiteData(propValue));
        } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) {
            mBirthday = new BirthdayData(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
            mAnniversary = new AnniversaryData(propValue);
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
            mNameData.mPhoneticGiven = propValue;
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
            mNameData.mPhoneticMiddle = propValue;
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
            mNameData.mPhoneticFamily = propValue;
        } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) {
            // See also RFC 4770 (for vCard 3.0)
            if (propValue.startsWith("sip:")) {
                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
                handleSipCase(propValue, typeCollection);
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) {
            if (!TextUtils.isEmpty(propValue)) {
                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
                handleSipCase(propValue, typeCollection);
            }
        } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
            final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue,
                    mVCardType);
            handleAndroidCustomProperty(customPropertyList);
        } else {
        }
        // Be careful when adding some logic here, as some blocks above may use "return".
    }

    /**
     * @param propValue may contain "sip:" at the beginning.
     * @param typeCollection
     */
    private void handleSipCase(String propValue, Collection<String> typeCollection) {
        if (TextUtils.isEmpty(propValue)) {
            return;
        }
        if (propValue.startsWith("sip:")) {
            propValue = propValue.substring(4);
            if (propValue.length() == 0) {
                return;
            }
        }

        int type = -1;
        String label = null;
        boolean isPrimary = false;
        if (typeCollection != null) {
            for (final String typeStringOrg : typeCollection) {
                final String typeStringUpperCase = typeStringOrg.toUpperCase();
                if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
                    isPrimary = true;
                } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
                    type = SipAddress.TYPE_HOME;
                } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
                    type = SipAddress.TYPE_WORK;
                } else if (type < 0) { // If no other type is specified before
                    if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
                        label = typeStringOrg.substring(2);
                    } else {
                        label = typeStringOrg;
                    }
                    type = SipAddress.TYPE_CUSTOM;
                }
            }
        }
        if (type < 0) {
            type = SipAddress.TYPE_OTHER;
        }
        addSip(propValue, type, label, isPrimary);
    }

    public void addChild(VCardEntry child) {
        if (mChildren == null) {
            mChildren = new ArrayList<VCardEntry>();
        }
        mChildren.add(child);
    }

    private void handleAndroidCustomProperty(final List<String> customPropertyList) {
        if (mAndroidCustomDataList == null) {
            mAndroidCustomDataList = new ArrayList<AndroidCustomData>();
        }
        mAndroidCustomDataList
                .add(AndroidCustomData.constructAndroidCustomData(customPropertyList));
    }

    /**
     * Construct the display name. The constructed data must not be null.
     */
    private String constructDisplayName() {
        String displayName = null;
        // FullName (created via "FN" or "NAME" field) is prefered.
        if (!TextUtils.isEmpty(mNameData.mFormatted)) {
            displayName = mNameData.mFormatted;
        } else if (!mNameData.emptyStructuredName()) {
            displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily,
                    mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix);
        } else if (!mNameData.emptyPhoneticStructuredName()) {
            displayName = VCardUtils.constructNameFromElements(mVCardType,
                    mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven);
        } else if (mEmailList != null && mEmailList.size() > 0) {
            displayName = mEmailList.get(0).mAddress;
        } else if (mPhoneList != null && mPhoneList.size() > 0) {
            displayName = mPhoneList.get(0).mNumber;
        } else if (mPostalList != null && mPostalList.size() > 0) {
            displayName = mPostalList.get(0).getFormattedAddress(mVCardType);
        } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
            displayName = mOrganizationList.get(0).getFormattedString();
        }
        if (displayName == null) {
            displayName = "";
        }
        return displayName;
    }

    /**
     * Consolidate several fielsds (like mName) using name candidates,
     */
    public void consolidateFields() {
        mNameData.displayName = constructDisplayName();
    }

    /**
     * @return true when this object has nothing meaningful for Android's
     *         Contacts, and thus is "ignorable" for Android's Contacts. This
     *         does not mean an original vCard is really empty. Even when the
     *         original vCard has some fields, this may ignore it if those
     *         fields cannot be transcoded into Android's Contacts
     *         representation.
     */
    public boolean isIgnorable() {
        IsIgnorableIterator iterator = new IsIgnorableIterator();
        iterateAllData(iterator);
        return iterator.getResult();
    }

    /**
     * Constructs the list of insert operation for this object. When the
     * operationList argument is null, this method creates a new ArrayList and
     * return it. The returned object is filled with new insert operations for
     * this object. When operationList argument is not null, this method appends
     * those new operations into the object instead of creating a new ArrayList.
     *
     * @param resolver {@link ContentResolver} object to be used in this method.
     * @param operationList object to be filled. You can use this argument to
     *            concatinate operation lists. If null, this method creates a
     *            new array object.
     * @return If operationList argument is null, new object with new insert
     *         operations. If it is not null, the operationList object with
     *         operations inserted by this method.
     */
    public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver,
            ArrayList<ContentProviderOperation> operationList) {
        if (operationList == null) {
            operationList = new ArrayList<ContentProviderOperation>();
        }

        if (isIgnorable()) {
            return operationList;
        }

        final int backReferenceIndex = operationList.size();

        // After applying the batch the first result's Uri is returned so it is important that
        // the RawContact is the first operation that gets inserted into the list.
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newInsert(RawContacts.CONTENT_URI);
        if (mAccount != null) {
            builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
            builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
        } else {
            builder.withValue(RawContacts.ACCOUNT_NAME, null);
            builder.withValue(RawContacts.ACCOUNT_TYPE, null);
        }
        operationList.add(builder.build());

        int start = operationList.size();
        iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex));
        int end = operationList.size();

        return operationList;
    }

    public static VCardEntry buildFromResolver(ContentResolver resolver) {
        return buildFromResolver(resolver, Contacts.CONTENT_URI);
    }

    public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
        return null;
    }

    private String listToString(List<String> list) {
        final int size = list.size();
        if (size > 1) {
            StringBuilder builder = new StringBuilder();
            int i = 0;
            for (String type : list) {
                builder.append(type);
                if (i < size - 1) {
                    builder.append(";");
                }
            }
            return builder.toString();
        } else if (size == 1) {
            return list.get(0);
        } else {
            return "";
        }
    }

    public final NameData getNameData() {
        return mNameData;
    }

    public final List<NicknameData> getNickNameList() {
        return mNicknameList;
    }

    public final String getBirthday() {
        return mBirthday != null ? mBirthday.mBirthday : null;
    }

    public final List<NoteData> getNotes() {
        return mNoteList;
    }

    public final List<PhoneData> getPhoneList() {
        return mPhoneList;
    }

    public final List<EmailData> getEmailList() {
        return mEmailList;
    }

    public final List<PostalData> getPostalList() {
        return mPostalList;
    }

    public final List<OrganizationData> getOrganizationList() {
        return mOrganizationList;
    }

    public final List<ImData> getImList() {
        return mImList;
    }

    public final List<PhotoData> getPhotoList() {
        return mPhotoList;
    }

    public final List<WebsiteData> getWebsiteList() {
        return mWebsiteList;
    }

    /**
     * @hide this interface may be changed for better support of vCard 4.0 (UID)
     */
    public final List<VCardEntry> getChildlen() {
        return mChildren;
    }

    public String getDisplayName() {
        if (mNameData.displayName == null) {
            mNameData.displayName = constructDisplayName();
        }
        return mNameData.displayName;
    }
}
