alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Android example source code file (VCardBuilder.java)

This example Android source code file (VCardBuilder.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Android by Example" TM.

Java - Android tags/keywords

android, arraylist, charset, content, contentvalues, integer, list, postalstruct, shift_jis, string, stringbuffer, stringbuilder, telephony, unknown, util, utilities, utils, vcard_data_vcard, vcardbuilder, x

The VCardBuilder.java Android example source code

/*
 * 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 android.pim.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.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.CharsetUtils;
import android.util.Log;

import org.apache.commons.codec.binary.Base64;

import java.io.UnsupportedEncodingException;
import java.nio.charset.UnsupportedCharsetException;
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;

/**
 * The class which lets users create their own vCard String.
 */
public class VCardBuilder {
    private static final String LOG_TAG = "VCardBuilder";

    // 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 = ";";
    private 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=QUOTED-PRINTABLE";

    private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64";
    private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b";

    private static final String SHIFT_JIS = "SHIFT_JIS";
    private static final String UTF_8 = "UTF-8";

    private final int mVCardType;

    private final boolean mIsV30;
    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 mUsesUtf8;
    private final boolean mUsesShiftJis;
    private final boolean mAppendTypeParamName;
    private final boolean mRefrainsQPToNameProperties;
    private final boolean mNeedsToConvertPhoneticString;

    private final boolean mShouldAppendCharsetParam;

    private final String mCharsetString;
    private final String mVCardCharsetParameter;

    private StringBuilder mBuilder;
    private boolean mEndAppended;

    public VCardBuilder(final int vcardType) {
        mVCardType = vcardType;

        mIsV30 = VCardConfig.isV30(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);
        mUsesUtf8 = VCardConfig.usesUtf8(vcardType);
        mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
        mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
        mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
        mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);

        mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8);

        if (mIsDoCoMo) {
            String charset;
            try {
                charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
            } catch (UnsupportedCharsetException e) {
                Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
                charset = SHIFT_JIS;
            }
            mCharsetString = charset;
            // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but
            // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in
            // Android, not shown to the public).
            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
        } else if (mUsesShiftJis) {
            String charset;
            try {
                charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
            } catch (UnsupportedCharsetException e) {
                Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
                charset = SHIFT_JIS;
            }
            mCharsetString = charset;
            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
        } else {
            mCharsetString = UTF_8;
            mVCardCharsetParameter = "CHARSET=" + UTF_8;
        }
        clear();
    }

    public void clear() {
        mBuilder = new StringBuilder();
        mEndAppended = false;
        appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
        if (mIsV30) {
            appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
        } else {
            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 getPrimaryContentValue(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 {
                Log.e(LOG_TAG, "All ContentValues given from database is empty.");
                primaryContentValues = new ContentValues();
            }
        }

        return primaryContentValues;
    }

    /**
     * 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 (contentValuesList == null || contentValuesList.isEmpty()) {
            if (mIsDoCoMo) {
                appendLine(VCardConstants.PROPERTY_N, "");
            } else if (mIsV30) {
                // vCard 3.0 requires "N" and "FN" properties.
                appendLine(VCardConstants.PROPERTY_N, "");
                appendLine(VCardConstants.PROPERTY_FN, "");
            }
            return this;
        }

        final ContentValues contentValues = getPrimaryContentValue(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)) {
            final boolean reallyUseQuotedPrintableToDisplayName =
                (!mRefrainsQPToNameProperties &&
                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
            final String encodedDisplayName =
                    reallyUseQuotedPrintableToDisplayName ?
                            encodeQuotedPrintable(displayName) :
                                escapeCharacters(displayName);

            mBuilder.append(VCardConstants.PROPERTY_N);
            if (shouldAppendCharsetParam(displayName)) {
                mBuilder.append(VCARD_PARAM_SEPARATOR);
                mBuilder.append(mVCardCharsetParameter);
            }
            if (reallyUseQuotedPrintableToDisplayName) {
                mBuilder.append(VCARD_PARAM_SEPARATOR);
                mBuilder.append(VCARD_PARAM_ENCODING_QP);
            }
            mBuilder.append(VCARD_DATA_SEPARATOR);
            mBuilder.append(encodedDisplayName);
            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);
            mBuilder.append(VCardConstants.PROPERTY_FN);

            // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
            //       when it would be useful for external importers, assuming no external
            //       importer allows this vioration.
            if (shouldAppendCharsetParam(displayName)) {
                mBuilder.append(VCARD_PARAM_SEPARATOR);
                mBuilder.append(mVCardCharsetParameter);
            }
            mBuilder.append(VCARD_DATA_SEPARATOR);
            mBuilder.append(encodedDisplayName);
            mBuilder.append(VCARD_END_OF_LINE);
        } else if (mIsV30) {
            // vCard 3.0 specification requires these fields.
            appendLine(VCardConstants.PROPERTY_N, "");
            appendLine(VCardConstants.PROPERTY_FN, "");
        } else if (mIsDoCoMo) {
            appendLine(VCardConstants.PROPERTY_N, "");
        }

        appendPhoneticNameFields(contentValues);
        return this;
    }

    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;
        }

        // Try to emit the field(s) related to phonetic name.
        if (mIsV30) {
            final String sortString = VCardUtils
                    .constructNameFromElements(mVCardType,
                            phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
            mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
            if (shouldAppendCharsetParam(sortString)) {
                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 in vCard 2.1, while there is in
            //       vCard 3.0 (SORT-STRING).
            //       We chose to use DoCoMo's way when the device is Japanese one
            //       since it is 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.
            //       o  SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
            //       x  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);
            mBuilder.append(VCARD_ITEM_SEPARATOR);
            mBuilder.append(VCARD_ITEM_SEPARATOR);
            mBuilder.append(VCARD_ITEM_SEPARATOR);
            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(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(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);
            }
        }
    }

    public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
        final boolean useAndroidProperty;
        if (mIsV30) {
            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) {
        boolean phoneLineExists = false;
        if (contentValuesList != null) {
            Set<String> phoneSet = new HashSet();
            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;
                }
                int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
                if (type == Phone.TYPE_PAGER) {
                    phoneLineExists = true;
                    if (!phoneSet.contains(phoneNumber)) {
                        phoneSet.add(phoneNumber);
                        appendTelLine(type, label, phoneNumber, isPrimary);
                    }
                } else {
                    // The entry "may" have several phone numbers when the contact entry is
                    // corrupted because of its original source.
                    //
                    // e.g. I encountered the entry like the following.
                    // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..."
                    // This kind of entry is not able to be inserted via Android devices, but
                    // possible if the source of the data is already corrupted.
                    List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber);
                    if (phoneNumberList.isEmpty()) {
                        continue;
                    }
                    phoneLineExists = true;
                    for (String actualPhoneNumber : phoneNumberList) {
                        if (!phoneSet.contains(actualPhoneNumber)) {
                            final int format = VCardUtils.getPhoneNumberFormat(mVCardType);
                            final String formattedPhoneNumber =
                                    PhoneNumberUtils.formatNumber(actualPhoneNumber, format);
                            phoneSet.add(actualPhoneNumber);
                            appendTelLine(type, label, formattedPhoneNumber, isPrimary);
                        }
                    }
                }
            }
        }

        if (!phoneLineExists && mIsDoCoMo) {
            appendTelLine(Phone.TYPE_HOME, "", "", false);
        }

        return this;
    }

    private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) {
        List<String> phoneList = new ArrayList();

        StringBuilder builder = new StringBuilder();
        final int length = phoneNumber.length();
        for (int i = 0; i < length; i++) {
            final char ch = phoneNumber.charAt(i);
            // TODO: add a test case for string with '+', and care the other possible issues
            // which may happen by ignoring non-digits other than '+'.
            if (Character.isDigit(ch) || ch == '+') {
                builder.append(ch);
            } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
                phoneList.add(builder.toString());
                builder = new StringBuilder();
            }
        }
        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();
            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 StringBuffer addressBuffer = new StringBuffer();
            addressBuffer.append(encodedPoBox);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedStreet);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedLocality);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedRegion);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedPostalCode);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedCountry);
            return new PostalStruct(
                    reallyUseQuotedPrintable, appendCharset, addressBuffer.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 StringBuffer addressBuffer = new StringBuffer();
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(encodedFormattedAddress);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            addressBuffer.append(VCARD_ITEM_SEPARATOR);
            return new PostalStruct(
                    reallyUseQuotedPrintable, appendCharset, addressBuffer.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();
                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;
                }
                final String photoString = new String(Base64.encodeBase64(data));
                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) {
        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;
    }

    public void appendPostalLine(final int type, final String label,
            final ContentValues contentValues,
            final boolean isPrimary, final boolean emitLineEveryTime) {
        final boolean reallyUseQuotedPrintable;
        final boolean appendCharset;
        final String addressValue;
        {
            PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
            if (postalStruct == null) {
                if (emitLineEveryTime) {
                    reallyUseQuotedPrintable = false;
                    appendCharset = false;
                    addressValue = "";
                } else {
                    return;
                }
            } else {
                reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
                appendCharset = postalStruct.appendCharset;
                addressValue = postalStruct.addressData;
            }
        }

        List<String> parameterList = new ArrayList();
        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();
        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();
        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 {
                    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 (mIsV30) {
            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30);
        } 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);
    }

    public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) {
        if (!sAllowedAndroidPropertySet.contains(mimeType)) {
            return;
        }
        final List<String> rawValueList = new ArrayList();
        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 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 this.
            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) {
            // Note: vCard 3.0 specifies the different type of acceptable type Strings, but
            //       we don't emit that kind of vCard 3.0 specific type since there should be
            //       high probabilyty in which external importers cannot understand them.
            //
            // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they
            //      are quoted.)
            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 ((mIsV30 || 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(mCharsetString);
        } catch (UnsupportedEncodingException e) {
            Log.e(LOG_TAG, "Charset " + mCharsetString + " 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 (mIsV30) {
                        tmpBuilder.append("\\\\");
                        break;
                    } else {
                        // fall through
                    }
                }
                case '<':
                case '>': {
                    if (mIsDoCoMo) {
                        tmpBuilder.append('\\');
                        tmpBuilder.append(ch);
                    } else {
                        tmpBuilder.append(ch);
                    }
                    break;
                }
                case ',': {
                    if (mIsV30) {
                        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();
    }
}

Other Android examples (source code examples)

Here is a short list of links related to this Android VCardBuilder.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.