FileDocCategorySizeDatePackage
VCardBuilder.javaAPI DocAndroid 5.1 API99644Thu Mar 12 22:22:54 GMT 2015com.android.vcard

VCardBuilder.java

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