Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package com.android.vcard;
     17 
     18 import android.content.ContentValues;
     19 import android.provider.ContactsContract.CommonDataKinds.Email;
     20 import android.provider.ContactsContract.CommonDataKinds.Event;
     21 import android.provider.ContactsContract.CommonDataKinds.Im;
     22 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     23 import android.provider.ContactsContract.CommonDataKinds.Note;
     24 import android.provider.ContactsContract.CommonDataKinds.Organization;
     25 import android.provider.ContactsContract.CommonDataKinds.Phone;
     26 import android.provider.ContactsContract.CommonDataKinds.Photo;
     27 import android.provider.ContactsContract.CommonDataKinds.Relation;
     28 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     29 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     30 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     31 import android.provider.ContactsContract.CommonDataKinds.Website;
     32 import android.telephony.PhoneNumberUtils;
     33 import android.text.TextUtils;
     34 import android.util.Base64;
     35 import android.util.Log;
     36 
     37 import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
     38 
     39 import java.io.UnsupportedEncodingException;
     40 import java.util.ArrayList;
     41 import java.util.Arrays;
     42 import java.util.Collections;
     43 import java.util.HashMap;
     44 import java.util.HashSet;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.Set;
     48 
     49 /**
     50  * <p>
     51  * The class which lets users create their own vCard String. Typical usage is as follows:
     52  * </p>
     53  * <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType);
     54  * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
     55  *     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
     56  *     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
     57  *     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
     58  *     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
     59  *     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
     60  *     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
     61  *     .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
     62  *     .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
     63  *     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
     64  *     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
     65  *     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
     66  * return builder.toString();</pre>
     67  */
     68 public class VCardBuilder {
     69     private static final String LOG_TAG = VCardConstants.LOG_TAG;
     70 
     71     // If you add the other element, please check all the columns are able to be
     72     // converted to String.
     73     //
     74     // e.g. BLOB is not what we can handle here now.
     75     private static final Set<String> sAllowedAndroidPropertySet =
     76             Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
     77                     Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
     78                     Relation.CONTENT_ITEM_TYPE)));
     79 
     80     public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
     81     public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
     82     public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
     83 
     84     private static final String VCARD_DATA_VCARD = "VCARD";
     85     private static final String VCARD_DATA_PUBLIC = "PUBLIC";
     86 
     87     private static final String VCARD_PARAM_SEPARATOR = ";";
     88     public static final String VCARD_END_OF_LINE = "\r\n";
     89     private static final String VCARD_DATA_SEPARATOR = ":";
     90     private static final String VCARD_ITEM_SEPARATOR = ";";
     91     private static final String VCARD_WS = " ";
     92     private static final String VCARD_PARAM_EQUAL = "=";
     93 
     94     private static final String VCARD_PARAM_ENCODING_QP =
     95             "ENCODING=" + VCardConstants.PARAM_ENCODING_QP;
     96     private static final String VCARD_PARAM_ENCODING_BASE64_V21 =
     97             "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64;
     98     private static final String VCARD_PARAM_ENCODING_BASE64_AS_B =
     99             "ENCODING=" + VCardConstants.PARAM_ENCODING_B;
    100 
    101     private static final String SHIFT_JIS = "SHIFT_JIS";
    102 
    103     private final int mVCardType;
    104 
    105     private final boolean mIsV30OrV40;
    106     private final boolean mIsJapaneseMobilePhone;
    107     private final boolean mOnlyOneNoteFieldIsAvailable;
    108     private final boolean mIsDoCoMo;
    109     private final boolean mShouldUseQuotedPrintable;
    110     private final boolean mUsesAndroidProperty;
    111     private final boolean mUsesDefactProperty;
    112     private final boolean mAppendTypeParamName;
    113     private final boolean mRefrainsQPToNameProperties;
    114     private final boolean mNeedsToConvertPhoneticString;
    115 
    116     private final boolean mShouldAppendCharsetParam;
    117 
    118     private final String mCharset;
    119     private final String mVCardCharsetParameter;
    120 
    121     private StringBuilder mBuilder;
    122     private boolean mEndAppended;
    123 
    124     public VCardBuilder(final int vcardType) {
    125         // Default charset should be used
    126         this(vcardType, null);
    127     }
    128 
    129     /**
    130      * @param vcardType
    131      * @param charset If null, we use default charset for export.
    132      * @hide
    133      */
    134     public VCardBuilder(final int vcardType, String charset) {
    135         mVCardType = vcardType;
    136 
    137         if (VCardConfig.isVersion40(vcardType)) {
    138             Log.w(LOG_TAG, "Should not use vCard 4.0 when building vCard. " +
    139                     "It is not officially published yet.");
    140         }
    141 
    142         mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType);
    143         mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
    144         mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
    145         mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
    146         mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
    147         mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
    148         mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
    149         mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
    150         mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
    151         mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
    152 
    153         // vCard 2.1 requires charset.
    154         // vCard 3.0 does not allow it but we found some devices use it to determine
    155         // the exact charset.
    156         // We currently append it only when charset other than UTF_8 is used.
    157         mShouldAppendCharsetParam =
    158                 !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset));
    159 
    160         if (VCardConfig.isDoCoMo(vcardType)) {
    161             if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
    162                 /* Log.w(LOG_TAG,
    163                         "The charset \"" + charset + "\" is used while "
    164                         + SHIFT_JIS + " is needed to be used."); */
    165                 if (TextUtils.isEmpty(charset)) {
    166                     mCharset = SHIFT_JIS;
    167                 } else {
    168                     /*try {
    169                         charset = CharsetUtils.charsetForVendor(charset).name();
    170                     } catch (UnsupportedCharsetException e) {
    171                         Log.i(LOG_TAG,
    172                                 "Career-specific \"" + charset + "\" was not found (as usual). "
    173                                 + "Use it as is.");
    174                     }*/
    175                     mCharset = charset;
    176                 }
    177             } else {
    178                 /*if (mIsDoCoMo) {
    179                     try {
    180                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
    181                     } catch (UnsupportedCharsetException e) {
    182                         Log.e(LOG_TAG,
    183                                 "DoCoMo-specific SHIFT_JIS was not found. "
    184                                 + "Use SHIFT_JIS as is.");
    185                         charset = SHIFT_JIS;
    186                     }
    187                 } else {
    188                     try {
    189                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
    190                     } catch (UnsupportedCharsetException e) {
    191                         Log.e(LOG_TAG,
    192                                 "Career-specific SHIFT_JIS was not found. "
    193                                 + "Use SHIFT_JIS as is.");
    194                         charset = SHIFT_JIS;
    195                     }
    196                 }*/
    197                 mCharset = charset;
    198             }
    199             mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
    200         } else {
    201             if (TextUtils.isEmpty(charset)) {
    202                 Log.i(LOG_TAG,
    203                         "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET
    204                         + "\" for export.");
    205                 mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET;
    206                 mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET;
    207             } else {
    208                 /*
    209                 try {
    210                     charset = CharsetUtils.charsetForVendor(charset).name();
    211                 } catch (UnsupportedCharsetException e) {
    212                     Log.i(LOG_TAG,
    213                             "Career-specific \"" + charset + "\" was not found (as usual). "
    214                             + "Use it as is.");
    215                 }*/
    216                 mCharset = charset;
    217                 mVCardCharsetParameter = "CHARSET=" + charset;
    218             }
    219         }
    220         clear();
    221     }
    222 
    223     public void clear() {
    224         mBuilder = new StringBuilder();
    225         mEndAppended = false;
    226         appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
    227         if (VCardConfig.isVersion40(mVCardType)) {
    228             appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40);
    229         } else if (VCardConfig.isVersion30(mVCardType)) {
    230             appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
    231         } else {
    232             if (!VCardConfig.isVersion21(mVCardType)) {
    233                 Log.w(LOG_TAG, "Unknown vCard version detected.");
    234             }
    235             appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21);
    236         }
    237     }
    238 
    239     private boolean containsNonEmptyName(final ContentValues contentValues) {
    240         final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
    241         final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
    242         final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
    243         final String prefix = contentValues.getAsString(StructuredName.PREFIX);
    244         final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
    245         final String phoneticFamilyName =
    246                 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
    247         final String phoneticMiddleName =
    248                 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
    249         final String phoneticGivenName =
    250                 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
    251         final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
    252         return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
    253                 TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
    254                 TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
    255                 TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
    256                 TextUtils.isEmpty(displayName));
    257     }
    258 
    259     private ContentValues getPrimaryContentValueWithStructuredName(
    260             final List<ContentValues> contentValuesList) {
    261         ContentValues primaryContentValues = null;
    262         ContentValues subprimaryContentValues = null;
    263         for (ContentValues contentValues : contentValuesList) {
    264             if (contentValues == null){
    265                 continue;
    266             }
    267             Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
    268             if (isSuperPrimary != null && isSuperPrimary > 0) {
    269                 // We choose "super primary" ContentValues.
    270                 primaryContentValues = contentValues;
    271                 break;
    272             } else if (primaryContentValues == null) {
    273                 // We choose the first "primary" ContentValues
    274                 // if "super primary" ContentValues does not exist.
    275                 final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
    276                 if (isPrimary != null && isPrimary > 0 &&
    277                         containsNonEmptyName(contentValues)) {
    278                     primaryContentValues = contentValues;
    279                     // Do not break, since there may be ContentValues with "super primary"
    280                     // afterword.
    281                 } else if (subprimaryContentValues == null &&
    282                         containsNonEmptyName(contentValues)) {
    283                     subprimaryContentValues = contentValues;
    284                 }
    285             }
    286         }
    287 
    288         if (primaryContentValues == null) {
    289             if (subprimaryContentValues != null) {
    290                 // We choose the first ContentValues if any "primary" ContentValues does not exist.
    291                 primaryContentValues = subprimaryContentValues;
    292             } else {
    293                 // There's no appropriate ContentValue with StructuredName.
    294                 primaryContentValues = new ContentValues();
    295             }
    296         }
    297 
    298         return primaryContentValues;
    299     }
    300 
    301     /**
    302      * To avoid unnecessary complication in logic, we use this method to construct N, FN
    303      * properties for vCard 4.0.
    304      */
    305     private VCardBuilder appendNamePropertiesV40(final List<ContentValues> contentValuesList) {
    306         if (mIsDoCoMo || mNeedsToConvertPhoneticString) {
    307             // Ignore all flags that look stale from the view of vCard 4.0 to
    308             // simplify construction algorithm. Actually we don't have any vCard file
    309             // available from real world yet, so we may need to re-enable some of these
    310             // in the future.
    311             Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored.");
    312         }
    313 
    314         if (contentValuesList == null || contentValuesList.isEmpty()) {
    315             appendLine(VCardConstants.PROPERTY_FN, "");
    316             return this;
    317         }
    318 
    319         // We have difficulty here. How can we appropriately handle StructuredName with
    320         // missing parts necessary for displaying while it has suppremental information.
    321         //
    322         // e.g. How to handle non-empty phonetic names with empty structured names?
    323 
    324         final ContentValues contentValues =
    325                 getPrimaryContentValueWithStructuredName(contentValuesList);
    326         String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
    327         final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
    328         final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
    329         final String prefix = contentValues.getAsString(StructuredName.PREFIX);
    330         final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
    331         final String formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
    332         if (TextUtils.isEmpty(familyName)
    333                 && TextUtils.isEmpty(givenName)
    334                 && TextUtils.isEmpty(middleName)
    335                 && TextUtils.isEmpty(prefix)
    336                 && TextUtils.isEmpty(suffix)) {
    337             if (TextUtils.isEmpty(formattedName)) {
    338                 appendLine(VCardConstants.PROPERTY_FN, "");
    339                 return this;
    340             }
    341             familyName = formattedName;
    342         }
    343 
    344         final String phoneticFamilyName =
    345                 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
    346         final String phoneticMiddleName =
    347                 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
    348         final String phoneticGivenName =
    349                 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
    350         final String escapedFamily = escapeCharacters(familyName);
    351         final String escapedGiven = escapeCharacters(givenName);
    352         final String escapedMiddle = escapeCharacters(middleName);
    353         final String escapedPrefix = escapeCharacters(prefix);
    354         final String escapedSuffix = escapeCharacters(suffix);
    355 
    356         mBuilder.append(VCardConstants.PROPERTY_N);
    357 
    358         if (!(TextUtils.isEmpty(phoneticFamilyName) &&
    359                         TextUtils.isEmpty(phoneticMiddleName) &&
    360                         TextUtils.isEmpty(phoneticGivenName))) {
    361             mBuilder.append(VCARD_PARAM_SEPARATOR);
    362             final String sortAs = escapeCharacters(phoneticFamilyName)
    363                     + ';' + escapeCharacters(phoneticGivenName)
    364                     + ';' + escapeCharacters(phoneticMiddleName);
    365             mBuilder.append("SORT-AS=").append(
    366                     VCardUtils.toStringAsV40ParamValue(sortAs));
    367         }
    368 
    369         mBuilder.append(VCARD_DATA_SEPARATOR);
    370         mBuilder.append(escapedFamily);
    371         mBuilder.append(VCARD_ITEM_SEPARATOR);
    372         mBuilder.append(escapedGiven);
    373         mBuilder.append(VCARD_ITEM_SEPARATOR);
    374         mBuilder.append(escapedMiddle);
    375         mBuilder.append(VCARD_ITEM_SEPARATOR);
    376         mBuilder.append(escapedPrefix);
    377         mBuilder.append(VCARD_ITEM_SEPARATOR);
    378         mBuilder.append(escapedSuffix);
    379         mBuilder.append(VCARD_END_OF_LINE);
    380 
    381         if (TextUtils.isEmpty(formattedName)) {
    382             // Note:
    383             // DISPLAY_NAME doesn't exist while some other elements do, which is usually
    384             // weird in Android, as DISPLAY_NAME should (usually) be constructed
    385             // from the others using locale information and its code points.
    386             Log.w(LOG_TAG, "DISPLAY_NAME is empty.");
    387 
    388             final String escaped = escapeCharacters(VCardUtils.constructNameFromElements(
    389                     VCardConfig.getNameOrderType(mVCardType),
    390                     familyName, middleName, givenName, prefix, suffix));
    391             appendLine(VCardConstants.PROPERTY_FN, escaped);
    392         } else {
    393             final String escapedFormatted = escapeCharacters(formattedName);
    394             mBuilder.append(VCardConstants.PROPERTY_FN);
    395             mBuilder.append(VCARD_DATA_SEPARATOR);
    396             mBuilder.append(escapedFormatted);
    397             mBuilder.append(VCARD_END_OF_LINE);
    398         }
    399 
    400         // We may need X- properties for phonetic names.
    401         appendPhoneticNameFields(contentValues);
    402         return this;
    403     }
    404 
    405     /**
    406      * For safety, we'll emit just one value around StructuredName, as external importers
    407      * may get confused with multiple "N", "FN", etc. properties, though it is valid in
    408      * vCard spec.
    409      */
    410     public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) {
    411         if (VCardConfig.isVersion40(mVCardType)) {
    412             return appendNamePropertiesV40(contentValuesList);
    413         }
    414 
    415         if (contentValuesList == null || contentValuesList.isEmpty()) {
    416             if (VCardConfig.isVersion30(mVCardType)) {
    417                 // vCard 3.0 requires "N" and "FN" properties.
    418                 // vCard 4.0 does NOT require N, but we take care of possible backward
    419                 // compatibility issues.
    420                 appendLine(VCardConstants.PROPERTY_N, "");
    421                 appendLine(VCardConstants.PROPERTY_FN, "");
    422             } else if (mIsDoCoMo) {
    423                 appendLine(VCardConstants.PROPERTY_N, "");
    424             }
    425             return this;
    426         }
    427 
    428         final ContentValues contentValues =
    429                 getPrimaryContentValueWithStructuredName(contentValuesList);
    430         final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
    431         final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
    432         final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
    433         final String prefix = contentValues.getAsString(StructuredName.PREFIX);
    434         final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
    435         final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
    436 
    437         if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
    438             final boolean reallyAppendCharsetParameterToName =
    439                     shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
    440             final boolean reallyUseQuotedPrintableToName =
    441                     (!mRefrainsQPToNameProperties &&
    442                             !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
    443                                     VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
    444                                     VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
    445                                     VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
    446                                     VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
    447 
    448             final String formattedName;
    449             if (!TextUtils.isEmpty(displayName)) {
    450                 formattedName = displayName;
    451             } else {
    452                 formattedName = VCardUtils.constructNameFromElements(
    453                         VCardConfig.getNameOrderType(mVCardType),
    454                         familyName, middleName, givenName, prefix, suffix);
    455             }
    456             final boolean reallyAppendCharsetParameterToFN =
    457                     shouldAppendCharsetParam(formattedName);
    458             final boolean reallyUseQuotedPrintableToFN =
    459                     !mRefrainsQPToNameProperties &&
    460                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
    461 
    462             final String encodedFamily;
    463             final String encodedGiven;
    464             final String encodedMiddle;
    465             final String encodedPrefix;
    466             final String encodedSuffix;
    467             if (reallyUseQuotedPrintableToName) {
    468                 encodedFamily = encodeQuotedPrintable(familyName);
    469                 encodedGiven = encodeQuotedPrintable(givenName);
    470                 encodedMiddle = encodeQuotedPrintable(middleName);
    471                 encodedPrefix = encodeQuotedPrintable(prefix);
    472                 encodedSuffix = encodeQuotedPrintable(suffix);
    473             } else {
    474                 encodedFamily = escapeCharacters(familyName);
    475                 encodedGiven = escapeCharacters(givenName);
    476                 encodedMiddle = escapeCharacters(middleName);
    477                 encodedPrefix = escapeCharacters(prefix);
    478                 encodedSuffix = escapeCharacters(suffix);
    479             }
    480 
    481             final String encodedFormattedname =
    482                     (reallyUseQuotedPrintableToFN ?
    483                             encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
    484 
    485             mBuilder.append(VCardConstants.PROPERTY_N);
    486             if (mIsDoCoMo) {
    487                 if (reallyAppendCharsetParameterToName) {
    488                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    489                     mBuilder.append(mVCardCharsetParameter);
    490                 }
    491                 if (reallyUseQuotedPrintableToName) {
    492                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    493                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    494                 }
    495                 mBuilder.append(VCARD_DATA_SEPARATOR);
    496                 // DoCoMo phones require that all the elements in the "family name" field.
    497                 mBuilder.append(formattedName);
    498                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    499                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    500                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    501                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    502             } else {
    503                 if (reallyAppendCharsetParameterToName) {
    504                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    505                     mBuilder.append(mVCardCharsetParameter);
    506                 }
    507                 if (reallyUseQuotedPrintableToName) {
    508                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    509                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    510                 }
    511                 mBuilder.append(VCARD_DATA_SEPARATOR);
    512                 mBuilder.append(encodedFamily);
    513                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    514                 mBuilder.append(encodedGiven);
    515                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    516                 mBuilder.append(encodedMiddle);
    517                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    518                 mBuilder.append(encodedPrefix);
    519                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    520                 mBuilder.append(encodedSuffix);
    521             }
    522             mBuilder.append(VCARD_END_OF_LINE);
    523 
    524             // FN property
    525             mBuilder.append(VCardConstants.PROPERTY_FN);
    526             if (reallyAppendCharsetParameterToFN) {
    527                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    528                 mBuilder.append(mVCardCharsetParameter);
    529             }
    530             if (reallyUseQuotedPrintableToFN) {
    531                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    532                 mBuilder.append(VCARD_PARAM_ENCODING_QP);
    533             }
    534             mBuilder.append(VCARD_DATA_SEPARATOR);
    535             mBuilder.append(encodedFormattedname);
    536             mBuilder.append(VCARD_END_OF_LINE);
    537         } else if (!TextUtils.isEmpty(displayName)) {
    538 
    539             // N
    540             buildSinglePartNameField(VCardConstants.PROPERTY_N, displayName);
    541             mBuilder.append(VCARD_ITEM_SEPARATOR);
    542             mBuilder.append(VCARD_ITEM_SEPARATOR);
    543             mBuilder.append(VCARD_ITEM_SEPARATOR);
    544             mBuilder.append(VCARD_ITEM_SEPARATOR);
    545             mBuilder.append(VCARD_END_OF_LINE);
    546 
    547             // FN
    548             buildSinglePartNameField(VCardConstants.PROPERTY_FN, displayName);
    549             mBuilder.append(VCARD_END_OF_LINE);
    550 
    551         } else if (VCardConfig.isVersion30(mVCardType)) {
    552             appendLine(VCardConstants.PROPERTY_N, "");
    553             appendLine(VCardConstants.PROPERTY_FN, "");
    554         } else if (mIsDoCoMo) {
    555             appendLine(VCardConstants.PROPERTY_N, "");
    556         }
    557 
    558         appendPhoneticNameFields(contentValues);
    559         return this;
    560     }
    561 
    562     private void buildSinglePartNameField(String property, String part) {
    563         final boolean reallyUseQuotedPrintable =
    564                 (!mRefrainsQPToNameProperties &&
    565                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(part));
    566         final String encodedPart = reallyUseQuotedPrintable ?
    567                 encodeQuotedPrintable(part) :
    568                 escapeCharacters(part);
    569 
    570         mBuilder.append(property);
    571 
    572         // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
    573         //       when it would be useful or necessary for external importers,
    574         //       assuming the external importer allows this vioration of the spec.
    575         if (shouldAppendCharsetParam(part)) {
    576             mBuilder.append(VCARD_PARAM_SEPARATOR);
    577             mBuilder.append(mVCardCharsetParameter);
    578         }
    579         if (reallyUseQuotedPrintable) {
    580             mBuilder.append(VCARD_PARAM_SEPARATOR);
    581             mBuilder.append(VCARD_PARAM_ENCODING_QP);
    582         }
    583         mBuilder.append(VCARD_DATA_SEPARATOR);
    584         mBuilder.append(encodedPart);
    585     }
    586 
    587     /**
    588      * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY.
    589      */
    590     private void appendPhoneticNameFields(final ContentValues contentValues) {
    591         final String phoneticFamilyName;
    592         final String phoneticMiddleName;
    593         final String phoneticGivenName;
    594         {
    595             final String tmpPhoneticFamilyName =
    596                 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
    597             final String tmpPhoneticMiddleName =
    598                 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
    599             final String tmpPhoneticGivenName =
    600                 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
    601             if (mNeedsToConvertPhoneticString) {
    602                 phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
    603                 phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
    604                 phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
    605             } else {
    606                 phoneticFamilyName = tmpPhoneticFamilyName;
    607                 phoneticMiddleName = tmpPhoneticMiddleName;
    608                 phoneticGivenName = tmpPhoneticGivenName;
    609             }
    610         }
    611 
    612         if (TextUtils.isEmpty(phoneticFamilyName)
    613                 && TextUtils.isEmpty(phoneticMiddleName)
    614                 && TextUtils.isEmpty(phoneticGivenName)) {
    615             if (mIsDoCoMo) {
    616                 mBuilder.append(VCardConstants.PROPERTY_SOUND);
    617                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    618                 mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
    619                 mBuilder.append(VCARD_DATA_SEPARATOR);
    620                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    621                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    622                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    623                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    624                 mBuilder.append(VCARD_END_OF_LINE);
    625             }
    626             return;
    627         }
    628 
    629         if (VCardConfig.isVersion40(mVCardType)) {
    630             // We don't want SORT-STRING anyway.
    631         } else if (VCardConfig.isVersion30(mVCardType)) {
    632             final String sortString =
    633                     VCardUtils.constructNameFromElements(mVCardType,
    634                             phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
    635             mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
    636             if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) {
    637                 // vCard 3.0 does not force us to use UTF-8 and actually we see some
    638                 // programs which emit this value. It is incorrect from the view of
    639                 // specification, but actually necessary for parsing vCard with non-UTF-8
    640                 // charsets, expecting other parsers not get confused with this value.
    641                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    642                 mBuilder.append(mVCardCharsetParameter);
    643             }
    644             mBuilder.append(VCARD_DATA_SEPARATOR);
    645             mBuilder.append(escapeCharacters(sortString));
    646             mBuilder.append(VCARD_END_OF_LINE);
    647         } else if (mIsJapaneseMobilePhone) {
    648             // Note: There is no appropriate property for expressing
    649             //       phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
    650             //       vCard 3.0 (SORT-STRING).
    651             //       We use DoCoMo's way when the device is Japanese one since it is already
    652             //       supported by a lot of Japanese mobile phones.
    653             //       This is "X-" property, so any parser hopefully would not get
    654             //       confused with this.
    655             //
    656             //       Also, DoCoMo's specification requires vCard composer to use just the first
    657             //       column.
    658             //       i.e.
    659             //       good:  SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
    660             //       bad :  SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
    661             mBuilder.append(VCardConstants.PROPERTY_SOUND);
    662             mBuilder.append(VCARD_PARAM_SEPARATOR);
    663             mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
    664 
    665             boolean reallyUseQuotedPrintable =
    666                 (!mRefrainsQPToNameProperties
    667                         && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
    668                                 phoneticFamilyName)
    669                                 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
    670                                         phoneticMiddleName)
    671                                 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
    672                                         phoneticGivenName)));
    673 
    674             final String encodedPhoneticFamilyName;
    675             final String encodedPhoneticMiddleName;
    676             final String encodedPhoneticGivenName;
    677             if (reallyUseQuotedPrintable) {
    678                 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
    679                 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
    680                 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
    681             } else {
    682                 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
    683                 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
    684                 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
    685             }
    686 
    687             if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
    688                     encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
    689                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    690                 mBuilder.append(mVCardCharsetParameter);
    691             }
    692             mBuilder.append(VCARD_DATA_SEPARATOR);
    693             {
    694                 boolean first = true;
    695                 if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
    696                     mBuilder.append(encodedPhoneticFamilyName);
    697                     first = false;
    698                 }
    699                 if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
    700                     if (first) {
    701                         first = false;
    702                     } else {
    703                         mBuilder.append(' ');
    704                     }
    705                     mBuilder.append(encodedPhoneticMiddleName);
    706                 }
    707                 if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
    708                     if (!first) {
    709                         mBuilder.append(' ');
    710                     }
    711                     mBuilder.append(encodedPhoneticGivenName);
    712                 }
    713             }
    714             mBuilder.append(VCARD_ITEM_SEPARATOR);  // family;given
    715             mBuilder.append(VCARD_ITEM_SEPARATOR);  // given;middle
    716             mBuilder.append(VCARD_ITEM_SEPARATOR);  // middle;prefix
    717             mBuilder.append(VCARD_ITEM_SEPARATOR);  // prefix;suffix
    718             mBuilder.append(VCARD_END_OF_LINE);
    719         }
    720 
    721         if (mUsesDefactProperty) {
    722             if (!TextUtils.isEmpty(phoneticGivenName)) {
    723                 final boolean reallyUseQuotedPrintable =
    724                     (mShouldUseQuotedPrintable &&
    725                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
    726                 final String encodedPhoneticGivenName;
    727                 if (reallyUseQuotedPrintable) {
    728                     encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
    729                 } else {
    730                     encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
    731                 }
    732                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
    733                 if (shouldAppendCharsetParam(phoneticGivenName)) {
    734                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    735                     mBuilder.append(mVCardCharsetParameter);
    736                 }
    737                 if (reallyUseQuotedPrintable) {
    738                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    739                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    740                 }
    741                 mBuilder.append(VCARD_DATA_SEPARATOR);
    742                 mBuilder.append(encodedPhoneticGivenName);
    743                 mBuilder.append(VCARD_END_OF_LINE);
    744             }  // if (!TextUtils.isEmpty(phoneticGivenName))
    745             if (!TextUtils.isEmpty(phoneticMiddleName)) {
    746                 final boolean reallyUseQuotedPrintable =
    747                     (mShouldUseQuotedPrintable &&
    748                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
    749                 final String encodedPhoneticMiddleName;
    750                 if (reallyUseQuotedPrintable) {
    751                     encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
    752                 } else {
    753                     encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
    754                 }
    755                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
    756                 if (shouldAppendCharsetParam(phoneticMiddleName)) {
    757                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    758                     mBuilder.append(mVCardCharsetParameter);
    759                 }
    760                 if (reallyUseQuotedPrintable) {
    761                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    762                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    763                 }
    764                 mBuilder.append(VCARD_DATA_SEPARATOR);
    765                 mBuilder.append(encodedPhoneticMiddleName);
    766                 mBuilder.append(VCARD_END_OF_LINE);
    767             }  // if (!TextUtils.isEmpty(phoneticGivenName))
    768             if (!TextUtils.isEmpty(phoneticFamilyName)) {
    769                 final boolean reallyUseQuotedPrintable =
    770                     (mShouldUseQuotedPrintable &&
    771                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
    772                 final String encodedPhoneticFamilyName;
    773                 if (reallyUseQuotedPrintable) {
    774                     encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
    775                 } else {
    776                     encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
    777                 }
    778                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
    779                 if (shouldAppendCharsetParam(phoneticFamilyName)) {
    780                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    781                     mBuilder.append(mVCardCharsetParameter);
    782                 }
    783                 if (reallyUseQuotedPrintable) {
    784                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    785                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    786                 }
    787                 mBuilder.append(VCARD_DATA_SEPARATOR);
    788                 mBuilder.append(encodedPhoneticFamilyName);
    789                 mBuilder.append(VCARD_END_OF_LINE);
    790             }  // if (!TextUtils.isEmpty(phoneticFamilyName))
    791         }
    792     }
    793 
    794     public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
    795         final boolean useAndroidProperty;
    796         if (mIsV30OrV40) {   // These specifications have NICKNAME property.
    797             useAndroidProperty = false;
    798         } else if (mUsesAndroidProperty) {
    799             useAndroidProperty = true;
    800         } else {
    801             // There's no way to add this field.
    802             return this;
    803         }
    804         if (contentValuesList != null) {
    805             for (ContentValues contentValues : contentValuesList) {
    806                 final String nickname = contentValues.getAsString(Nickname.NAME);
    807                 if (TextUtils.isEmpty(nickname)) {
    808                     continue;
    809                 }
    810                 if (useAndroidProperty) {
    811                     appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
    812                 } else {
    813                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
    814                 }
    815             }
    816         }
    817         return this;
    818     }
    819 
    820     public VCardBuilder appendPhones(final List<ContentValues> contentValuesList,
    821             VCardPhoneNumberTranslationCallback translationCallback) {
    822         boolean phoneLineExists = false;
    823         if (contentValuesList != null) {
    824             Set<String> phoneSet = new HashSet<String>();
    825             for (ContentValues contentValues : contentValuesList) {
    826                 final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
    827                 final String label = contentValues.getAsString(Phone.LABEL);
    828                 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
    829                 final boolean isPrimary = (isPrimaryAsInteger != null ?
    830                         (isPrimaryAsInteger > 0) : false);
    831                 String phoneNumber = contentValues.getAsString(Phone.NUMBER);
    832                 if (phoneNumber != null) {
    833                     phoneNumber = phoneNumber.trim();
    834                 }
    835                 if (TextUtils.isEmpty(phoneNumber)) {
    836                     continue;
    837                 }
    838 
    839                 final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
    840                 // Note: We prioritize this callback over FLAG_REFRAIN_PHONE_NUMBER_FORMATTING
    841                 // intentionally. In the future the flag will be replaced by callback
    842                 // mechanism entirely.
    843                 if (translationCallback != null) {
    844                     phoneNumber = translationCallback.onValueReceived(
    845                             phoneNumber, type, label, isPrimary);
    846                     if (!phoneSet.contains(phoneNumber)) {
    847                         phoneSet.add(phoneNumber);
    848                         appendTelLine(type, label, phoneNumber, isPrimary);
    849                     }
    850                 } else if (type == Phone.TYPE_PAGER ||
    851                         VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
    852                     // Note: PAGER number needs unformatted "phone number".
    853                     phoneLineExists = true;
    854                     if (!phoneSet.contains(phoneNumber)) {
    855                         phoneSet.add(phoneNumber);
    856                         appendTelLine(type, label, phoneNumber, isPrimary);
    857                     }
    858                 } else {
    859                     final List<String> phoneNumberList = splitPhoneNumbers(phoneNumber);
    860                     if (phoneNumberList.isEmpty()) {
    861                         continue;
    862                     }
    863                     phoneLineExists = true;
    864                     for (String actualPhoneNumber : phoneNumberList) {
    865                         if (!phoneSet.contains(actualPhoneNumber)) {
    866                             // 'p' and 'w' are the standard characters for pause and wait
    867                             // (see RFC 3601)
    868                             // so use those when exporting phone numbers via vCard.
    869                             String numberWithControlSequence = actualPhoneNumber
    870                                     .replace(PhoneNumberUtils.PAUSE, 'p')
    871                                     .replace(PhoneNumberUtils.WAIT, 'w');
    872                             String formatted;
    873                             // TODO: remove this code and relevant test cases. vCard and any other
    874                             // codes using it shouldn't rely on the formatter here.
    875                             if (TextUtils.equals(numberWithControlSequence, actualPhoneNumber)) {
    876                                 StringBuilder digitsOnlyBuilder = new StringBuilder();
    877                                 final int length = actualPhoneNumber.length();
    878                                 for (int i = 0; i < length; i++) {
    879                                     final char ch = actualPhoneNumber.charAt(i);
    880                                     if (Character.isDigit(ch) || ch == '+') {
    881                                         digitsOnlyBuilder.append(ch);
    882                                     }
    883                                 }
    884                                 final int phoneFormat =
    885                                         VCardUtils.getPhoneNumberFormat(mVCardType);
    886                                 formatted = PhoneNumberUtilsPort.formatNumber(
    887                                         digitsOnlyBuilder.toString(), phoneFormat);
    888                             } else {
    889                                 // Be conservative.
    890                                 formatted = numberWithControlSequence;
    891                             }
    892 
    893                             // In vCard 4.0, value type must be "a single URI value",
    894                             // not just a phone number. (Based on vCard 4.0 rev.13)
    895                             if (VCardConfig.isVersion40(mVCardType)
    896                                     && !TextUtils.isEmpty(formatted)
    897                                     && !formatted.startsWith("tel:")) {
    898                                 formatted = "tel:" + formatted;
    899                             }
    900 
    901                             // Pre-formatted string should be stored.
    902                             phoneSet.add(actualPhoneNumber);
    903                             appendTelLine(type, label, formatted, isPrimary);
    904                         }
    905                     }  // for (String actualPhoneNumber : phoneNumberList) {
    906 
    907                     // TODO: TEL with SIP URI?
    908                 }
    909             }
    910         }
    911 
    912         if (!phoneLineExists && mIsDoCoMo) {
    913             appendTelLine(Phone.TYPE_HOME, "", "", false);
    914         }
    915 
    916         return this;
    917     }
    918 
    919     /**
    920      * <p>
    921      * Splits a given string expressing phone numbers into several strings, and remove
    922      * unnecessary characters inside them. The size of a returned list becomes 1 when
    923      * no split is needed.
    924      * </p>
    925      * <p>
    926      * The given number "may" have several phone numbers when the contact entry is corrupted
    927      * because of its original source.
    928      * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
    929      * </p>
    930      * <p>
    931      * This kind of "phone numbers" will not be created with Android vCard implementation,
    932      * but we may encounter them if the source of the input data has already corrupted
    933      * implementation.
    934      * </p>
    935      * <p>
    936      * To handle this case, this method first splits its input into multiple parts
    937      * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
    938      * removes unnecessary strings like "(Miami)".
    939      * </p>
    940      * <p>
    941      * Do not call this method when trimming is inappropriate for its receivers.
    942      * </p>
    943      */
    944     private List<String> splitPhoneNumbers(final String phoneNumber) {
    945         final List<String> phoneList = new ArrayList<String>();
    946 
    947         StringBuilder builder = new StringBuilder();
    948         final int length = phoneNumber.length();
    949         for (int i = 0; i < length; i++) {
    950             final char ch = phoneNumber.charAt(i);
    951             if (ch == '\n' && builder.length() > 0) {
    952                 phoneList.add(builder.toString());
    953                 builder = new StringBuilder();
    954             } else {
    955                 builder.append(ch);
    956             }
    957         }
    958         if (builder.length() > 0) {
    959             phoneList.add(builder.toString());
    960         }
    961         return phoneList;
    962     }
    963 
    964     public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
    965         boolean emailAddressExists = false;
    966         if (contentValuesList != null) {
    967             final Set<String> addressSet = new HashSet<String>();
    968             for (ContentValues contentValues : contentValuesList) {
    969                 String emailAddress = contentValues.getAsString(Email.DATA);
    970                 if (emailAddress != null) {
    971                     emailAddress = emailAddress.trim();
    972                 }
    973                 if (TextUtils.isEmpty(emailAddress)) {
    974                     continue;
    975                 }
    976                 Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
    977                 final int type = (typeAsObject != null ?
    978                         typeAsObject : DEFAULT_EMAIL_TYPE);
    979                 final String label = contentValues.getAsString(Email.LABEL);
    980                 Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
    981                 final boolean isPrimary = (isPrimaryAsInteger != null ?
    982                         (isPrimaryAsInteger > 0) : false);
    983                 emailAddressExists = true;
    984                 if (!addressSet.contains(emailAddress)) {
    985                     addressSet.add(emailAddress);
    986                     appendEmailLine(type, label, emailAddress, isPrimary);
    987                 }
    988             }
    989         }
    990 
    991         if (!emailAddressExists && mIsDoCoMo) {
    992             appendEmailLine(Email.TYPE_HOME, "", "", false);
    993         }
    994 
    995         return this;
    996     }
    997 
    998     public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
    999         if (contentValuesList == null || contentValuesList.isEmpty()) {
   1000             if (mIsDoCoMo) {
   1001                 mBuilder.append(VCardConstants.PROPERTY_ADR);
   1002                 mBuilder.append(VCARD_PARAM_SEPARATOR);
   1003                 mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
   1004                 mBuilder.append(VCARD_DATA_SEPARATOR);
   1005                 mBuilder.append(VCARD_END_OF_LINE);
   1006             }
   1007         } else {
   1008             if (mIsDoCoMo) {
   1009                 appendPostalsForDoCoMo(contentValuesList);
   1010             } else {
   1011                 appendPostalsForGeneric(contentValuesList);
   1012             }
   1013         }
   1014 
   1015         return this;
   1016     }
   1017 
   1018     private static final Map<Integer, Integer> sPostalTypePriorityMap;
   1019 
   1020     static {
   1021         sPostalTypePriorityMap = new HashMap<Integer, Integer>();
   1022         sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
   1023         sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
   1024         sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
   1025         sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
   1026     }
   1027 
   1028     /**
   1029      * Tries to append just one line. If there's no appropriate address
   1030      * information, append an empty line.
   1031      */
   1032     private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
   1033         int currentPriority = Integer.MAX_VALUE;
   1034         int currentType = Integer.MAX_VALUE;
   1035         ContentValues currentContentValues = null;
   1036         for (final ContentValues contentValues : contentValuesList) {
   1037             if (contentValues == null) {
   1038                 continue;
   1039             }
   1040             final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
   1041             final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
   1042             final int priority =
   1043                     (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
   1044             if (priority < currentPriority) {
   1045                 currentPriority = priority;
   1046                 currentType = typeAsInteger;
   1047                 currentContentValues = contentValues;
   1048                 if (priority == 0) {
   1049                     break;
   1050                 }
   1051             }
   1052         }
   1053 
   1054         if (currentContentValues == null) {
   1055             Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
   1056             return;
   1057         }
   1058 
   1059         final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
   1060         appendPostalLine(currentType, label, currentContentValues, false, true);
   1061     }
   1062 
   1063     private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
   1064         for (final ContentValues contentValues : contentValuesList) {
   1065             if (contentValues == null) {
   1066                 continue;
   1067             }
   1068             final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
   1069             final int type = (typeAsInteger != null ?
   1070                     typeAsInteger : DEFAULT_POSTAL_TYPE);
   1071             final String label = contentValues.getAsString(StructuredPostal.LABEL);
   1072             final Integer isPrimaryAsInteger =
   1073                 contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
   1074             final boolean isPrimary = (isPrimaryAsInteger != null ?
   1075                     (isPrimaryAsInteger > 0) : false);
   1076             appendPostalLine(type, label, contentValues, isPrimary, false);
   1077         }
   1078     }
   1079 
   1080     private static class PostalStruct {
   1081         final boolean reallyUseQuotedPrintable;
   1082         final boolean appendCharset;
   1083         final String addressData;
   1084         public PostalStruct(final boolean reallyUseQuotedPrintable,
   1085                 final boolean appendCharset, final String addressData) {
   1086             this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
   1087             this.appendCharset = appendCharset;
   1088             this.addressData = addressData;
   1089         }
   1090     }
   1091 
   1092     /**
   1093      * @return null when there's no information available to construct the data.
   1094      */
   1095     private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
   1096         // adr-value    = 0*6(text-value ";") text-value
   1097         //              ; PO Box, Extended Address, Street, Locality, Region, Postal
   1098         //              ; Code, Country Name
   1099         final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
   1100         final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
   1101         final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
   1102         final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
   1103         final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
   1104         final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
   1105         final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
   1106         final String[] rawAddressArray = new String[]{
   1107                 rawPoBox, rawNeighborhood, rawStreet, rawLocality,
   1108                 rawRegion, rawPostalCode, rawCountry};
   1109         if (!VCardUtils.areAllEmpty(rawAddressArray)) {
   1110             final boolean reallyUseQuotedPrintable =
   1111                 (mShouldUseQuotedPrintable &&
   1112                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
   1113             final boolean appendCharset =
   1114                 !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
   1115             final String encodedPoBox;
   1116             final String encodedStreet;
   1117             final String encodedLocality;
   1118             final String encodedRegion;
   1119             final String encodedPostalCode;
   1120             final String encodedCountry;
   1121             final String encodedNeighborhood;
   1122 
   1123             final String rawLocality2;
   1124             // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
   1125             // but this is intentional.
   1126             //
   1127             // QP encoding may add line feeds when needed and the result of
   1128             // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
   1129             // may be different from
   1130             // - encodedLocality + " " + encodedNeighborhood.
   1131             //
   1132             // We use safer way.
   1133             if (TextUtils.isEmpty(rawLocality)) {
   1134                 if (TextUtils.isEmpty(rawNeighborhood)) {
   1135                     rawLocality2 = "";
   1136                 } else {
   1137                     rawLocality2 = rawNeighborhood;
   1138                 }
   1139             } else {
   1140                 if (TextUtils.isEmpty(rawNeighborhood)) {
   1141                     rawLocality2 = rawLocality;
   1142                 } else {
   1143                     rawLocality2 = rawLocality + " " + rawNeighborhood;
   1144                 }
   1145             }
   1146             if (reallyUseQuotedPrintable) {
   1147                 encodedPoBox = encodeQuotedPrintable(rawPoBox);
   1148                 encodedStreet = encodeQuotedPrintable(rawStreet);
   1149                 encodedLocality = encodeQuotedPrintable(rawLocality2);
   1150                 encodedRegion = encodeQuotedPrintable(rawRegion);
   1151                 encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
   1152                 encodedCountry = encodeQuotedPrintable(rawCountry);
   1153             } else {
   1154                 encodedPoBox = escapeCharacters(rawPoBox);
   1155                 encodedStreet = escapeCharacters(rawStreet);
   1156                 encodedLocality = escapeCharacters(rawLocality2);
   1157                 encodedRegion = escapeCharacters(rawRegion);
   1158                 encodedPostalCode = escapeCharacters(rawPostalCode);
   1159                 encodedCountry = escapeCharacters(rawCountry);
   1160                 encodedNeighborhood = escapeCharacters(rawNeighborhood);
   1161             }
   1162             final StringBuilder addressBuilder = new StringBuilder();
   1163             addressBuilder.append(encodedPoBox);
   1164             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
   1165             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
   1166             addressBuilder.append(encodedStreet);
   1167             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
   1168             addressBuilder.append(encodedLocality);
   1169             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
   1170             addressBuilder.append(encodedRegion);
   1171             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
   1172             addressBuilder.append(encodedPostalCode);
   1173             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
   1174             addressBuilder.append(encodedCountry);
   1175             return new PostalStruct(
   1176                     reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
   1177         } else {  // VCardUtils.areAllEmpty(rawAddressArray) == true
   1178             // Try to use FORMATTED_ADDRESS instead.
   1179             final String rawFormattedAddress =
   1180                 contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
   1181             if (TextUtils.isEmpty(rawFormattedAddress)) {
   1182                 return null;
   1183             }
   1184             final boolean reallyUseQuotedPrintable =
   1185                 (mShouldUseQuotedPrintable &&
   1186                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
   1187             final boolean appendCharset =
   1188                 !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
   1189             final String encodedFormattedAddress;
   1190             if (reallyUseQuotedPrintable) {
   1191                 encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
   1192             } else {
   1193                 encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
   1194             }
   1195 
   1196             // We use the second value ("Extended Address") just because Japanese mobile phones
   1197             // do so. If the other importer expects the value be in the other field, some flag may
   1198             // be needed.
   1199             final StringBuilder addressBuilder = new StringBuilder();
   1200             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
   1201             addressBuilder.append(encodedFormattedAddress);
   1202             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
   1203             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
   1204             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
   1205             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
   1206             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
   1207             return new PostalStruct(
   1208                     reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
   1209         }
   1210     }
   1211 
   1212     public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
   1213         if (contentValuesList != null) {
   1214             for (ContentValues contentValues : contentValuesList) {
   1215                 final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
   1216                 if (protocolAsObject == null) {
   1217                     continue;
   1218                 }
   1219                 final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
   1220                 if (propertyName == null) {
   1221                     continue;
   1222                 }
   1223                 String data = contentValues.getAsString(Im.DATA);
   1224                 if (data != null) {
   1225                     data = data.trim();
   1226                 }
   1227                 if (TextUtils.isEmpty(data)) {
   1228                     continue;
   1229                 }
   1230                 final String typeAsString;
   1231                 {
   1232                     final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
   1233                     switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
   1234                         case Im.TYPE_HOME: {
   1235                             typeAsString = VCardConstants.PARAM_TYPE_HOME;
   1236                             break;
   1237                         }
   1238                         case Im.TYPE_WORK: {
   1239                             typeAsString = VCardConstants.PARAM_TYPE_WORK;
   1240                             break;
   1241                         }
   1242                         case Im.TYPE_CUSTOM: {
   1243                             final String label = contentValues.getAsString(Im.LABEL);
   1244                             typeAsString = (label != null ? "X-" + label : null);
   1245                             break;
   1246                         }
   1247                         case Im.TYPE_OTHER:  // Ignore
   1248                         default: {
   1249                             typeAsString = null;
   1250                             break;
   1251                         }
   1252                     }
   1253                 }
   1254 
   1255                 final List<String> parameterList = new ArrayList<String>();
   1256                 if (!TextUtils.isEmpty(typeAsString)) {
   1257                     parameterList.add(typeAsString);
   1258                 }
   1259                 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
   1260                 final boolean isPrimary = (isPrimaryAsInteger != null ?
   1261                         (isPrimaryAsInteger > 0) : false);
   1262                 if (isPrimary) {
   1263                     parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1264                 }
   1265 
   1266                 appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
   1267             }
   1268         }
   1269         return this;
   1270     }
   1271 
   1272     public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
   1273         if (contentValuesList != null) {
   1274             for (ContentValues contentValues : contentValuesList) {
   1275                 String website = contentValues.getAsString(Website.URL);
   1276                 if (website != null) {
   1277                     website = website.trim();
   1278                 }
   1279 
   1280                 // Note: vCard 3.0 does not allow any parameter addition toward "URL"
   1281                 //       property, while there's no document in vCard 2.1.
   1282                 if (!TextUtils.isEmpty(website)) {
   1283                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
   1284                 }
   1285             }
   1286         }
   1287         return this;
   1288     }
   1289 
   1290     public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
   1291         if (contentValuesList != null) {
   1292             for (ContentValues contentValues : contentValuesList) {
   1293                 String company = contentValues.getAsString(Organization.COMPANY);
   1294                 if (company != null) {
   1295                     company = company.trim();
   1296                 }
   1297                 String department = contentValues.getAsString(Organization.DEPARTMENT);
   1298                 if (department != null) {
   1299                     department = department.trim();
   1300                 }
   1301                 String title = contentValues.getAsString(Organization.TITLE);
   1302                 if (title != null) {
   1303                     title = title.trim();
   1304                 }
   1305 
   1306                 StringBuilder orgBuilder = new StringBuilder();
   1307                 if (!TextUtils.isEmpty(company)) {
   1308                     orgBuilder.append(company);
   1309                 }
   1310                 if (!TextUtils.isEmpty(department)) {
   1311                     if (orgBuilder.length() > 0) {
   1312                         orgBuilder.append(';');
   1313                     }
   1314                     orgBuilder.append(department);
   1315                 }
   1316                 final String orgline = orgBuilder.toString();
   1317                 appendLine(VCardConstants.PROPERTY_ORG, orgline,
   1318                         !VCardUtils.containsOnlyPrintableAscii(orgline),
   1319                         (mShouldUseQuotedPrintable &&
   1320                                 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
   1321 
   1322                 if (!TextUtils.isEmpty(title)) {
   1323                     appendLine(VCardConstants.PROPERTY_TITLE, title,
   1324                             !VCardUtils.containsOnlyPrintableAscii(title),
   1325                             (mShouldUseQuotedPrintable &&
   1326                                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
   1327                 }
   1328             }
   1329         }
   1330         return this;
   1331     }
   1332 
   1333     public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
   1334         if (contentValuesList != null) {
   1335             for (ContentValues contentValues : contentValuesList) {
   1336                 if (contentValues == null) {
   1337                     continue;
   1338                 }
   1339                 byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
   1340                 if (data == null) {
   1341                     continue;
   1342                 }
   1343                 final String photoType = VCardUtils.guessImageType(data);
   1344                 if (photoType == null) {
   1345                     Log.d(LOG_TAG, "Unknown photo type. Ignored.");
   1346                     continue;
   1347                 }
   1348                 // TODO: check this works fine.
   1349                 final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
   1350                 if (!TextUtils.isEmpty(photoString)) {
   1351                     appendPhotoLine(photoString, photoType);
   1352                 }
   1353             }
   1354         }
   1355         return this;
   1356     }
   1357 
   1358     public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
   1359         if (contentValuesList != null) {
   1360             if (mOnlyOneNoteFieldIsAvailable) {
   1361                 final StringBuilder noteBuilder = new StringBuilder();
   1362                 boolean first = true;
   1363                 for (final ContentValues contentValues : contentValuesList) {
   1364                     String note = contentValues.getAsString(Note.NOTE);
   1365                     if (note == null) {
   1366                         note = "";
   1367                     }
   1368                     if (note.length() > 0) {
   1369                         if (first) {
   1370                             first = false;
   1371                         } else {
   1372                             noteBuilder.append('\n');
   1373                         }
   1374                         noteBuilder.append(note);
   1375                     }
   1376                 }
   1377                 final String noteStr = noteBuilder.toString();
   1378                 // This means we scan noteStr completely twice, which is redundant.
   1379                 // But for now, we assume this is not so time-consuming..
   1380                 final boolean shouldAppendCharsetInfo =
   1381                     !VCardUtils.containsOnlyPrintableAscii(noteStr);
   1382                 final boolean reallyUseQuotedPrintable =
   1383                         (mShouldUseQuotedPrintable &&
   1384                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
   1385                 appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
   1386                         shouldAppendCharsetInfo, reallyUseQuotedPrintable);
   1387             } else {
   1388                 for (ContentValues contentValues : contentValuesList) {
   1389                     final String noteStr = contentValues.getAsString(Note.NOTE);
   1390                     if (!TextUtils.isEmpty(noteStr)) {
   1391                         final boolean shouldAppendCharsetInfo =
   1392                                 !VCardUtils.containsOnlyPrintableAscii(noteStr);
   1393                         final boolean reallyUseQuotedPrintable =
   1394                                 (mShouldUseQuotedPrintable &&
   1395                                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
   1396                         appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
   1397                                 shouldAppendCharsetInfo, reallyUseQuotedPrintable);
   1398                     }
   1399                 }
   1400             }
   1401         }
   1402         return this;
   1403     }
   1404 
   1405     public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
   1406         // There's possibility where a given object may have more than one birthday, which
   1407         // is inappropriate. We just build one birthday.
   1408         if (contentValuesList != null) {
   1409             String primaryBirthday = null;
   1410             String secondaryBirthday = null;
   1411             for (final ContentValues contentValues : contentValuesList) {
   1412                 if (contentValues == null) {
   1413                     continue;
   1414                 }
   1415                 final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
   1416                 final int eventType;
   1417                 if (eventTypeAsInteger != null) {
   1418                     eventType = eventTypeAsInteger;
   1419                 } else {
   1420                     eventType = Event.TYPE_OTHER;
   1421                 }
   1422                 if (eventType == Event.TYPE_BIRTHDAY) {
   1423                     final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
   1424                     if (birthdayCandidate == null) {
   1425                         continue;
   1426                     }
   1427                     final Integer isSuperPrimaryAsInteger =
   1428                         contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
   1429                     final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
   1430                             (isSuperPrimaryAsInteger > 0) : false);
   1431                     if (isSuperPrimary) {
   1432                         // "super primary" birthday should the prefered one.
   1433                         primaryBirthday = birthdayCandidate;
   1434                         break;
   1435                     }
   1436                     final Integer isPrimaryAsInteger =
   1437                         contentValues.getAsInteger(Event.IS_PRIMARY);
   1438                     final boolean isPrimary = (isPrimaryAsInteger != null ?
   1439                             (isPrimaryAsInteger > 0) : false);
   1440                     if (isPrimary) {
   1441                         // We don't break here since "super primary" birthday may exist later.
   1442                         primaryBirthday = birthdayCandidate;
   1443                     } else if (secondaryBirthday == null) {
   1444                         // First entry is set to the "secondary" candidate.
   1445                         secondaryBirthday = birthdayCandidate;
   1446                     }
   1447                 } else if (mUsesAndroidProperty) {
   1448                     // Event types other than Birthday is not supported by vCard.
   1449                     appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
   1450                 }
   1451             }
   1452             if (primaryBirthday != null) {
   1453                 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
   1454                         primaryBirthday.trim());
   1455             } else if (secondaryBirthday != null){
   1456                 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
   1457                         secondaryBirthday.trim());
   1458             }
   1459         }
   1460         return this;
   1461     }
   1462 
   1463     public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
   1464         if (mUsesAndroidProperty && contentValuesList != null) {
   1465             for (final ContentValues contentValues : contentValuesList) {
   1466                 if (contentValues == null) {
   1467                     continue;
   1468                 }
   1469                 appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
   1470             }
   1471         }
   1472         return this;
   1473     }
   1474 
   1475     /**
   1476      * @param emitEveryTime If true, builder builds the line even when there's no entry.
   1477      */
   1478     public void appendPostalLine(final int type, final String label,
   1479             final ContentValues contentValues,
   1480             final boolean isPrimary, final boolean emitEveryTime) {
   1481         final boolean reallyUseQuotedPrintable;
   1482         final boolean appendCharset;
   1483         final String addressValue;
   1484         {
   1485             PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
   1486             if (postalStruct == null) {
   1487                 if (emitEveryTime) {
   1488                     reallyUseQuotedPrintable = false;
   1489                     appendCharset = false;
   1490                     addressValue = "";
   1491                 } else {
   1492                     return;
   1493                 }
   1494             } else {
   1495                 reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
   1496                 appendCharset = postalStruct.appendCharset;
   1497                 addressValue = postalStruct.addressData;
   1498             }
   1499         }
   1500 
   1501         List<String> parameterList = new ArrayList<String>();
   1502         if (isPrimary) {
   1503             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1504         }
   1505         switch (type) {
   1506             case StructuredPostal.TYPE_HOME: {
   1507                 parameterList.add(VCardConstants.PARAM_TYPE_HOME);
   1508                 break;
   1509             }
   1510             case StructuredPostal.TYPE_WORK: {
   1511                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1512                 break;
   1513             }
   1514             case StructuredPostal.TYPE_CUSTOM: {
   1515                 if (!TextUtils.isEmpty(label)
   1516                         && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1517                     // We're not sure whether the label is valid in the spec
   1518                     // ("IANA-token" in the vCard 3.0 is unclear...)
   1519                     // Just  for safety, we add "X-" at the beggining of each label.
   1520                     // Also checks the label obeys with vCard 3.0 spec.
   1521                     parameterList.add("X-" + label);
   1522                 }
   1523                 break;
   1524             }
   1525             case StructuredPostal.TYPE_OTHER: {
   1526                 break;
   1527             }
   1528             default: {
   1529                 Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
   1530                 break;
   1531             }
   1532         }
   1533 
   1534         mBuilder.append(VCardConstants.PROPERTY_ADR);
   1535         if (!parameterList.isEmpty()) {
   1536             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1537             appendTypeParameters(parameterList);
   1538         }
   1539         if (appendCharset) {
   1540             // Strictly, vCard 3.0 does not allow exporters to emit charset information,
   1541             // but we will add it since the information should be useful for importers,
   1542             //
   1543             // Assume no parser does not emit error with this parameter in vCard 3.0.
   1544             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1545             mBuilder.append(mVCardCharsetParameter);
   1546         }
   1547         if (reallyUseQuotedPrintable) {
   1548             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1549             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1550         }
   1551         mBuilder.append(VCARD_DATA_SEPARATOR);
   1552         mBuilder.append(addressValue);
   1553         mBuilder.append(VCARD_END_OF_LINE);
   1554     }
   1555 
   1556     public void appendEmailLine(final int type, final String label,
   1557             final String rawValue, final boolean isPrimary) {
   1558         final String typeAsString;
   1559         switch (type) {
   1560             case Email.TYPE_CUSTOM: {
   1561                 if (VCardUtils.isMobilePhoneLabel(label)) {
   1562                     typeAsString = VCardConstants.PARAM_TYPE_CELL;
   1563                 } else if (!TextUtils.isEmpty(label)
   1564                         && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1565                     typeAsString = "X-" + label;
   1566                 } else {
   1567                     typeAsString = null;
   1568                 }
   1569                 break;
   1570             }
   1571             case Email.TYPE_HOME: {
   1572                 typeAsString = VCardConstants.PARAM_TYPE_HOME;
   1573                 break;
   1574             }
   1575             case Email.TYPE_WORK: {
   1576                 typeAsString = VCardConstants.PARAM_TYPE_WORK;
   1577                 break;
   1578             }
   1579             case Email.TYPE_OTHER: {
   1580                 typeAsString = null;
   1581                 break;
   1582             }
   1583             case Email.TYPE_MOBILE: {
   1584                 typeAsString = VCardConstants.PARAM_TYPE_CELL;
   1585                 break;
   1586             }
   1587             default: {
   1588                 Log.e(LOG_TAG, "Unknown Email type: " + type);
   1589                 typeAsString = null;
   1590                 break;
   1591             }
   1592         }
   1593 
   1594         final List<String> parameterList = new ArrayList<String>();
   1595         if (isPrimary) {
   1596             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1597         }
   1598         if (!TextUtils.isEmpty(typeAsString)) {
   1599             parameterList.add(typeAsString);
   1600         }
   1601 
   1602         appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
   1603                 rawValue);
   1604     }
   1605 
   1606     public void appendTelLine(final Integer typeAsInteger, final String label,
   1607             final String encodedValue, boolean isPrimary) {
   1608         mBuilder.append(VCardConstants.PROPERTY_TEL);
   1609         mBuilder.append(VCARD_PARAM_SEPARATOR);
   1610 
   1611         final int type;
   1612         if (typeAsInteger == null) {
   1613             type = Phone.TYPE_OTHER;
   1614         } else {
   1615             type = typeAsInteger;
   1616         }
   1617 
   1618         ArrayList<String> parameterList = new ArrayList<String>();
   1619         switch (type) {
   1620             case Phone.TYPE_HOME: {
   1621                 parameterList.addAll(
   1622                         Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
   1623                 break;
   1624             }
   1625             case Phone.TYPE_WORK: {
   1626                 parameterList.addAll(
   1627                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
   1628                 break;
   1629             }
   1630             case Phone.TYPE_FAX_HOME: {
   1631                 parameterList.addAll(
   1632                         Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
   1633                 break;
   1634             }
   1635             case Phone.TYPE_FAX_WORK: {
   1636                 parameterList.addAll(
   1637                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
   1638                 break;
   1639             }
   1640             case Phone.TYPE_MOBILE: {
   1641                 parameterList.add(VCardConstants.PARAM_TYPE_CELL);
   1642                 break;
   1643             }
   1644             case Phone.TYPE_PAGER: {
   1645                 if (mIsDoCoMo) {
   1646                     // Not sure about the reason, but previous implementation had
   1647                     // used "VOICE" instead of "PAGER"
   1648                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1649                 } else {
   1650                     parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
   1651                 }
   1652                 break;
   1653             }
   1654             case Phone.TYPE_OTHER: {
   1655                 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1656                 break;
   1657             }
   1658             case Phone.TYPE_CAR: {
   1659                 parameterList.add(VCardConstants.PARAM_TYPE_CAR);
   1660                 break;
   1661             }
   1662             case Phone.TYPE_COMPANY_MAIN: {
   1663                 // There's no relevant field in vCard (at least 2.1).
   1664                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1665                 isPrimary = true;
   1666                 break;
   1667             }
   1668             case Phone.TYPE_ISDN: {
   1669                 parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
   1670                 break;
   1671             }
   1672             case Phone.TYPE_MAIN: {
   1673                 isPrimary = true;
   1674                 break;
   1675             }
   1676             case Phone.TYPE_OTHER_FAX: {
   1677                 parameterList.add(VCardConstants.PARAM_TYPE_FAX);
   1678                 break;
   1679             }
   1680             case Phone.TYPE_TELEX: {
   1681                 parameterList.add(VCardConstants.PARAM_TYPE_TLX);
   1682                 break;
   1683             }
   1684             case Phone.TYPE_WORK_MOBILE: {
   1685                 parameterList.addAll(
   1686                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
   1687                 break;
   1688             }
   1689             case Phone.TYPE_WORK_PAGER: {
   1690                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1691                 // See above.
   1692                 if (mIsDoCoMo) {
   1693                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1694                 } else {
   1695                     parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
   1696                 }
   1697                 break;
   1698             }
   1699             case Phone.TYPE_MMS: {
   1700                 parameterList.add(VCardConstants.PARAM_TYPE_MSG);
   1701                 break;
   1702             }
   1703             case Phone.TYPE_CUSTOM: {
   1704                 if (TextUtils.isEmpty(label)) {
   1705                     // Just ignore the custom type.
   1706                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1707                 } else if (VCardUtils.isMobilePhoneLabel(label)) {
   1708                     parameterList.add(VCardConstants.PARAM_TYPE_CELL);
   1709                 } else if (mIsV30OrV40) {
   1710                     // This label is appropriately encoded in appendTypeParameters.
   1711                     parameterList.add(label);
   1712                 } else {
   1713                     final String upperLabel = label.toUpperCase();
   1714                     if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
   1715                         parameterList.add(upperLabel);
   1716                     } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1717                         // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
   1718                         //       "TYPE=" string.
   1719                         parameterList.add("X-" + label);
   1720                     }
   1721                 }
   1722                 break;
   1723             }
   1724             case Phone.TYPE_RADIO:
   1725             case Phone.TYPE_TTY_TDD:
   1726             default: {
   1727                 break;
   1728             }
   1729         }
   1730 
   1731         if (isPrimary) {
   1732             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1733         }
   1734 
   1735         if (parameterList.isEmpty()) {
   1736             appendUncommonPhoneType(mBuilder, type);
   1737         } else {
   1738             appendTypeParameters(parameterList);
   1739         }
   1740 
   1741         mBuilder.append(VCARD_DATA_SEPARATOR);
   1742         mBuilder.append(encodedValue);
   1743         mBuilder.append(VCARD_END_OF_LINE);
   1744     }
   1745 
   1746     /**
   1747      * Appends phone type string which may not be available in some devices.
   1748      */
   1749     private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
   1750         if (mIsDoCoMo) {
   1751             // The previous implementation for DoCoMo had been conservative
   1752             // about miscellaneous types.
   1753             builder.append(VCardConstants.PARAM_TYPE_VOICE);
   1754         } else {
   1755             String phoneType = VCardUtils.getPhoneTypeString(type);
   1756             if (phoneType != null) {
   1757                 appendTypeParameter(phoneType);
   1758             } else {
   1759                 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
   1760             }
   1761         }
   1762     }
   1763 
   1764     /**
   1765      * @param encodedValue Must be encoded by BASE64
   1766      * @param photoType
   1767      */
   1768     public void appendPhotoLine(final String encodedValue, final String photoType) {
   1769         StringBuilder tmpBuilder = new StringBuilder();
   1770         tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
   1771         tmpBuilder.append(VCARD_PARAM_SEPARATOR);
   1772         if (mIsV30OrV40) {
   1773             tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
   1774         } else {
   1775             tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
   1776         }
   1777         tmpBuilder.append(VCARD_PARAM_SEPARATOR);
   1778         appendTypeParameter(tmpBuilder, photoType);
   1779         tmpBuilder.append(VCARD_DATA_SEPARATOR);
   1780         tmpBuilder.append(encodedValue);
   1781 
   1782         final String tmpStr = tmpBuilder.toString();
   1783         tmpBuilder = new StringBuilder();
   1784         int lineCount = 0;
   1785         final int length = tmpStr.length();
   1786         final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
   1787                 - VCARD_END_OF_LINE.length();
   1788         final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
   1789         int maxNum = maxNumForFirstLine;
   1790         for (int i = 0; i < length; i++) {
   1791             tmpBuilder.append(tmpStr.charAt(i));
   1792             lineCount++;
   1793             if (lineCount > maxNum) {
   1794                 tmpBuilder.append(VCARD_END_OF_LINE);
   1795                 tmpBuilder.append(VCARD_WS);
   1796                 maxNum = maxNumInGeneral;
   1797                 lineCount = 0;
   1798             }
   1799         }
   1800         mBuilder.append(tmpBuilder.toString());
   1801         mBuilder.append(VCARD_END_OF_LINE);
   1802         mBuilder.append(VCARD_END_OF_LINE);
   1803     }
   1804 
   1805     /**
   1806      * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
   1807      * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
   1808      * instead of "IMPP;sip:...".
   1809      *
   1810      * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
   1811      */
   1812     public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
   1813         final boolean useXProperty;
   1814         if (mIsV30OrV40) {
   1815             useXProperty = false;
   1816         } else if (mUsesDefactProperty){
   1817             useXProperty = true;
   1818         } else {
   1819             return this;
   1820         }
   1821 
   1822         if (contentValuesList != null) {
   1823             for (ContentValues contentValues : contentValuesList) {
   1824                 String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
   1825                 if (TextUtils.isEmpty(sipAddress)) {
   1826                     continue;
   1827                 }
   1828                 if (useXProperty) {
   1829                     // X-SIP does not contain "sip:" prefix.
   1830                     if (sipAddress.startsWith("sip:")) {
   1831                         if (sipAddress.length() == 4) {
   1832                             continue;
   1833                         }
   1834                         sipAddress = sipAddress.substring(4);
   1835                     }
   1836                     // No type is available yet.
   1837                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
   1838                 } else {
   1839                     if (!sipAddress.startsWith("sip:")) {
   1840                         sipAddress = "sip:" + sipAddress;
   1841                     }
   1842                     final String propertyName;
   1843                     if (VCardConfig.isVersion40(mVCardType)) {
   1844                         // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
   1845                         // TEL seems appropriate but may change in the future.
   1846                         propertyName = VCardConstants.PROPERTY_TEL;
   1847                     } else {
   1848                         // RFC 4770 (for vCard 3.0)
   1849                         propertyName = VCardConstants.PROPERTY_IMPP;
   1850                     }
   1851                     appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
   1852                 }
   1853             }
   1854         }
   1855         return this;
   1856     }
   1857 
   1858     public void appendAndroidSpecificProperty(
   1859             final String mimeType, ContentValues contentValues) {
   1860         if (!sAllowedAndroidPropertySet.contains(mimeType)) {
   1861             return;
   1862         }
   1863         final List<String> rawValueList = new ArrayList<String>();
   1864         for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
   1865             String value = contentValues.getAsString("data" + i);
   1866             if (value == null) {
   1867                 value = "";
   1868             }
   1869             rawValueList.add(value);
   1870         }
   1871 
   1872         boolean needCharset =
   1873             (mShouldAppendCharsetParam &&
   1874                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1875         boolean reallyUseQuotedPrintable =
   1876             (mShouldUseQuotedPrintable &&
   1877                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1878         mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
   1879         if (needCharset) {
   1880             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1881             mBuilder.append(mVCardCharsetParameter);
   1882         }
   1883         if (reallyUseQuotedPrintable) {
   1884             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1885             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1886         }
   1887         mBuilder.append(VCARD_DATA_SEPARATOR);
   1888         mBuilder.append(mimeType);  // Should not be encoded.
   1889         for (String rawValue : rawValueList) {
   1890             final String encodedValue;
   1891             if (reallyUseQuotedPrintable) {
   1892                 encodedValue = encodeQuotedPrintable(rawValue);
   1893             } else {
   1894                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
   1895                 //        (which says "When generating a content line, lines longer than
   1896                 //        75 characters SHOULD be folded"), though several
   1897                 //        (even well-known) applications do not care this.
   1898                 encodedValue = escapeCharacters(rawValue);
   1899             }
   1900             mBuilder.append(VCARD_ITEM_SEPARATOR);
   1901             mBuilder.append(encodedValue);
   1902         }
   1903         mBuilder.append(VCARD_END_OF_LINE);
   1904     }
   1905 
   1906     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1907             final String rawValue) {
   1908         appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
   1909     }
   1910 
   1911     public void appendLineWithCharsetAndQPDetection(
   1912             final String propertyName, final List<String> rawValueList) {
   1913         appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
   1914     }
   1915 
   1916     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1917             final List<String> parameterList, final String rawValue) {
   1918         final boolean needCharset =
   1919                 !VCardUtils.containsOnlyPrintableAscii(rawValue);
   1920         final boolean reallyUseQuotedPrintable =
   1921                 (mShouldUseQuotedPrintable &&
   1922                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
   1923         appendLine(propertyName, parameterList,
   1924                 rawValue, needCharset, reallyUseQuotedPrintable);
   1925     }
   1926 
   1927     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1928             final List<String> parameterList, final List<String> rawValueList) {
   1929         boolean needCharset =
   1930             (mShouldAppendCharsetParam &&
   1931                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1932         boolean reallyUseQuotedPrintable =
   1933             (mShouldUseQuotedPrintable &&
   1934                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1935         appendLine(propertyName, parameterList, rawValueList,
   1936                 needCharset, reallyUseQuotedPrintable);
   1937     }
   1938 
   1939     /**
   1940      * Appends one line with a given property name and value.
   1941      */
   1942     public void appendLine(final String propertyName, final String rawValue) {
   1943         appendLine(propertyName, rawValue, false, false);
   1944     }
   1945 
   1946     public void appendLine(final String propertyName, final List<String> rawValueList) {
   1947         appendLine(propertyName, rawValueList, false, false);
   1948     }
   1949 
   1950     public void appendLine(final String propertyName,
   1951             final String rawValue, final boolean needCharset,
   1952             boolean reallyUseQuotedPrintable) {
   1953         appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
   1954     }
   1955 
   1956     public void appendLine(final String propertyName, final List<String> parameterList,
   1957             final String rawValue) {
   1958         appendLine(propertyName, parameterList, rawValue, false, false);
   1959     }
   1960 
   1961     public void appendLine(final String propertyName, final List<String> parameterList,
   1962             final String rawValue, final boolean needCharset,
   1963             boolean reallyUseQuotedPrintable) {
   1964         mBuilder.append(propertyName);
   1965         if (parameterList != null && parameterList.size() > 0) {
   1966             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1967             appendTypeParameters(parameterList);
   1968         }
   1969         if (needCharset) {
   1970             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1971             mBuilder.append(mVCardCharsetParameter);
   1972         }
   1973 
   1974         final String encodedValue;
   1975         if (reallyUseQuotedPrintable) {
   1976             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1977             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1978             encodedValue = encodeQuotedPrintable(rawValue);
   1979         } else {
   1980             // TODO: one line may be too huge, which may be invalid in vCard spec, though
   1981             //       several (even well-known) applications do not care that violation.
   1982             encodedValue = escapeCharacters(rawValue);
   1983         }
   1984 
   1985         mBuilder.append(VCARD_DATA_SEPARATOR);
   1986         mBuilder.append(encodedValue);
   1987         mBuilder.append(VCARD_END_OF_LINE);
   1988     }
   1989 
   1990     public void appendLine(final String propertyName, final List<String> rawValueList,
   1991             final boolean needCharset, boolean needQuotedPrintable) {
   1992         appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
   1993     }
   1994 
   1995     public void appendLine(final String propertyName, final List<String> parameterList,
   1996             final List<String> rawValueList, final boolean needCharset,
   1997             final boolean needQuotedPrintable) {
   1998         mBuilder.append(propertyName);
   1999         if (parameterList != null && parameterList.size() > 0) {
   2000             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2001             appendTypeParameters(parameterList);
   2002         }
   2003         if (needCharset) {
   2004             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2005             mBuilder.append(mVCardCharsetParameter);
   2006         }
   2007         if (needQuotedPrintable) {
   2008             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2009             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   2010         }
   2011 
   2012         mBuilder.append(VCARD_DATA_SEPARATOR);
   2013         boolean first = true;
   2014         for (String rawValue : rawValueList) {
   2015             final String encodedValue;
   2016             if (needQuotedPrintable) {
   2017                 encodedValue = encodeQuotedPrintable(rawValue);
   2018             } else {
   2019                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
   2020                 //        (which says "When generating a content line, lines longer than
   2021                 //        75 characters SHOULD be folded"), though several
   2022                 //        (even well-known) applications do not care this.
   2023                 encodedValue = escapeCharacters(rawValue);
   2024             }
   2025 
   2026             if (first) {
   2027                 first = false;
   2028             } else {
   2029                 mBuilder.append(VCARD_ITEM_SEPARATOR);
   2030             }
   2031             mBuilder.append(encodedValue);
   2032         }
   2033         mBuilder.append(VCARD_END_OF_LINE);
   2034     }
   2035 
   2036     /**
   2037      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
   2038      */
   2039     private void appendTypeParameters(final List<String> types) {
   2040         // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
   2041         // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
   2042         boolean first = true;
   2043         for (final String typeValue : types) {
   2044             if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
   2045                 final String encoded = (VCardConfig.isVersion40(mVCardType) ?
   2046                         VCardUtils.toStringAsV40ParamValue(typeValue) :
   2047                         VCardUtils.toStringAsV30ParamValue(typeValue));
   2048                 if (TextUtils.isEmpty(encoded)) {
   2049                     continue;
   2050                 }
   2051 
   2052                 if (first) {
   2053                     first = false;
   2054                 } else {
   2055                     mBuilder.append(VCARD_PARAM_SEPARATOR);
   2056                 }
   2057                 appendTypeParameter(encoded);
   2058             } else {  // vCard 2.1
   2059                 if (!VCardUtils.isV21Word(typeValue)) {
   2060                     continue;
   2061                 }
   2062                 if (first) {
   2063                     first = false;
   2064                 } else {
   2065                     mBuilder.append(VCARD_PARAM_SEPARATOR);
   2066                 }
   2067                 appendTypeParameter(typeValue);
   2068             }
   2069         }
   2070     }
   2071 
   2072     /**
   2073      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
   2074      */
   2075     private void appendTypeParameter(final String type) {
   2076         appendTypeParameter(mBuilder, type);
   2077     }
   2078 
   2079     private void appendTypeParameter(final StringBuilder builder, final String type) {
   2080         // Refrain from using appendType() so that "TYPE=" is not be appended when the
   2081         // device is DoCoMo's (just for safety).
   2082         //
   2083         // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
   2084         if (VCardConfig.isVersion40(mVCardType) ||
   2085                 ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
   2086             builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
   2087         }
   2088         builder.append(type);
   2089     }
   2090 
   2091     /**
   2092      * Returns true when the property line should contain charset parameter
   2093      * information. This method may return true even when vCard version is 3.0.
   2094      *
   2095      * Strictly, adding charset information is invalid in VCard 3.0.
   2096      * However we'll add the info only when charset we use is not UTF-8
   2097      * in vCard 3.0 format, since parser side may be able to use the charset
   2098      * via this field, though we may encounter another problem by adding it.
   2099      *
   2100      * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
   2101      * recommends UTF-8. By adding this field, parsers may be able
   2102      * to know this text is NOT UTF-8 but Shift_Jis.
   2103      */
   2104     private boolean shouldAppendCharsetParam(String...propertyValueList) {
   2105         if (!mShouldAppendCharsetParam) {
   2106             return false;
   2107         }
   2108         for (String propertyValue : propertyValueList) {
   2109             if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
   2110                 return true;
   2111             }
   2112         }
   2113         return false;
   2114     }
   2115 
   2116     private String encodeQuotedPrintable(final String str) {
   2117         if (TextUtils.isEmpty(str)) {
   2118             return "";
   2119         }
   2120 
   2121         final StringBuilder builder = new StringBuilder();
   2122         int index = 0;
   2123         int lineCount = 0;
   2124         byte[] strArray = null;
   2125 
   2126         try {
   2127             strArray = str.getBytes(mCharset);
   2128         } catch (UnsupportedEncodingException e) {
   2129             Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
   2130                     + "Try default charset");
   2131             strArray = str.getBytes();
   2132         }
   2133         while (index < strArray.length) {
   2134             builder.append(String.format("=%02X", strArray[index]));
   2135             index += 1;
   2136             lineCount += 3;
   2137 
   2138             if (lineCount >= 67) {
   2139                 // Specification requires CRLF must be inserted before the
   2140                 // length of the line
   2141                 // becomes more than 76.
   2142                 // Assuming that the next character is a multi-byte character,
   2143                 // it will become
   2144                 // 6 bytes.
   2145                 // 76 - 6 - 3 = 67
   2146                 builder.append("=\r\n");
   2147                 lineCount = 0;
   2148             }
   2149         }
   2150 
   2151         return builder.toString();
   2152     }
   2153 
   2154     /**
   2155      * Append '\' to the characters which should be escaped. The character set is different
   2156      * not only between vCard 2.1 and vCard 3.0 but also among each device.
   2157      *
   2158      * Note that Quoted-Printable string must not be input here.
   2159      */
   2160     @SuppressWarnings("fallthrough")
   2161     private String escapeCharacters(final String unescaped) {
   2162         if (TextUtils.isEmpty(unescaped)) {
   2163             return "";
   2164         }
   2165 
   2166         final StringBuilder tmpBuilder = new StringBuilder();
   2167         final int length = unescaped.length();
   2168         for (int i = 0; i < length; i++) {
   2169             final char ch = unescaped.charAt(i);
   2170             switch (ch) {
   2171                 case ';': {
   2172                     tmpBuilder.append('\\');
   2173                     tmpBuilder.append(';');
   2174                     break;
   2175                 }
   2176                 case '\r': {
   2177                     if (i + 1 < length) {
   2178                         char nextChar = unescaped.charAt(i);
   2179                         if (nextChar == '\n') {
   2180                             break;
   2181                         } else {
   2182                             // fall through
   2183                         }
   2184                     } else {
   2185                         // fall through
   2186                     }
   2187                 }
   2188                 case '\n': {
   2189                     // In vCard 2.1, there's no specification about this, while
   2190                     // vCard 3.0 explicitly requires this should be encoded to "\n".
   2191                     tmpBuilder.append("\\n");
   2192                     break;
   2193                 }
   2194                 case '\\': {
   2195                     if (mIsV30OrV40) {
   2196                         tmpBuilder.append("\\\\");
   2197                         break;
   2198                     } else {
   2199                         // fall through
   2200                     }
   2201                 }
   2202                 case '<':
   2203                 case '>': {
   2204                     if (mIsDoCoMo) {
   2205                         tmpBuilder.append('\\');
   2206                         tmpBuilder.append(ch);
   2207                     } else {
   2208                         tmpBuilder.append(ch);
   2209                     }
   2210                     break;
   2211                 }
   2212                 case ',': {
   2213                     if (mIsV30OrV40) {
   2214                         tmpBuilder.append("\\,");
   2215                     } else {
   2216                         tmpBuilder.append(ch);
   2217                     }
   2218                     break;
   2219                 }
   2220                 default: {
   2221                     tmpBuilder.append(ch);
   2222                     break;
   2223                 }
   2224             }
   2225         }
   2226         return tmpBuilder.toString();
   2227     }
   2228 
   2229     @Override
   2230     public String toString() {
   2231         if (!mEndAppended) {
   2232             if (mIsDoCoMo) {
   2233                 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
   2234                 appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
   2235                 appendLine(VCardConstants.PROPERTY_X_NO, "");
   2236                 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
   2237             }
   2238             appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
   2239             mEndAppended = true;
   2240         }
   2241         return mBuilder.toString();
   2242     }
   2243 }
   2244