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