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 android.content.ContentValues;
  import android.provider.ContactsContract.CommonDataKinds.Email;
  import android.provider.ContactsContract.CommonDataKinds.Event;
  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.Relation;
  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.telephony.PhoneNumberUtils;
  import android.text.TextUtils;
  import android.util.Base64;
  import android.util.Log;
  
  import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
  
  import java.io.UnsupportedEncodingException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * <p>
   * The class which lets users create their own vCard String. Typical usage is as follows:
   * </p>
   * <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType);
   * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
   *     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
   *     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
   *     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
   *     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
   *     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
   *     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
   *     .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
   *     .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
   *     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
   *     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
   *     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
   * return builder.toString();</pre>
   */
  public class VCardBuilder {
      private static final String LOG_TAG = VCardConstants.LOG_TAG;
  
      // If you add the other element, please check all the columns are able to be
      // converted to String.
      //
      // e.g. BLOB is not what we can handle here now.
      private static final Set<String> sAllowedAndroidPropertySet =
              Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
                      Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
                      Relation.CONTENT_ITEM_TYPE)));
  
      public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
      public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
      public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
  
      private static final String VCARD_DATA_VCARD = "VCARD";
      private static final String VCARD_DATA_PUBLIC = "PUBLIC";
  
      private static final String VCARD_PARAM_SEPARATOR = ";";
      public static final String VCARD_END_OF_LINE = "\r\n";
      private static final String VCARD_DATA_SEPARATOR = ":";
      private static final String VCARD_ITEM_SEPARATOR = ";";
      private static final String VCARD_WS = " ";
      private static final String VCARD_PARAM_EQUAL = "=";
  
      private static final String VCARD_PARAM_ENCODING_QP =
              "ENCODING=" + VCardConstants.PARAM_ENCODING_QP;
      private static final String VCARD_PARAM_ENCODING_BASE64_V21 =
              "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64;
      private static final String VCARD_PARAM_ENCODING_BASE64_AS_B =
              "ENCODING=" + VCardConstants.PARAM_ENCODING_B;
  
      private static final String SHIFT_JIS = "SHIFT_JIS";
  
      private final int mVCardType;
  
      private final boolean mIsV30OrV40;
      private final boolean mIsJapaneseMobilePhone;
      private final boolean mOnlyOneNoteFieldIsAvailable;
      private final boolean mIsDoCoMo;
      private final boolean mShouldUseQuotedPrintable;
      private final boolean mUsesAndroidProperty;
      private final boolean mUsesDefactProperty;
      private final boolean mAppendTypeParamName;
      private final boolean mRefrainsQPToNameProperties;
      private final boolean mNeedsToConvertPhoneticString;
  
      private final boolean mShouldAppendCharsetParam;
  
      private final String mCharset;
      private final String mVCardCharsetParameter;
  
      private StringBuilder mBuilder;
      private boolean mEndAppended;
  
      public VCardBuilder(final int vcardType) {
          // Default charset should be used
          this(vcardType, null);
      }
  
      /**
       * @param vcardType
       * @param charset If null, we use default charset for export.
       * @hide
       */
      public VCardBuilder(final int vcardType, String charset) {
          mVCardType = vcardType;
  
          if (VCardConfig.isVersion40(vcardType)) {
              Log.w(LOG_TAG, "Should not use vCard 4.0 when building vCard. " +
                      "It is not officially published yet.");
          }
  
          mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType);
          mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
          mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
          mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
          mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
          mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
          mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
          mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
          mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
          mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
  
          // vCard 2.1 requires charset.
          // vCard 3.0 does not allow it but we found some devices use it to determine
          // the exact charset.
          // We currently append it only when charset other than UTF_8 is used.
          mShouldAppendCharsetParam =
                  !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset));
  
          if (VCardConfig.isDoCoMo(vcardType)) {
              if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
                  /* Log.w(LOG_TAG,
                          "The charset \"" + charset + "\" is used while "
                          + SHIFT_JIS + " is needed to be used."); */
                  if (TextUtils.isEmpty(charset)) {
                      mCharset = SHIFT_JIS;
                  } else {
                      mCharset = charset;
                  }
              } else {
                  mCharset = charset;
              }
              mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
          } else {
              if (TextUtils.isEmpty(charset)) {
                  Log.i(LOG_TAG,
                          "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET
                          + "\" for export.");
                  mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET;
                  mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET;
              } else {
                  mCharset = charset;
                  mVCardCharsetParameter = "CHARSET=" + charset;
              }
          }
          clear();
      }
  
      public void clear() {
          mBuilder = new StringBuilder();
          mEndAppended = false;
          appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
          if (VCardConfig.isVersion40(mVCardType)) {
              appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40);
          } else if (VCardConfig.isVersion30(mVCardType)) {
              appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
          } else {
              if (!VCardConfig.isVersion21(mVCardType)) {
                  Log.w(LOG_TAG, "Unknown vCard version detected.");
              }
              appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21);
          }
      }
  
      private boolean containsNonEmptyName(final ContentValues contentValues) {
          final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
          final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
          final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
          final String prefix = contentValues.getAsString(StructuredName.PREFIX);
          final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
          final String phoneticFamilyName =
                  contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
          final String phoneticMiddleName =
                  contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
          final String phoneticGivenName =
                  contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
          final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
          return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
                  TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
                  TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
                  TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
                  TextUtils.isEmpty(displayName));
      }
  
      private ContentValues getPrimaryContentValueWithStructuredName(
              final List<ContentValues> contentValuesList) {
          ContentValues primaryContentValues = null;
          ContentValues subprimaryContentValues = null;
          for (ContentValues contentValues : contentValuesList) {
              if (contentValues == null){
                  continue;
              }
              Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
              if (isSuperPrimary != null && isSuperPrimary > 0) {
                  // We choose "super primary" ContentValues.
                  primaryContentValues = contentValues;
                  break;
              } else if (primaryContentValues == null) {
                  // We choose the first "primary" ContentValues
                  // if "super primary" ContentValues does not exist.
                  final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
                  if (isPrimary != null && isPrimary > 0 &&
                          containsNonEmptyName(contentValues)) {
                      primaryContentValues = contentValues;
                      // Do not break, since there may be ContentValues with "super primary"
                      // afterword.
                  } else if (subprimaryContentValues == null &&
                          containsNonEmptyName(contentValues)) {
                      subprimaryContentValues = contentValues;
                  }
              }
          }
  
          if (primaryContentValues == null) {
              if (subprimaryContentValues != null) {
                  // We choose the first ContentValues if any "primary" ContentValues does not exist.
                  primaryContentValues = subprimaryContentValues;
              } else {
                  // There's no appropriate ContentValue with StructuredName.
                  primaryContentValues = new ContentValues();
              }
          }
  
          return primaryContentValues;
      }
  
      /**
       * To avoid unnecessary complication in logic, we use this method to construct N, FN
       * properties for vCard 4.0.
       */
      private VCardBuilder appendNamePropertiesV40(final List<ContentValues> contentValuesList) {
          if (mIsDoCoMo || mNeedsToConvertPhoneticString) {
              // Ignore all flags that look stale from the view of vCard 4.0 to
              // simplify construction algorithm. Actually we don't have any vCard file
              // available from real world yet, so we may need to re-enable some of these
              // in the future.
              Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored.");
          }
  
          if (contentValuesList == null || contentValuesList.isEmpty()) {
              appendLine(VCardConstants.PROPERTY_FN, "");
              return this;
          }
  
          // We have difficulty here. How can we appropriately handle StructuredName with
          // missing parts necessary for displaying while it has suppremental information.
          //
          // e.g. How to handle non-empty phonetic names with empty structured names?
  
          final ContentValues contentValues =
                  getPrimaryContentValueWithStructuredName(contentValuesList);
          String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
          final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
          final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
          final String prefix = contentValues.getAsString(StructuredName.PREFIX);
          final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
          final String formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
          if (TextUtils.isEmpty(familyName)
                  && TextUtils.isEmpty(givenName)
                  && TextUtils.isEmpty(middleName)
                  && TextUtils.isEmpty(prefix)
                  && TextUtils.isEmpty(suffix)) {
              if (TextUtils.isEmpty(formattedName)) {
                  appendLine(VCardConstants.PROPERTY_FN, "");
                  return this;
              }
              familyName = formattedName;
          }
  
          final String phoneticFamilyName =
                  contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
          final String phoneticMiddleName =
                  contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
          final String phoneticGivenName =
                  contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
          final String escapedFamily = escapeCharacters(familyName);
          final String escapedGiven = escapeCharacters(givenName);
          final String escapedMiddle = escapeCharacters(middleName);
          final String escapedPrefix = escapeCharacters(prefix);
          final String escapedSuffix = escapeCharacters(suffix);
  
          mBuilder.append(VCardConstants.PROPERTY_N);
  
          if (!(TextUtils.isEmpty(phoneticFamilyName) &&
                          TextUtils.isEmpty(phoneticMiddleName) &&
                          TextUtils.isEmpty(phoneticGivenName))) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              final String sortAs = escapeCharacters(phoneticFamilyName)
                      + ';' + escapeCharacters(phoneticGivenName)
                      + ';' + escapeCharacters(phoneticMiddleName);
              mBuilder.append("SORT-AS=").append(
                      VCardUtils.toStringAsV40ParamValue(sortAs));
          }
  
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(escapedFamily);
          mBuilder.append(VCARD_ITEM_SEPARATOR);
          mBuilder.append(escapedGiven);
          mBuilder.append(VCARD_ITEM_SEPARATOR);
          mBuilder.append(escapedMiddle);
          mBuilder.append(VCARD_ITEM_SEPARATOR);
          mBuilder.append(escapedPrefix);
          mBuilder.append(VCARD_ITEM_SEPARATOR);
          mBuilder.append(escapedSuffix);
          mBuilder.append(VCARD_END_OF_LINE);
  
          if (TextUtils.isEmpty(formattedName)) {
              // Note:
              // DISPLAY_NAME doesn't exist while some other elements do, which is usually
              // weird in Android, as DISPLAY_NAME should (usually) be constructed
              // from the others using locale information and its code points.
              Log.w(LOG_TAG, "DISPLAY_NAME is empty.");
  
              final String escaped = escapeCharacters(VCardUtils.constructNameFromElements(
                      VCardConfig.getNameOrderType(mVCardType),
                      familyName, middleName, givenName, prefix, suffix));
              appendLine(VCardConstants.PROPERTY_FN, escaped);
          } else {
              final String escapedFormatted = escapeCharacters(formattedName);
              mBuilder.append(VCardConstants.PROPERTY_FN);
              mBuilder.append(VCARD_DATA_SEPARATOR);
              mBuilder.append(escapedFormatted);
              mBuilder.append(VCARD_END_OF_LINE);
          }
  
          // We may need X- properties for phonetic names.
          appendPhoneticNameFields(contentValues);
          return this;
      }
  
      /**
       * For safety, we'll emit just one value around StructuredName, as external importers
       * may get confused with multiple "N", "FN", etc. properties, though it is valid in
       * vCard spec.
       */
      public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) {
          if (VCardConfig.isVersion40(mVCardType)) {
              return appendNamePropertiesV40(contentValuesList);
          }
  
          if (contentValuesList == null || contentValuesList.isEmpty()) {
              if (VCardConfig.isVersion30(mVCardType)) {
                  // vCard 3.0 requires "N" and "FN" properties.
                  // vCard 4.0 does NOT require N, but we take care of possible backward
                  // compatibility issues.
                  appendLine(VCardConstants.PROPERTY_N, "");
                  appendLine(VCardConstants.PROPERTY_FN, "");
              } else if (mIsDoCoMo) {
                  appendLine(VCardConstants.PROPERTY_N, "");
              }
              return this;
          }
  
          final ContentValues contentValues =
                  getPrimaryContentValueWithStructuredName(contentValuesList);
          final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
          final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
          final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
          final String prefix = contentValues.getAsString(StructuredName.PREFIX);
          final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
          final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
  
          if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
              final boolean reallyAppendCharsetParameterToName =
                      shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
              final boolean reallyUseQuotedPrintableToName =
                      (!mRefrainsQPToNameProperties &&
                              !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
                                      VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
                                      VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
                                      VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
                                      VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
  
              final String formattedName;
              if (!TextUtils.isEmpty(displayName)) {
                  formattedName = displayName;
              } else {
                  formattedName = VCardUtils.constructNameFromElements(
                          VCardConfig.getNameOrderType(mVCardType),
                          familyName, middleName, givenName, prefix, suffix);
              }
              final boolean reallyAppendCharsetParameterToFN =
                      shouldAppendCharsetParam(formattedName);
              final boolean reallyUseQuotedPrintableToFN =
                      !mRefrainsQPToNameProperties &&
                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
  
              final String encodedFamily;
              final String encodedGiven;
              final String encodedMiddle;
              final String encodedPrefix;
              final String encodedSuffix;
              if (reallyUseQuotedPrintableToName) {
                  encodedFamily = encodeQuotedPrintable(familyName);
                  encodedGiven = encodeQuotedPrintable(givenName);
                  encodedMiddle = encodeQuotedPrintable(middleName);
                  encodedPrefix = encodeQuotedPrintable(prefix);
                  encodedSuffix = encodeQuotedPrintable(suffix);
              } else {
                  encodedFamily = escapeCharacters(familyName);
                  encodedGiven = escapeCharacters(givenName);
                  encodedMiddle = escapeCharacters(middleName);
                  encodedPrefix = escapeCharacters(prefix);
                  encodedSuffix = escapeCharacters(suffix);
              }
  
              final String encodedFormattedname =
                      (reallyUseQuotedPrintableToFN ?
                              encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
  
              mBuilder.append(VCardConstants.PROPERTY_N);
              if (mIsDoCoMo) {
                  if (reallyAppendCharsetParameterToName) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(mVCardCharsetParameter);
                  }
                  if (reallyUseQuotedPrintableToName) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(VCARD_PARAM_ENCODING_QP);
                  }
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  // DoCoMo phones require that all the elements in the "family name" field.
                  mBuilder.append(formattedName);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
              } else {
                  if (reallyAppendCharsetParameterToName) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(mVCardCharsetParameter);
                  }
                  if (reallyUseQuotedPrintableToName) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(VCARD_PARAM_ENCODING_QP);
                  }
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(encodedFamily);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(encodedGiven);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(encodedMiddle);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(encodedPrefix);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(encodedSuffix);
              }
              mBuilder.append(VCARD_END_OF_LINE);
  
              // FN property
              mBuilder.append(VCardConstants.PROPERTY_FN);
              if (reallyAppendCharsetParameterToFN) {
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(mVCardCharsetParameter);
              }
              if (reallyUseQuotedPrintableToFN) {
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(VCARD_PARAM_ENCODING_QP);
              }
              mBuilder.append(VCARD_DATA_SEPARATOR);
              mBuilder.append(encodedFormattedname);
              mBuilder.append(VCARD_END_OF_LINE);
          } else if (!TextUtils.isEmpty(displayName)) {
  
              // N
              buildSinglePartNameField(VCardConstants.PROPERTY_N, displayName);
              mBuilder.append(VCARD_ITEM_SEPARATOR);
              mBuilder.append(VCARD_ITEM_SEPARATOR);
              mBuilder.append(VCARD_ITEM_SEPARATOR);
              mBuilder.append(VCARD_ITEM_SEPARATOR);
              mBuilder.append(VCARD_END_OF_LINE);
  
              // FN
              buildSinglePartNameField(VCardConstants.PROPERTY_FN, displayName);
              mBuilder.append(VCARD_END_OF_LINE);
  
          } else if (VCardConfig.isVersion30(mVCardType)) {
              appendLine(VCardConstants.PROPERTY_N, "");
              appendLine(VCardConstants.PROPERTY_FN, "");
          } else if (mIsDoCoMo) {
              appendLine(VCardConstants.PROPERTY_N, "");
          }
  
          appendPhoneticNameFields(contentValues);
          return this;
      }
  
      private void buildSinglePartNameField(String property, String part) {
          final boolean reallyUseQuotedPrintable =
                  (!mRefrainsQPToNameProperties &&
                          !VCardUtils.containsOnlyNonCrLfPrintableAscii(part));
          final String encodedPart = reallyUseQuotedPrintable ?
                  encodeQuotedPrintable(part) :
                  escapeCharacters(part);
  
          mBuilder.append(property);
  
          // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
          //       when it would be useful or necessary for external importers,
          //       assuming the external importer allows this vioration of the spec.
          if (shouldAppendCharsetParam(part)) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(mVCardCharsetParameter);
          }
          if (reallyUseQuotedPrintable) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCARD_PARAM_ENCODING_QP);
          }
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(encodedPart);
      }
  
      /**
       * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY.
       */
      private void appendPhoneticNameFields(final ContentValues contentValues) {
          final String phoneticFamilyName;
          final String phoneticMiddleName;
          final String phoneticGivenName;
          {
              final String tmpPhoneticFamilyName =
                  contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
              final String tmpPhoneticMiddleName =
                  contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
              final String tmpPhoneticGivenName =
                  contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
              if (mNeedsToConvertPhoneticString) {
                  phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
                  phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
                  phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
              } else {
                  phoneticFamilyName = tmpPhoneticFamilyName;
                  phoneticMiddleName = tmpPhoneticMiddleName;
                  phoneticGivenName = tmpPhoneticGivenName;
              }
          }
  
          if (TextUtils.isEmpty(phoneticFamilyName)
                  && TextUtils.isEmpty(phoneticMiddleName)
                  && TextUtils.isEmpty(phoneticGivenName)) {
              if (mIsDoCoMo) {
                  mBuilder.append(VCardConstants.PROPERTY_SOUND);
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
                  mBuilder.append(VCARD_END_OF_LINE);
              }
              return;
          }
  
          if (VCardConfig.isVersion40(mVCardType)) {
              // We don't want SORT-STRING anyway.
          } else if (VCardConfig.isVersion30(mVCardType)) {
              final String sortString =
                      VCardUtils.constructNameFromElements(mVCardType,
                              phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
              mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
              if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) {
                  // vCard 3.0 does not force us to use UTF-8 and actually we see some
                  // programs which emit this value. It is incorrect from the view of
                  // specification, but actually necessary for parsing vCard with non-UTF-8
                  // charsets, expecting other parsers not get confused with this value.
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(mVCardCharsetParameter);
              }
              mBuilder.append(VCARD_DATA_SEPARATOR);
              mBuilder.append(escapeCharacters(sortString));
              mBuilder.append(VCARD_END_OF_LINE);
          } else if (mIsJapaneseMobilePhone) {
              // Note: There is no appropriate property for expressing
              //       phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
              //       vCard 3.0 (SORT-STRING).
              //       We use DoCoMo's way when the device is Japanese one since it is already
              //       supported by a lot of Japanese mobile phones.
              //       This is "X-" property, so any parser hopefully would not get
              //       confused with this.
              //
              //       Also, DoCoMo's specification requires vCard composer to use just the first
              //       column.
              //       i.e.
              //       good:  SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
              //       bad :  SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
              mBuilder.append(VCardConstants.PROPERTY_SOUND);
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
  
              boolean reallyUseQuotedPrintable =
                  (!mRefrainsQPToNameProperties
                          && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
                                  phoneticFamilyName)
                                  && VCardUtils.containsOnlyNonCrLfPrintableAscii(
                                          phoneticMiddleName)
                                  && VCardUtils.containsOnlyNonCrLfPrintableAscii(
                                          phoneticGivenName)));
  
              final String encodedPhoneticFamilyName;
              final String encodedPhoneticMiddleName;
              final String encodedPhoneticGivenName;
              if (reallyUseQuotedPrintable) {
                  encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
                  encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
                  encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
              } else {
                  encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
                  encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
                  encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
              }
  
              if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
                      encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(mVCardCharsetParameter);
              }
              mBuilder.append(VCARD_DATA_SEPARATOR);
              {
                  boolean first = true;
                  if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
                      mBuilder.append(encodedPhoneticFamilyName);
                      first = false;
                  }
                  if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
                      if (first) {
                          first = false;
                      } else {
                          mBuilder.append(' ');
                      }
                      mBuilder.append(encodedPhoneticMiddleName);
                  }
                  if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
                      if (!first) {
                          mBuilder.append(' ');
                      }
                      mBuilder.append(encodedPhoneticGivenName);
                  }
              }
              mBuilder.append(VCARD_ITEM_SEPARATOR);  // family;given
              mBuilder.append(VCARD_ITEM_SEPARATOR);  // given;middle
              mBuilder.append(VCARD_ITEM_SEPARATOR);  // middle;prefix
              mBuilder.append(VCARD_ITEM_SEPARATOR);  // prefix;suffix
              mBuilder.append(VCARD_END_OF_LINE);
          }
  
          if (mUsesDefactProperty) {
              if (!TextUtils.isEmpty(phoneticGivenName)) {
                  final boolean reallyUseQuotedPrintable =
                      (mShouldUseQuotedPrintable &&
                              !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
                  final String encodedPhoneticGivenName;
                  if (reallyUseQuotedPrintable) {
                      encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
                  } else {
                      encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
                  }
                  mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
                  if (shouldAppendCharsetParam(phoneticGivenName)) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(mVCardCharsetParameter);
                  }
                  if (reallyUseQuotedPrintable) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(VCARD_PARAM_ENCODING_QP);
                  }
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(encodedPhoneticGivenName);
                  mBuilder.append(VCARD_END_OF_LINE);
              }  // if (!TextUtils.isEmpty(phoneticGivenName))
              if (!TextUtils.isEmpty(phoneticMiddleName)) {
                  final boolean reallyUseQuotedPrintable =
                      (mShouldUseQuotedPrintable &&
                              !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
                  final String encodedPhoneticMiddleName;
                  if (reallyUseQuotedPrintable) {
                      encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
                  } else {
                      encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
                  }
                  mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
                  if (shouldAppendCharsetParam(phoneticMiddleName)) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(mVCardCharsetParameter);
                  }
                  if (reallyUseQuotedPrintable) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(VCARD_PARAM_ENCODING_QP);
                  }
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(encodedPhoneticMiddleName);
                  mBuilder.append(VCARD_END_OF_LINE);
              }  // if (!TextUtils.isEmpty(phoneticGivenName))
              if (!TextUtils.isEmpty(phoneticFamilyName)) {
                  final boolean reallyUseQuotedPrintable =
                      (mShouldUseQuotedPrintable &&
                              !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
                  final String encodedPhoneticFamilyName;
                  if (reallyUseQuotedPrintable) {
                      encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
                  } else {
                      encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
                  }
                  mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
                  if (shouldAppendCharsetParam(phoneticFamilyName)) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(mVCardCharsetParameter);
                  }
                  if (reallyUseQuotedPrintable) {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                      mBuilder.append(VCARD_PARAM_ENCODING_QP);
                  }
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(encodedPhoneticFamilyName);
                  mBuilder.append(VCARD_END_OF_LINE);
              }  // if (!TextUtils.isEmpty(phoneticFamilyName))
          }
      }
  
      public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
          final boolean useAndroidProperty;
          if (mIsV30OrV40) {   // These specifications have NICKNAME property.
              useAndroidProperty = false;
          } else if (mUsesAndroidProperty) {
              useAndroidProperty = true;
          } else {
              // There's no way to add this field.
              return this;
          }
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  final String nickname = contentValues.getAsString(Nickname.NAME);
                  if (TextUtils.isEmpty(nickname)) {
                      continue;
                  }
                  if (useAndroidProperty) {
                      appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
                  } else {
                      appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
                  }
              }
          }
          return this;
      }
  
      public VCardBuilder appendPhones(final List<ContentValues> contentValuesList,
              VCardPhoneNumberTranslationCallback translationCallback) {
          boolean phoneLineExists = false;
          if (contentValuesList != null) {
              Set<String> phoneSet = new HashSet<String>();
              for (ContentValues contentValues : contentValuesList) {
                  final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
                  final String label = contentValues.getAsString(Phone.LABEL);
                  final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
                  final boolean isPrimary = (isPrimaryAsInteger != null ?
                          (isPrimaryAsInteger > 0) : false);
                  String phoneNumber = contentValues.getAsString(Phone.NUMBER);
                  if (phoneNumber != null) {
                      phoneNumber = phoneNumber.trim();
                  }
                  if (TextUtils.isEmpty(phoneNumber)) {
                      continue;
                  }
  
                  final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
                  // Note: We prioritize this callback over FLAG_REFRAIN_PHONE_NUMBER_FORMATTING
                  // intentionally. In the future the flag will be replaced by callback
                  // mechanism entirely.
                  if (translationCallback != null) {
                      phoneNumber = translationCallback.onValueReceived(
                              phoneNumber, type, label, isPrimary);
                      if (!phoneSet.contains(phoneNumber)) {
                          phoneSet.add(phoneNumber);
                          appendTelLine(type, label, phoneNumber, isPrimary);
                      }
                  } else if (type == Phone.TYPE_PAGER ||
                          VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
                      // Note: PAGER number needs unformatted "phone number".
                      phoneLineExists = true;
                      if (!phoneSet.contains(phoneNumber)) {
                          phoneSet.add(phoneNumber);
                          appendTelLine(type, label, phoneNumber, isPrimary);
                      }
                  } else {
                      final List<String> phoneNumberList = splitPhoneNumbers(phoneNumber);
                      if (phoneNumberList.isEmpty()) {
                          continue;
                      }
                      phoneLineExists = true;
                      for (String actualPhoneNumber : phoneNumberList) {
                          if (!phoneSet.contains(actualPhoneNumber)) {
                              // 'p' and 'w' are the standard characters for pause and wait
                              // (see RFC 3601)
                              // so use those when exporting phone numbers via vCard.
                              String numberWithControlSequence = actualPhoneNumber
                                      .replace(PhoneNumberUtils.PAUSE, 'p')
                                      .replace(PhoneNumberUtils.WAIT, 'w');
                              String formatted;
                              // TODO: remove this code and relevant test cases. vCard and any other
                              // codes using it shouldn't rely on the formatter here.
                              if (TextUtils.equals(numberWithControlSequence, actualPhoneNumber)) {
                                  StringBuilder digitsOnlyBuilder = new StringBuilder();
                                  final int length = actualPhoneNumber.length();
                                  for (int i = 0; i < length; i++) {
                                      final char ch = actualPhoneNumber.charAt(i);
                                      if (Character.isDigit(ch) || ch == '+') {
                                          digitsOnlyBuilder.append(ch);
                                      }
                                  }
                                  final int phoneFormat =
                                          VCardUtils.getPhoneNumberFormat(mVCardType);
                                  formatted = PhoneNumberUtilsPort.formatNumber(
                                          digitsOnlyBuilder.toString(), phoneFormat);
                              } else {
                                  // Be conservative.
                                  formatted = numberWithControlSequence;
                              }
  
                              // In vCard 4.0, value type must be "a single URI value",
                              // not just a phone number. (Based on vCard 4.0 rev.13)
                              if (VCardConfig.isVersion40(mVCardType)
                                      && !TextUtils.isEmpty(formatted)
                                      && !formatted.startsWith("tel:")) {
                                  formatted = "tel:" + formatted;
                              }
  
                              // Pre-formatted string should be stored.
                              phoneSet.add(actualPhoneNumber);
                              appendTelLine(type, label, formatted, isPrimary);
                          }
                      }  // for (String actualPhoneNumber : phoneNumberList) {
  
                      // TODO: TEL with SIP URI?
                  }
              }
          }
  
          if (!phoneLineExists && mIsDoCoMo) {
              appendTelLine(Phone.TYPE_HOME, "", "", false);
          }
  
          return this;
      }
  
      /**
       * <p>
       * Splits a given string expressing phone numbers into several strings, and remove
       * unnecessary characters inside them. The size of a returned list becomes 1 when
       * no split is needed.
       * </p>
       * <p>
       * The given number "may" have several phone numbers when the contact entry is corrupted
       * because of its original source.
       * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
       * </p>
       * <p>
       * This kind of "phone numbers" will not be created with Android vCard implementation,
       * but we may encounter them if the source of the input data has already corrupted
       * implementation.
       * </p>
       * <p>
       * To handle this case, this method first splits its input into multiple parts
       * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
       * removes unnecessary strings like "(Miami)".
       * </p>
       * <p>
       * Do not call this method when trimming is inappropriate for its receivers.
       * </p>
       */
      private List<String> splitPhoneNumbers(final String phoneNumber) {
          final List<String> phoneList = new ArrayList<String>();
  
          StringBuilder builder = new StringBuilder();
          final int length = phoneNumber.length();
          for (int i = 0; i < length; i++) {
              final char ch = phoneNumber.charAt(i);
              if (ch == '\n' && builder.length() > 0) {
                  phoneList.add(builder.toString());
                  builder = new StringBuilder();
              } else {
                  builder.append(ch);
              }
          }
          if (builder.length() > 0) {
              phoneList.add(builder.toString());
          }
          return phoneList;
      }
  
      public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
          boolean emailAddressExists = false;
          if (contentValuesList != null) {
              final Set<String> addressSet = new HashSet<String>();
              for (ContentValues contentValues : contentValuesList) {
                  String emailAddress = contentValues.getAsString(Email.DATA);
                  if (emailAddress != null) {
                      emailAddress = emailAddress.trim();
                  }
                  if (TextUtils.isEmpty(emailAddress)) {
                      continue;
                  }
                  Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
                  final int type = (typeAsObject != null ?
                          typeAsObject : DEFAULT_EMAIL_TYPE);
                  final String label = contentValues.getAsString(Email.LABEL);
                  Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
                  final boolean isPrimary = (isPrimaryAsInteger != null ?
                          (isPrimaryAsInteger > 0) : false);
                  emailAddressExists = true;
                  if (!addressSet.contains(emailAddress)) {
                      addressSet.add(emailAddress);
                      appendEmailLine(type, label, emailAddress, isPrimary);
                  }
              }
          }
  
          if (!emailAddressExists && mIsDoCoMo) {
              appendEmailLine(Email.TYPE_HOME, "", "", false);
          }
  
          return this;
      }
  
      public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
          if (contentValuesList == null || contentValuesList.isEmpty()) {
              if (mIsDoCoMo) {
                  mBuilder.append(VCardConstants.PROPERTY_ADR);
                  mBuilder.append(VCARD_PARAM_SEPARATOR);
                  mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
                  mBuilder.append(VCARD_DATA_SEPARATOR);
                  mBuilder.append(VCARD_END_OF_LINE);
              }
          } else {
              if (mIsDoCoMo) {
                  appendPostalsForDoCoMo(contentValuesList);
              } else {
                  appendPostalsForGeneric(contentValuesList);
              }
          }
  
          return this;
      }
  
      private static final Map<Integer, Integer> sPostalTypePriorityMap;
  
      static {
          sPostalTypePriorityMap = new HashMap<Integer, Integer>();
          sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
          sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
          sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
          sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
      }
  
      /**
       * Tries to append just one line. If there's no appropriate address
       * information, append an empty line.
       */
      private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
          int currentPriority = Integer.MAX_VALUE;
          int currentType = Integer.MAX_VALUE;
          ContentValues currentContentValues = null;
          for (final ContentValues contentValues : contentValuesList) {
              if (contentValues == null) {
                  continue;
              }
              final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
              final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
              final int priority =
                      (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
              if (priority < currentPriority) {
                  currentPriority = priority;
                  currentType = typeAsInteger;
                  currentContentValues = contentValues;
                  if (priority == 0) {
                      break;
                  }
              }
          }
  
          if (currentContentValues == null) {
              Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
              return;
          }
  
          final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
          appendPostalLine(currentType, label, currentContentValues, false, true);
      }
  
      private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
          for (final ContentValues contentValues : contentValuesList) {
              if (contentValues == null) {
                  continue;
              }
              final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
              final int type = (typeAsInteger != null ?
                      typeAsInteger : DEFAULT_POSTAL_TYPE);
              final String label = contentValues.getAsString(StructuredPostal.LABEL);
              final Integer isPrimaryAsInteger =
                  contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
              final boolean isPrimary = (isPrimaryAsInteger != null ?
                      (isPrimaryAsInteger > 0) : false);
              appendPostalLine(type, label, contentValues, isPrimary, false);
          }
      }
  
      private static class PostalStruct {
          final boolean reallyUseQuotedPrintable;
          final boolean appendCharset;
          final String addressData;
          public PostalStruct(final boolean reallyUseQuotedPrintable,
                  final boolean appendCharset, final String addressData) {
              this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
              this.appendCharset = appendCharset;
              this.addressData = addressData;
          }
      }
  
      /**
       * @return null when there's no information available to construct the data.
       */
      private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
          // adr-value    = 0*6(text-value ";") text-value
          //              ; PO Box, Extended Address, Street, Locality, Region, Postal
          //              ; Code, Country Name
          final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
          final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
          final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
          final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
          final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
          final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
          final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
          final String[] rawAddressArray = new String[]{
                  rawPoBox, rawNeighborhood, rawStreet, rawLocality,
                  rawRegion, rawPostalCode, rawCountry};
          if (!VCardUtils.areAllEmpty(rawAddressArray)) {
              final boolean reallyUseQuotedPrintable =
                  (mShouldUseQuotedPrintable &&
                          !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
              final boolean appendCharset =
                  !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
              final String encodedPoBox;
              final String encodedStreet;
              final String encodedLocality;
              final String encodedRegion;
              final String encodedPostalCode;
              final String encodedCountry;
              final String encodedNeighborhood;
  
              final String rawLocality2;
              // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
              // but this is intentional.
              //
              // QP encoding may add line feeds when needed and the result of
              // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
              // may be different from
              // - encodedLocality + " " + encodedNeighborhood.
              //
              // We use safer way.
              if (TextUtils.isEmpty(rawLocality)) {
                  if (TextUtils.isEmpty(rawNeighborhood)) {
                      rawLocality2 = "";
                  } else {
                      rawLocality2 = rawNeighborhood;
                  }
              } else {
                  if (TextUtils.isEmpty(rawNeighborhood)) {
                      rawLocality2 = rawLocality;
                  } else {
                      rawLocality2 = rawLocality + " " + rawNeighborhood;
                  }
              }
              if (reallyUseQuotedPrintable) {
                  encodedPoBox = encodeQuotedPrintable(rawPoBox);
                  encodedStreet = encodeQuotedPrintable(rawStreet);
                  encodedLocality = encodeQuotedPrintable(rawLocality2);
                  encodedRegion = encodeQuotedPrintable(rawRegion);
                  encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
                  encodedCountry = encodeQuotedPrintable(rawCountry);
              } else {
                  encodedPoBox = escapeCharacters(rawPoBox);
                  encodedStreet = escapeCharacters(rawStreet);
                  encodedLocality = escapeCharacters(rawLocality2);
                  encodedRegion = escapeCharacters(rawRegion);
                  encodedPostalCode = escapeCharacters(rawPostalCode);
                  encodedCountry = escapeCharacters(rawCountry);
                  encodedNeighborhood = escapeCharacters(rawNeighborhood);
              }
              final StringBuilder addressBuilder = new StringBuilder();
              addressBuilder.append(encodedPoBox);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
              addressBuilder.append(encodedStreet);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
              addressBuilder.append(encodedLocality);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
              addressBuilder.append(encodedRegion);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
              addressBuilder.append(encodedPostalCode);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
              addressBuilder.append(encodedCountry);
              return new PostalStruct(
                      reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
          } else {  // VCardUtils.areAllEmpty(rawAddressArray) == true
              // Try to use FORMATTED_ADDRESS instead.
              final String rawFormattedAddress =
                  contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
              if (TextUtils.isEmpty(rawFormattedAddress)) {
                  return null;
              }
              final boolean reallyUseQuotedPrintable =
                  (mShouldUseQuotedPrintable &&
                          !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
              final boolean appendCharset =
                  !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
              final String encodedFormattedAddress;
              if (reallyUseQuotedPrintable) {
                  encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
              } else {
                  encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
              }
  
              // We use the second value ("Extended Address") just because Japanese mobile phones
              // do so. If the other importer expects the value be in the other field, some flag may
              // be needed.
              final StringBuilder addressBuilder = new StringBuilder();
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
              addressBuilder.append(encodedFormattedAddress);
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
              addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
              return new PostalStruct(
                      reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
          }
      }
  
      public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
                  if (protocolAsObject == null) {
                      continue;
                  }
                  final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
                  if (propertyName == null) {
                      continue;
                  }
                  String data = contentValues.getAsString(Im.DATA);
                  if (data != null) {
                      data = data.trim();
                  }
                  if (TextUtils.isEmpty(data)) {
                      continue;
                  }
                  final String typeAsString;
                  {
                      final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
                      switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
                          case Im.TYPE_HOME: {
                              typeAsString = VCardConstants.PARAM_TYPE_HOME;
                              break;
                          }
                          case Im.TYPE_WORK: {
                              typeAsString = VCardConstants.PARAM_TYPE_WORK;
                              break;
                          }
                          case Im.TYPE_CUSTOM: {
                              final String label = contentValues.getAsString(Im.LABEL);
                              typeAsString = (label != null ? "X-" + label : null);
                              break;
                          }
                          case Im.TYPE_OTHER:  // Ignore
                          default: {
                              typeAsString = null;
                              break;
                          }
                      }
                  }
  
                  final List<String> parameterList = new ArrayList<String>();
                  if (!TextUtils.isEmpty(typeAsString)) {
                      parameterList.add(typeAsString);
                  }
                  final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
                  final boolean isPrimary = (isPrimaryAsInteger != null ?
                          (isPrimaryAsInteger > 0) : false);
                  if (isPrimary) {
                      parameterList.add(VCardConstants.PARAM_TYPE_PREF);
                  }
  
                  appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
              }
          }
          return this;
      }
  
      public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  String website = contentValues.getAsString(Website.URL);
                  if (website != null) {
                      website = website.trim();
                  }
  
                  // Note: vCard 3.0 does not allow any parameter addition toward "URL"
                  //       property, while there's no document in vCard 2.1.
                  if (!TextUtils.isEmpty(website)) {
                      appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
                  }
              }
          }
          return this;
      }
  
      public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  String company = contentValues.getAsString(Organization.COMPANY);
                  if (company != null) {
                      company = company.trim();
                  }
                  String department = contentValues.getAsString(Organization.DEPARTMENT);
                  if (department != null) {
                      department = department.trim();
                  }
                  String title = contentValues.getAsString(Organization.TITLE);
                  if (title != null) {
                      title = title.trim();
                  }
  
                  StringBuilder orgBuilder = new StringBuilder();
                  if (!TextUtils.isEmpty(company)) {
                      orgBuilder.append(company);
                  }
                  if (!TextUtils.isEmpty(department)) {
                      if (orgBuilder.length() > 0) {
                          orgBuilder.append(';');
                      }
                      orgBuilder.append(department);
                  }
                  final String orgline = orgBuilder.toString();
                  appendLine(VCardConstants.PROPERTY_ORG, orgline,
                          !VCardUtils.containsOnlyPrintableAscii(orgline),
                          (mShouldUseQuotedPrintable &&
                                  !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
  
                  if (!TextUtils.isEmpty(title)) {
                      appendLine(VCardConstants.PROPERTY_TITLE, title,
                              !VCardUtils.containsOnlyPrintableAscii(title),
                              (mShouldUseQuotedPrintable &&
                                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
                  }
              }
          }
          return this;
      }
  
      public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  if (contentValues == null) {
                      continue;
                  }
                  byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
                  if (data == null) {
                      continue;
                  }
                  final String photoType = VCardUtils.guessImageType(data);
                  if (photoType == null) {
                      Log.d(LOG_TAG, "Unknown photo type. Ignored.");
                      continue;
                  }
                  // TODO: check this works fine.
                  final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
                  if (!TextUtils.isEmpty(photoString)) {
                      appendPhotoLine(photoString, photoType);
                  }
              }
          }
          return this;
      }
  
      public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
          if (contentValuesList != null) {
              if (mOnlyOneNoteFieldIsAvailable) {
                  final StringBuilder noteBuilder = new StringBuilder();
                  boolean first = true;
                  for (final ContentValues contentValues : contentValuesList) {
                      String note = contentValues.getAsString(Note.NOTE);
                      if (note == null) {
                          note = "";
                      }
                      if (note.length() > 0) {
                          if (first) {
                              first = false;
                          } else {
                              noteBuilder.append('\n');
                          }
                          noteBuilder.append(note);
                      }
                  }
                  final String noteStr = noteBuilder.toString();
                  // This means we scan noteStr completely twice, which is redundant.
                  // But for now, we assume this is not so time-consuming..
                  final boolean shouldAppendCharsetInfo =
                      !VCardUtils.containsOnlyPrintableAscii(noteStr);
                  final boolean reallyUseQuotedPrintable =
                          (mShouldUseQuotedPrintable &&
                              !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
                  appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
                          shouldAppendCharsetInfo, reallyUseQuotedPrintable);
              } else {
                  for (ContentValues contentValues : contentValuesList) {
                      final String noteStr = contentValues.getAsString(Note.NOTE);
                      if (!TextUtils.isEmpty(noteStr)) {
                          final boolean shouldAppendCharsetInfo =
                                  !VCardUtils.containsOnlyPrintableAscii(noteStr);
                          final boolean reallyUseQuotedPrintable =
                                  (mShouldUseQuotedPrintable &&
                                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
                          appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
                                  shouldAppendCharsetInfo, reallyUseQuotedPrintable);
                      }
                  }
              }
          }
          return this;
      }
  
      public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
          // There's possibility where a given object may have more than one birthday, which
          // is inappropriate. We just build one birthday.
          if (contentValuesList != null) {
              String primaryBirthday = null;
              String secondaryBirthday = null;
              for (final ContentValues contentValues : contentValuesList) {
                  if (contentValues == null) {
                      continue;
                  }
                  final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
                  final int eventType;
                  if (eventTypeAsInteger != null) {
                      eventType = eventTypeAsInteger;
                  } else {
                      eventType = Event.TYPE_OTHER;
                  }
                  if (eventType == Event.TYPE_BIRTHDAY) {
                      final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
                      if (birthdayCandidate == null) {
                          continue;
                      }
                      final Integer isSuperPrimaryAsInteger =
                          contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
                      final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
                              (isSuperPrimaryAsInteger > 0) : false);
                      if (isSuperPrimary) {
                          // "super primary" birthday should the prefered one.
                          primaryBirthday = birthdayCandidate;
                          break;
                      }
                      final Integer isPrimaryAsInteger =
                          contentValues.getAsInteger(Event.IS_PRIMARY);
                      final boolean isPrimary = (isPrimaryAsInteger != null ?
                              (isPrimaryAsInteger > 0) : false);
                      if (isPrimary) {
                          // We don't break here since "super primary" birthday may exist later.
                          primaryBirthday = birthdayCandidate;
                      } else if (secondaryBirthday == null) {
                          // First entry is set to the "secondary" candidate.
                          secondaryBirthday = birthdayCandidate;
                      }
                  } else if (mUsesAndroidProperty) {
                      // Event types other than Birthday is not supported by vCard.
                      appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
                  }
              }
              if (primaryBirthday != null) {
                  appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
                          primaryBirthday.trim());
              } else if (secondaryBirthday != null){
                  appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
                          secondaryBirthday.trim());
              }
          }
          return this;
      }
  
      public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
          if (mUsesAndroidProperty && contentValuesList != null) {
              for (final ContentValues contentValues : contentValuesList) {
                  if (contentValues == null) {
                      continue;
                  }
                  appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
              }
          }
          return this;
      }
  
      /**
       * @param emitEveryTime If true, builder builds the line even when there's no entry.
       */
      public void appendPostalLine(final int type, final String label,
              final ContentValues contentValues,
              final boolean isPrimary, final boolean emitEveryTime) {
          final boolean reallyUseQuotedPrintable;
          final boolean appendCharset;
          final String addressValue;
          {
              PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
              if (postalStruct == null) {
                  if (emitEveryTime) {
                      reallyUseQuotedPrintable = false;
                      appendCharset = false;
                      addressValue = "";
                  } else {
                      return;
                  }
              } else {
                  reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
                  appendCharset = postalStruct.appendCharset;
                  addressValue = postalStruct.addressData;
              }
          }
  
          List<String> parameterList = new ArrayList<String>();
          if (isPrimary) {
              parameterList.add(VCardConstants.PARAM_TYPE_PREF);
          }
          switch (type) {
              case StructuredPostal.TYPE_HOME: {
                  parameterList.add(VCardConstants.PARAM_TYPE_HOME);
                  break;
              }
              case StructuredPostal.TYPE_WORK: {
                  parameterList.add(VCardConstants.PARAM_TYPE_WORK);
                  break;
              }
              case StructuredPostal.TYPE_CUSTOM: {
                  if (!TextUtils.isEmpty(label)
                          && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
                      // We're not sure whether the label is valid in the spec
                      // ("IANA-token" in the vCard 3.0 is unclear...)
                      // Just  for safety, we add "X-" at the beggining of each label.
                      // Also checks the label obeys with vCard 3.0 spec.
                      parameterList.add("X-" + label);
                  }
                  break;
              }
              case StructuredPostal.TYPE_OTHER: {
                  break;
              }
              default: {
                  Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
                  break;
              }
          }
  
          mBuilder.append(VCardConstants.PROPERTY_ADR);
          if (!parameterList.isEmpty()) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              appendTypeParameters(parameterList);
          }
          if (appendCharset) {
              // Strictly, vCard 3.0 does not allow exporters to emit charset information,
              // but we will add it since the information should be useful for importers,
              //
              // Assume no parser does not emit error with this parameter in vCard 3.0.
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(mVCardCharsetParameter);
          }
          if (reallyUseQuotedPrintable) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCARD_PARAM_ENCODING_QP);
          }
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(addressValue);
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      public void appendEmailLine(final int type, final String label,
              final String rawValue, final boolean isPrimary) {
          final String typeAsString;
          switch (type) {
              case Email.TYPE_CUSTOM: {
                  if (VCardUtils.isMobilePhoneLabel(label)) {
                      typeAsString = VCardConstants.PARAM_TYPE_CELL;
                  } else if (!TextUtils.isEmpty(label)
                          && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
                      typeAsString = "X-" + label;
                  } else {
                      typeAsString = null;
                  }
                  break;
              }
              case Email.TYPE_HOME: {
                  typeAsString = VCardConstants.PARAM_TYPE_HOME;
                  break;
              }
              case Email.TYPE_WORK: {
                  typeAsString = VCardConstants.PARAM_TYPE_WORK;
                  break;
              }
              case Email.TYPE_OTHER: {
                  typeAsString = null;
                  break;
              }
              case Email.TYPE_MOBILE: {
                  typeAsString = VCardConstants.PARAM_TYPE_CELL;
                  break;
              }
              default: {
                  Log.e(LOG_TAG, "Unknown Email type: " + type);
                  typeAsString = null;
                  break;
              }
          }
  
          final List<String> parameterList = new ArrayList<String>();
          if (isPrimary) {
              parameterList.add(VCardConstants.PARAM_TYPE_PREF);
          }
          if (!TextUtils.isEmpty(typeAsString)) {
              parameterList.add(typeAsString);
          }
  
          appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
                  rawValue);
      }
  
      public void appendTelLine(final Integer typeAsInteger, final String label,
              final String encodedValue, boolean isPrimary) {
          mBuilder.append(VCardConstants.PROPERTY_TEL);
          mBuilder.append(VCARD_PARAM_SEPARATOR);
  
          final int type;
          if (typeAsInteger == null) {
              type = Phone.TYPE_OTHER;
          } else {
              type = typeAsInteger;
          }
  
          ArrayList<String> parameterList = new ArrayList<String>();
          switch (type) {
              case Phone.TYPE_HOME: {
                  parameterList.addAll(
                          Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
                  break;
              }
              case Phone.TYPE_WORK: {
                  parameterList.addAll(
                          Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
                  break;
              }
              case Phone.TYPE_FAX_HOME: {
                  parameterList.addAll(
                          Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
                  break;
              }
              case Phone.TYPE_FAX_WORK: {
                  parameterList.addAll(
                          Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
                  break;
              }
              case Phone.TYPE_MOBILE: {
                  parameterList.add(VCardConstants.PARAM_TYPE_CELL);
                  break;
              }
              case Phone.TYPE_PAGER: {
                  if (mIsDoCoMo) {
                      // Not sure about the reason, but previous implementation had
                      // used "VOICE" instead of "PAGER"
                      parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
                  } else {
                      parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
                  }
                  break;
              }
              case Phone.TYPE_OTHER: {
                  parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
                  break;
              }
              case Phone.TYPE_CAR: {
                  parameterList.add(VCardConstants.PARAM_TYPE_CAR);
                  break;
              }
              case Phone.TYPE_COMPANY_MAIN: {
                  // There's no relevant field in vCard (at least 2.1).
                  parameterList.add(VCardConstants.PARAM_TYPE_WORK);
                  isPrimary = true;
                  break;
              }
              case Phone.TYPE_ISDN: {
                  parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
                  break;
              }
              case Phone.TYPE_MAIN: {
                  isPrimary = true;
                  break;
              }
              case Phone.TYPE_OTHER_FAX: {
                  parameterList.add(VCardConstants.PARAM_TYPE_FAX);
                  break;
              }
              case Phone.TYPE_TELEX: {
                  parameterList.add(VCardConstants.PARAM_TYPE_TLX);
                  break;
              }
              case Phone.TYPE_WORK_MOBILE: {
                  parameterList.addAll(
                          Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
                  break;
              }
              case Phone.TYPE_WORK_PAGER: {
                  parameterList.add(VCardConstants.PARAM_TYPE_WORK);
                  // See above.
                  if (mIsDoCoMo) {
                      parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
                  } else {
                      parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
                  }
                  break;
              }
              case Phone.TYPE_MMS: {
                  parameterList.add(VCardConstants.PARAM_TYPE_MSG);
                  break;
              }
              case Phone.TYPE_CUSTOM: {
                  if (TextUtils.isEmpty(label)) {
                      // Just ignore the custom type.
                      parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
                  } else if (VCardUtils.isMobilePhoneLabel(label)) {
                      parameterList.add(VCardConstants.PARAM_TYPE_CELL);
                  } else if (mIsV30OrV40) {
                      // This label is appropriately encoded in appendTypeParameters.
                      parameterList.add(label);
                  } else {
                      final String upperLabel = label.toUpperCase();
                      if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
                          parameterList.add(upperLabel);
                      } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
                          // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
                          //       "TYPE=" string.
                          parameterList.add("X-" + label);
                      }
                  }
                  break;
              }
              case Phone.TYPE_RADIO:
              case Phone.TYPE_TTY_TDD:
              default: {
                  break;
              }
          }
  
          if (isPrimary) {
              parameterList.add(VCardConstants.PARAM_TYPE_PREF);
          }
  
          if (parameterList.isEmpty()) {
              appendUncommonPhoneType(mBuilder, type);
          } else {
              appendTypeParameters(parameterList);
          }
  
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(encodedValue);
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      /**
       * Appends phone type string which may not be available in some devices.
       */
      private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
          if (mIsDoCoMo) {
              // The previous implementation for DoCoMo had been conservative
              // about miscellaneous types.
              builder.append(VCardConstants.PARAM_TYPE_VOICE);
          } else {
              String phoneType = VCardUtils.getPhoneTypeString(type);
              if (phoneType != null) {
                  appendTypeParameter(phoneType);
              } else {
                  Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
              }
          }
      }
  
      /**
       * @param encodedValue Must be encoded by BASE64
       * @param photoType
       */
      public void appendPhotoLine(final String encodedValue, final String photoType) {
          StringBuilder tmpBuilder = new StringBuilder();
          tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
          tmpBuilder.append(VCARD_PARAM_SEPARATOR);
          if (mIsV30OrV40) {
              tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
          } else {
              tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
          }
          tmpBuilder.append(VCARD_PARAM_SEPARATOR);
          appendTypeParameter(tmpBuilder, photoType);
          tmpBuilder.append(VCARD_DATA_SEPARATOR);
          tmpBuilder.append(encodedValue);
  
          final String tmpStr = tmpBuilder.toString();
          tmpBuilder = new StringBuilder();
          int lineCount = 0;
          final int length = tmpStr.length();
          final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
                  - VCARD_END_OF_LINE.length();
          final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
          int maxNum = maxNumForFirstLine;
          for (int i = 0; i < length; i++) {
              tmpBuilder.append(tmpStr.charAt(i));
              lineCount++;
              if (lineCount > maxNum) {
                  tmpBuilder.append(VCARD_END_OF_LINE);
                  tmpBuilder.append(VCARD_WS);
                  maxNum = maxNumInGeneral;
                  lineCount = 0;
              }
          }
          mBuilder.append(tmpBuilder.toString());
          mBuilder.append(VCARD_END_OF_LINE);
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      /**
       * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
       * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
       * instead of "IMPP;sip:...".
       *
       * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
       */
      public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
          final boolean useXProperty;
          if (mIsV30OrV40) {
              useXProperty = false;
          } else if (mUsesDefactProperty){
              useXProperty = true;
          } else {
              return this;
          }
  
          if (contentValuesList != null) {
              for (ContentValues contentValues : contentValuesList) {
                  String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
                  if (TextUtils.isEmpty(sipAddress)) {
                      continue;
                  }
                  if (useXProperty) {
                      // X-SIP does not contain "sip:" prefix.
                      if (sipAddress.startsWith("sip:")) {
                          if (sipAddress.length() == 4) {
                              continue;
                          }
                          sipAddress = sipAddress.substring(4);
                      }
                      // No type is available yet.
                      appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
                  } else {
                      if (!sipAddress.startsWith("sip:")) {
                          sipAddress = "sip:" + sipAddress;
                      }
                      final String propertyName;
                      if (VCardConfig.isVersion40(mVCardType)) {
                          // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
                          // TEL seems appropriate but may change in the future.
                          propertyName = VCardConstants.PROPERTY_TEL;
                      } else {
                          // RFC 4770 (for vCard 3.0)
                          propertyName = VCardConstants.PROPERTY_IMPP;
                      }
                      appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
                  }
              }
          }
          return this;
      }
  
      public void appendAndroidSpecificProperty(
              final String mimeType, ContentValues contentValues) {
          if (!sAllowedAndroidPropertySet.contains(mimeType)) {
              return;
          }
          final List<String> rawValueList = new ArrayList<String>();
          for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
              String value = contentValues.getAsString("data" + i);
              if (value == null) {
                  value = "";
              }
              rawValueList.add(value);
          }
  
          boolean needCharset =
              (mShouldAppendCharsetParam &&
                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
          boolean reallyUseQuotedPrintable =
              (mShouldUseQuotedPrintable &&
                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
          mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
          if (needCharset) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(mVCardCharsetParameter);
          }
          if (reallyUseQuotedPrintable) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCARD_PARAM_ENCODING_QP);
          }
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(mimeType);  // Should not be encoded.
          for (String rawValue : rawValueList) {
              final String encodedValue;
              if (reallyUseQuotedPrintable) {
                  encodedValue = encodeQuotedPrintable(rawValue);
              } else {
                  // TODO: one line may be too huge, which may be invalid in vCard 3.0
                  //        (which says "When generating a content line, lines longer than
                  //        75 characters SHOULD be folded"), though several
                  //        (even well-known) applications do not care this.
                  encodedValue = escapeCharacters(rawValue);
              }
              mBuilder.append(VCARD_ITEM_SEPARATOR);
              mBuilder.append(encodedValue);
          }
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      public void appendLineWithCharsetAndQPDetection(final String propertyName,
              final String rawValue) {
          appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
      }
  
      public void appendLineWithCharsetAndQPDetection(
              final String propertyName, final List<String> rawValueList) {
          appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
      }
  
      public void appendLineWithCharsetAndQPDetection(final String propertyName,
              final List<String> parameterList, final String rawValue) {
          final boolean needCharset =
                  !VCardUtils.containsOnlyPrintableAscii(rawValue);
          final boolean reallyUseQuotedPrintable =
                  (mShouldUseQuotedPrintable &&
                          !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
          appendLine(propertyName, parameterList,
                  rawValue, needCharset, reallyUseQuotedPrintable);
      }
  
      public void appendLineWithCharsetAndQPDetection(final String propertyName,
              final List<String> parameterList, final List<String> rawValueList) {
          boolean needCharset =
              (mShouldAppendCharsetParam &&
                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
          boolean reallyUseQuotedPrintable =
              (mShouldUseQuotedPrintable &&
                      !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
          appendLine(propertyName, parameterList, rawValueList,
                  needCharset, reallyUseQuotedPrintable);
      }
  
      /**
       * Appends one line with a given property name and value.
       */
      public void appendLine(final String propertyName, final String rawValue) {
          appendLine(propertyName, rawValue, false, false);
      }
  
      public void appendLine(final String propertyName, final List<String> rawValueList) {
          appendLine(propertyName, rawValueList, false, false);
      }
  
      public void appendLine(final String propertyName,
              final String rawValue, final boolean needCharset,
              boolean reallyUseQuotedPrintable) {
          appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
      }
  
      public void appendLine(final String propertyName, final List<String> parameterList,
              final String rawValue) {
          appendLine(propertyName, parameterList, rawValue, false, false);
      }
  
      public void appendLine(final String propertyName, final List<String> parameterList,
              final String rawValue, final boolean needCharset,
              boolean reallyUseQuotedPrintable) {
          mBuilder.append(propertyName);
          if (parameterList != null && parameterList.size() > 0) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              appendTypeParameters(parameterList);
          }
          if (needCharset) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(mVCardCharsetParameter);
          }
  
          final String encodedValue;
          if (reallyUseQuotedPrintable) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCARD_PARAM_ENCODING_QP);
              encodedValue = encodeQuotedPrintable(rawValue);
          } else {
              // TODO: one line may be too huge, which may be invalid in vCard spec, though
              //       several (even well-known) applications do not care that violation.
              encodedValue = escapeCharacters(rawValue);
          }
  
          mBuilder.append(VCARD_DATA_SEPARATOR);
          mBuilder.append(encodedValue);
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      public void appendLine(final String propertyName, final List<String> rawValueList,
              final boolean needCharset, boolean needQuotedPrintable) {
          appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
      }
  
      public void appendLine(final String propertyName, final List<String> parameterList,
              final List<String> rawValueList, final boolean needCharset,
              final boolean needQuotedPrintable) {
          mBuilder.append(propertyName);
          if (parameterList != null && parameterList.size() > 0) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              appendTypeParameters(parameterList);
          }
          if (needCharset) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(mVCardCharsetParameter);
          }
          if (needQuotedPrintable) {
              mBuilder.append(VCARD_PARAM_SEPARATOR);
              mBuilder.append(VCARD_PARAM_ENCODING_QP);
          }
  
          mBuilder.append(VCARD_DATA_SEPARATOR);
          boolean first = true;
          for (String rawValue : rawValueList) {
              final String encodedValue;
              if (needQuotedPrintable) {
                  encodedValue = encodeQuotedPrintable(rawValue);
              } else {
                  // TODO: one line may be too huge, which may be invalid in vCard 3.0
                  //        (which says "When generating a content line, lines longer than
                  //        75 characters SHOULD be folded"), though several
                  //        (even well-known) applications do not care this.
                  encodedValue = escapeCharacters(rawValue);
              }
  
              if (first) {
                  first = false;
              } else {
                  mBuilder.append(VCARD_ITEM_SEPARATOR);
              }
              mBuilder.append(encodedValue);
          }
          mBuilder.append(VCARD_END_OF_LINE);
      }
  
      /**
       * VCARD_PARAM_SEPARATOR must be appended before this method being called.
       */
      private void appendTypeParameters(final List<String> types) {
          // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
          // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
          boolean first = true;
          for (final String typeValue : types) {
              if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
                  final String encoded = (VCardConfig.isVersion40(mVCardType) ?
                          VCardUtils.toStringAsV40ParamValue(typeValue) :
                          VCardUtils.toStringAsV30ParamValue(typeValue));
                  if (TextUtils.isEmpty(encoded)) {
                      continue;
                  }
  
                  if (first) {
                      first = false;
                  } else {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                  }
                  appendTypeParameter(encoded);
              } else {  // vCard 2.1
                  if (!VCardUtils.isV21Word(typeValue)) {
                      continue;
                  }
                  if (first) {
                      first = false;
                  } else {
                      mBuilder.append(VCARD_PARAM_SEPARATOR);
                  }
                  appendTypeParameter(typeValue);
              }
          }
      }
  
      /**
       * VCARD_PARAM_SEPARATOR must be appended before this method being called.
       */
      private void appendTypeParameter(final String type) {
          appendTypeParameter(mBuilder, type);
      }
  
      private void appendTypeParameter(final StringBuilder builder, final String type) {
          // Refrain from using appendType() so that "TYPE=" is not be appended when the
          // device is DoCoMo's (just for safety).
          //
          // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
          if (VCardConfig.isVersion40(mVCardType) ||
                  ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
              builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
          }
          builder.append(type);
      }
  
      /**
       * Returns true when the property line should contain charset parameter
       * information. This method may return true even when vCard version is 3.0.
       *
       * Strictly, adding charset information is invalid in VCard 3.0.
       * However we'll add the info only when charset we use is not UTF-8
       * in vCard 3.0 format, since parser side may be able to use the charset
       * via this field, though we may encounter another problem by adding it.
       *
       * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
       * recommends UTF-8. By adding this field, parsers may be able
       * to know this text is NOT UTF-8 but Shift_Jis.
       */
      private boolean shouldAppendCharsetParam(String...propertyValueList) {
          if (!mShouldAppendCharsetParam) {
              return false;
          }
          for (String propertyValue : propertyValueList) {
              if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
                  return true;
              }
          }
          return false;
      }
  
      private String encodeQuotedPrintable(final String str) {
          if (TextUtils.isEmpty(str)) {
              return "";
          }
  
          final StringBuilder builder = new StringBuilder();
          int index = 0;
          int lineCount = 0;
          byte[] strArray = null;
  
          try {
              strArray = str.getBytes(mCharset);
          } catch (UnsupportedEncodingException e) {
              Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
                      + "Try default charset");
              strArray = str.getBytes();
          }
          while (index < strArray.length) {
              builder.append(String.format("=%02X", strArray[index]));
              index += 1;
              lineCount += 3;
  
              if (lineCount >= 67) {
                  // Specification requires CRLF must be inserted before the
                  // length of the line
                  // becomes more than 76.
                  // Assuming that the next character is a multi-byte character,
                  // it will become
                  // 6 bytes.
                  // 76 - 6 - 3 = 67
                  builder.append("=\r\n");
                  lineCount = 0;
              }
          }
  
          return builder.toString();
      }
  
      /**
       * Append '\' to the characters which should be escaped. The character set is different
       * not only between vCard 2.1 and vCard 3.0 but also among each device.
       *
       * Note that Quoted-Printable string must not be input here.
       */
      @SuppressWarnings("fallthrough")
      private String escapeCharacters(final String unescaped) {
          if (TextUtils.isEmpty(unescaped)) {
              return "";
          }
  
          final StringBuilder tmpBuilder = new StringBuilder();
          final int length = unescaped.length();
          for (int i = 0; i < length; i++) {
              final char ch = unescaped.charAt(i);
              switch (ch) {
                  case ';': {
                      tmpBuilder.append('\\');
                      tmpBuilder.append(';');
                      break;
                  }
                  case '\r': {
                      if (i + 1 < length) {
                          char nextChar = unescaped.charAt(i);
                          if (nextChar == '\n') {
                              break;
                          } else {
                              // fall through
                          }
                      } else {
                          // fall through
                      }
                  }
                  case '\n': {
                      // In vCard 2.1, there's no specification about this, while
                      // vCard 3.0 explicitly requires this should be encoded to "\n".
                      tmpBuilder.append("\\n");
                      break;
                  }
                  case '\\': {
                      if (mIsV30OrV40) {
                          tmpBuilder.append("\\\\");
                          break;
                      } else {
                          // fall through
                      }
                  }
                  case '<':
                  case '>': {
                      if (mIsDoCoMo) {
                          tmpBuilder.append('\\');
                          tmpBuilder.append(ch);
                      } else {
                          tmpBuilder.append(ch);
                      }
                      break;
                  }
                  case ',': {
                      if (mIsV30OrV40) {
                          tmpBuilder.append("\\,");
                      } else {
                          tmpBuilder.append(ch);
                      }
                      break;
                  }
                  default: {
                      tmpBuilder.append(ch);
                      break;
                  }
              }
          }
          return tmpBuilder.toString();
      }
  
      @Override
      public String toString() {
          if (!mEndAppended) {
              if (mIsDoCoMo) {
                  appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
                  appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
                  appendLine(VCardConstants.PROPERTY_X_NO, "");
                  appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
              }
              appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
              mEndAppended = true;
          }
          return mBuilder.toString();
      }
  }
  