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 com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
     19 
     20 import android.content.ContentValues;
     21 import android.provider.ContactsContract.CommonDataKinds.Email;
     22 import android.provider.ContactsContract.CommonDataKinds.Event;
     23 import android.provider.ContactsContract.CommonDataKinds.Im;
     24 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     25 import android.provider.ContactsContract.CommonDataKinds.Note;
     26 import android.provider.ContactsContract.CommonDataKinds.Organization;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.provider.ContactsContract.CommonDataKinds.Photo;
     29 import android.provider.ContactsContract.CommonDataKinds.Relation;
     30 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     31 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     32 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     33 import android.provider.ContactsContract.CommonDataKinds.Website;
     34 import android.telephony.PhoneNumberUtils;
     35 import android.text.TextUtils;
     36 import android.util.Base64;
     37 import android.util.Log;
     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     private 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             final boolean reallyUseQuotedPrintableToDisplayName =
    539                 (!mRefrainsQPToNameProperties &&
    540                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
    541             final String encodedDisplayName =
    542                     reallyUseQuotedPrintableToDisplayName ?
    543                             encodeQuotedPrintable(displayName) :
    544                                 escapeCharacters(displayName);
    545 
    546             // N
    547             mBuilder.append(VCardConstants.PROPERTY_N);
    548             if (shouldAppendCharsetParam(displayName)) {
    549                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    550                 mBuilder.append(mVCardCharsetParameter);
    551             }
    552             if (reallyUseQuotedPrintableToDisplayName) {
    553                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    554                 mBuilder.append(VCARD_PARAM_ENCODING_QP);
    555             }
    556             mBuilder.append(VCARD_DATA_SEPARATOR);
    557             mBuilder.append(encodedDisplayName);
    558             mBuilder.append(VCARD_ITEM_SEPARATOR);
    559             mBuilder.append(VCARD_ITEM_SEPARATOR);
    560             mBuilder.append(VCARD_ITEM_SEPARATOR);
    561             mBuilder.append(VCARD_ITEM_SEPARATOR);
    562             mBuilder.append(VCARD_END_OF_LINE);
    563 
    564             // FN
    565             mBuilder.append(VCardConstants.PROPERTY_FN);
    566 
    567             // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
    568             //       when it would be useful or necessary for external importers,
    569             //       assuming the external importer allows this vioration of the spec.
    570             if (shouldAppendCharsetParam(displayName)) {
    571                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    572                 mBuilder.append(mVCardCharsetParameter);
    573             }
    574             mBuilder.append(VCARD_DATA_SEPARATOR);
    575             mBuilder.append(encodedDisplayName);
    576             mBuilder.append(VCARD_END_OF_LINE);
    577         } else if (VCardConfig.isVersion30(mVCardType)) {
    578             appendLine(VCardConstants.PROPERTY_N, "");
    579             appendLine(VCardConstants.PROPERTY_FN, "");
    580         } else if (mIsDoCoMo) {
    581             appendLine(VCardConstants.PROPERTY_N, "");
    582         }
    583 
    584         appendPhoneticNameFields(contentValues);
    585         return this;
    586     }
    587 
    588     /**
    589      * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY.
    590      */
    591     private void appendPhoneticNameFields(final ContentValues contentValues) {
    592         final String phoneticFamilyName;
    593         final String phoneticMiddleName;
    594         final String phoneticGivenName;
    595         {
    596             final String tmpPhoneticFamilyName =
    597                 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
    598             final String tmpPhoneticMiddleName =
    599                 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
    600             final String tmpPhoneticGivenName =
    601                 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
    602             if (mNeedsToConvertPhoneticString) {
    603                 phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
    604                 phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
    605                 phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
    606             } else {
    607                 phoneticFamilyName = tmpPhoneticFamilyName;
    608                 phoneticMiddleName = tmpPhoneticMiddleName;
    609                 phoneticGivenName = tmpPhoneticGivenName;
    610             }
    611         }
    612 
    613         if (TextUtils.isEmpty(phoneticFamilyName)
    614                 && TextUtils.isEmpty(phoneticMiddleName)
    615                 && TextUtils.isEmpty(phoneticGivenName)) {
    616             if (mIsDoCoMo) {
    617                 mBuilder.append(VCardConstants.PROPERTY_SOUND);
    618                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    619                 mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
    620                 mBuilder.append(VCARD_DATA_SEPARATOR);
    621                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    622                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    623                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    624                 mBuilder.append(VCARD_ITEM_SEPARATOR);
    625                 mBuilder.append(VCARD_END_OF_LINE);
    626             }
    627             return;
    628         }
    629 
    630         if (VCardConfig.isVersion40(mVCardType)) {
    631             // We don't want SORT-STRING anyway.
    632         } else if (VCardConfig.isVersion30(mVCardType)) {
    633             final String sortString =
    634                     VCardUtils.constructNameFromElements(mVCardType,
    635                             phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
    636             mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
    637             if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) {
    638                 // vCard 3.0 does not force us to use UTF-8 and actually we see some
    639                 // programs which emit this value. It is incorrect from the view of
    640                 // specification, but actually necessary for parsing vCard with non-UTF-8
    641                 // charsets, expecting other parsers not get confused with this value.
    642                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    643                 mBuilder.append(mVCardCharsetParameter);
    644             }
    645             mBuilder.append(VCARD_DATA_SEPARATOR);
    646             mBuilder.append(escapeCharacters(sortString));
    647             mBuilder.append(VCARD_END_OF_LINE);
    648         } else if (mIsJapaneseMobilePhone) {
    649             // Note: There is no appropriate property for expressing
    650             //       phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
    651             //       vCard 3.0 (SORT-STRING).
    652             //       We use DoCoMo's way when the device is Japanese one since it is already
    653             //       supported by a lot of Japanese mobile phones.
    654             //       This is "X-" property, so any parser hopefully would not get
    655             //       confused with this.
    656             //
    657             //       Also, DoCoMo's specification requires vCard composer to use just the first
    658             //       column.
    659             //       i.e.
    660             //       good:  SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
    661             //       bad :  SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
    662             mBuilder.append(VCardConstants.PROPERTY_SOUND);
    663             mBuilder.append(VCARD_PARAM_SEPARATOR);
    664             mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
    665 
    666             boolean reallyUseQuotedPrintable =
    667                 (!mRefrainsQPToNameProperties
    668                         && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
    669                                 phoneticFamilyName)
    670                                 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
    671                                         phoneticMiddleName)
    672                                 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
    673                                         phoneticGivenName)));
    674 
    675             final String encodedPhoneticFamilyName;
    676             final String encodedPhoneticMiddleName;
    677             final String encodedPhoneticGivenName;
    678             if (reallyUseQuotedPrintable) {
    679                 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
    680                 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
    681                 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
    682             } else {
    683                 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
    684                 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
    685                 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
    686             }
    687 
    688             if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
    689                     encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
    690                 mBuilder.append(VCARD_PARAM_SEPARATOR);
    691                 mBuilder.append(mVCardCharsetParameter);
    692             }
    693             mBuilder.append(VCARD_DATA_SEPARATOR);
    694             {
    695                 boolean first = true;
    696                 if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
    697                     mBuilder.append(encodedPhoneticFamilyName);
    698                     first = false;
    699                 }
    700                 if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
    701                     if (first) {
    702                         first = false;
    703                     } else {
    704                         mBuilder.append(' ');
    705                     }
    706                     mBuilder.append(encodedPhoneticMiddleName);
    707                 }
    708                 if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
    709                     if (!first) {
    710                         mBuilder.append(' ');
    711                     }
    712                     mBuilder.append(encodedPhoneticGivenName);
    713                 }
    714             }
    715             mBuilder.append(VCARD_ITEM_SEPARATOR);  // family;given
    716             mBuilder.append(VCARD_ITEM_SEPARATOR);  // given;middle
    717             mBuilder.append(VCARD_ITEM_SEPARATOR);  // middle;prefix
    718             mBuilder.append(VCARD_ITEM_SEPARATOR);  // prefix;suffix
    719             mBuilder.append(VCARD_END_OF_LINE);
    720         }
    721 
    722         if (mUsesDefactProperty) {
    723             if (!TextUtils.isEmpty(phoneticGivenName)) {
    724                 final boolean reallyUseQuotedPrintable =
    725                     (mShouldUseQuotedPrintable &&
    726                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
    727                 final String encodedPhoneticGivenName;
    728                 if (reallyUseQuotedPrintable) {
    729                     encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
    730                 } else {
    731                     encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
    732                 }
    733                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
    734                 if (shouldAppendCharsetParam(phoneticGivenName)) {
    735                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    736                     mBuilder.append(mVCardCharsetParameter);
    737                 }
    738                 if (reallyUseQuotedPrintable) {
    739                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    740                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    741                 }
    742                 mBuilder.append(VCARD_DATA_SEPARATOR);
    743                 mBuilder.append(encodedPhoneticGivenName);
    744                 mBuilder.append(VCARD_END_OF_LINE);
    745             }  // if (!TextUtils.isEmpty(phoneticGivenName))
    746             if (!TextUtils.isEmpty(phoneticMiddleName)) {
    747                 final boolean reallyUseQuotedPrintable =
    748                     (mShouldUseQuotedPrintable &&
    749                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
    750                 final String encodedPhoneticMiddleName;
    751                 if (reallyUseQuotedPrintable) {
    752                     encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
    753                 } else {
    754                     encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
    755                 }
    756                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
    757                 if (shouldAppendCharsetParam(phoneticMiddleName)) {
    758                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    759                     mBuilder.append(mVCardCharsetParameter);
    760                 }
    761                 if (reallyUseQuotedPrintable) {
    762                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    763                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    764                 }
    765                 mBuilder.append(VCARD_DATA_SEPARATOR);
    766                 mBuilder.append(encodedPhoneticMiddleName);
    767                 mBuilder.append(VCARD_END_OF_LINE);
    768             }  // if (!TextUtils.isEmpty(phoneticGivenName))
    769             if (!TextUtils.isEmpty(phoneticFamilyName)) {
    770                 final boolean reallyUseQuotedPrintable =
    771                     (mShouldUseQuotedPrintable &&
    772                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
    773                 final String encodedPhoneticFamilyName;
    774                 if (reallyUseQuotedPrintable) {
    775                     encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
    776                 } else {
    777                     encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
    778                 }
    779                 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
    780                 if (shouldAppendCharsetParam(phoneticFamilyName)) {
    781                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    782                     mBuilder.append(mVCardCharsetParameter);
    783                 }
    784                 if (reallyUseQuotedPrintable) {
    785                     mBuilder.append(VCARD_PARAM_SEPARATOR);
    786                     mBuilder.append(VCARD_PARAM_ENCODING_QP);
    787                 }
    788                 mBuilder.append(VCARD_DATA_SEPARATOR);
    789                 mBuilder.append(encodedPhoneticFamilyName);
    790                 mBuilder.append(VCARD_END_OF_LINE);
    791             }  // if (!TextUtils.isEmpty(phoneticFamilyName))
    792         }
    793     }
    794 
    795     public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
    796         final boolean useAndroidProperty;
    797         if (mIsV30OrV40) {   // These specifications have NICKNAME property.
    798             useAndroidProperty = false;
    799         } else if (mUsesAndroidProperty) {
    800             useAndroidProperty = true;
    801         } else {
    802             // There's no way to add this field.
    803             return this;
    804         }
    805         if (contentValuesList != null) {
    806             for (ContentValues contentValues : contentValuesList) {
    807                 final String nickname = contentValues.getAsString(Nickname.NAME);
    808                 if (TextUtils.isEmpty(nickname)) {
    809                     continue;
    810                 }
    811                 if (useAndroidProperty) {
    812                     appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
    813                 } else {
    814                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
    815                 }
    816             }
    817         }
    818         return this;
    819     }
    820 
    821     public VCardBuilder appendPhones(final List<ContentValues> contentValuesList,
    822             VCardPhoneNumberTranslationCallback translationCallback) {
    823         boolean phoneLineExists = false;
    824         if (contentValuesList != null) {
    825             Set<String> phoneSet = new HashSet<String>();
    826             for (ContentValues contentValues : contentValuesList) {
    827                 final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
    828                 final String label = contentValues.getAsString(Phone.LABEL);
    829                 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
    830                 final boolean isPrimary = (isPrimaryAsInteger != null ?
    831                         (isPrimaryAsInteger > 0) : false);
    832                 String phoneNumber = contentValues.getAsString(Phone.NUMBER);
    833                 if (phoneNumber != null) {
    834                     phoneNumber = phoneNumber.trim();
    835                 }
    836                 if (TextUtils.isEmpty(phoneNumber)) {
    837                     continue;
    838                 }
    839 
    840                 final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
    841                 // Note: We prioritize this callback over FLAG_REFRAIN_PHONE_NUMBER_FORMATTING
    842                 // intentionally. In the future the flag will be replaced by callback
    843                 // mechanism entirely.
    844                 if (translationCallback != null) {
    845                     phoneNumber = translationCallback.onValueReceived(
    846                             phoneNumber, type, label, isPrimary);
    847                     if (!phoneSet.contains(phoneNumber)) {
    848                         phoneSet.add(phoneNumber);
    849                         appendTelLine(type, label, phoneNumber, isPrimary);
    850                     }
    851                 } else if (type == Phone.TYPE_PAGER ||
    852                         VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
    853                     // Note: PAGER number needs unformatted "phone number".
    854                     phoneLineExists = true;
    855                     if (!phoneSet.contains(phoneNumber)) {
    856                         phoneSet.add(phoneNumber);
    857                         appendTelLine(type, label, phoneNumber, isPrimary);
    858                     }
    859                 } else {
    860                     final List<String> phoneNumberList = splitPhoneNumbers(phoneNumber);
    861                     if (phoneNumberList.isEmpty()) {
    862                         continue;
    863                     }
    864                     phoneLineExists = true;
    865                     for (String actualPhoneNumber : phoneNumberList) {
    866                         if (!phoneSet.contains(actualPhoneNumber)) {
    867                             // 'p' and 'w' are the standard characters for pause and wait
    868                             // (see RFC 3601)
    869                             // so use those when exporting phone numbers via vCard.
    870                             String numberWithControlSequence = actualPhoneNumber
    871                                     .replace(PhoneNumberUtils.PAUSE, 'p')
    872                                     .replace(PhoneNumberUtils.WAIT, 'w');
    873                             String formatted;
    874                             // TODO: remove this code and relevant test cases. vCard and any other
    875                             // codes using it shouldn't rely on the formatter here.
    876                             if (TextUtils.equals(numberWithControlSequence, actualPhoneNumber)) {
    877                                 StringBuilder digitsOnlyBuilder = new StringBuilder();
    878                                 final int length = actualPhoneNumber.length();
    879                                 for (int i = 0; i < length; i++) {
    880                                     final char ch = actualPhoneNumber.charAt(i);
    881                                     if (Character.isDigit(ch) || ch == '+') {
    882                                         digitsOnlyBuilder.append(ch);
    883                                     }
    884                                 }
    885                                 final int phoneFormat =
    886                                         VCardUtils.getPhoneNumberFormat(mVCardType);
    887                                 formatted = PhoneNumberUtilsPort.formatNumber(
    888                                         digitsOnlyBuilder.toString(), phoneFormat);
    889                             } else {
    890                                 // Be conservative.
    891                                 formatted = numberWithControlSequence;
    892                             }
    893 
    894                             // In vCard 4.0, value type must be "a single URI value",
    895                             // not just a phone number. (Based on vCard 4.0 rev.13)
    896                             if (VCardConfig.isVersion40(mVCardType)
    897                                     && !TextUtils.isEmpty(formatted)
    898                                     && !formatted.startsWith("tel:")) {
    899                                 formatted = "tel:" + formatted;
    900                             }
    901 
    902                             // Pre-formatted string should be stored.
    903                             phoneSet.add(actualPhoneNumber);
    904                             appendTelLine(type, label, formatted, isPrimary);
    905                         }
    906                     }  // for (String actualPhoneNumber : phoneNumberList) {
    907 
    908                     // TODO: TEL with SIP URI?
    909                 }
    910             }
    911         }
    912 
    913         if (!phoneLineExists && mIsDoCoMo) {
    914             appendTelLine(Phone.TYPE_HOME, "", "", false);
    915         }
    916 
    917         return this;
    918     }
    919 
    920     /**
    921      * <p>
    922      * Splits a given string expressing phone numbers into several strings, and remove
    923      * unnecessary characters inside them. The size of a returned list becomes 1 when
    924      * no split is needed.
    925      * </p>
    926      * <p>
    927      * The given number "may" have several phone numbers when the contact entry is corrupted
    928      * because of its original source.
    929      * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
    930      * </p>
    931      * <p>
    932      * This kind of "phone numbers" will not be created with Android vCard implementation,
    933      * but we may encounter them if the source of the input data has already corrupted
    934      * implementation.
    935      * </p>
    936      * <p>
    937      * To handle this case, this method first splits its input into multiple parts
    938      * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
    939      * removes unnecessary strings like "(Miami)".
    940      * </p>
    941      * <p>
    942      * Do not call this method when trimming is inappropriate for its receivers.
    943      * </p>
    944      */
    945     private List<String> splitPhoneNumbers(final String phoneNumber) {
    946         final List<String> phoneList = new ArrayList<String>();
    947 
    948         StringBuilder builder = new StringBuilder();
    949         final int length = phoneNumber.length();
    950         for (int i = 0; i < length; i++) {
    951             final char ch = phoneNumber.charAt(i);
    952             if (ch == '\n' && builder.length() > 0) {
    953                 phoneList.add(builder.toString());
    954                 builder = new StringBuilder();
    955             } else {
    956                 builder.append(ch);
    957             }
    958         }
    959         if (builder.length() > 0) {
    960             phoneList.add(builder.toString());
    961         }
    962         return phoneList;
    963     }
    964 
    965     public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
    966         boolean emailAddressExists = false;
    967         if (contentValuesList != null) {
    968             final Set<String> addressSet = new HashSet<String>();
    969             for (ContentValues contentValues : contentValuesList) {
    970                 String emailAddress = contentValues.getAsString(Email.DATA);
    971                 if (emailAddress != null) {
    972                     emailAddress = emailAddress.trim();
    973                 }
    974                 if (TextUtils.isEmpty(emailAddress)) {
    975                     continue;
    976                 }
    977                 Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
    978                 final int type = (typeAsObject != null ?
    979                         typeAsObject : DEFAULT_EMAIL_TYPE);
    980                 final String label = contentValues.getAsString(Email.LABEL);
    981                 Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
    982                 final boolean isPrimary = (isPrimaryAsInteger != null ?
    983                         (isPrimaryAsInteger > 0) : false);
    984                 emailAddressExists = true;
    985                 if (!addressSet.contains(emailAddress)) {
    986                     addressSet.add(emailAddress);
    987                     appendEmailLine(type, label, emailAddress, isPrimary);
    988                 }
    989             }
    990         }
    991 
    992         if (!emailAddressExists && mIsDoCoMo) {
    993             appendEmailLine(Email.TYPE_HOME, "", "", false);
    994         }
    995 
    996         return this;
    997     }
    998 
    999     public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
   1000         if (contentValuesList == null || contentValuesList.isEmpty()) {
   1001             if (mIsDoCoMo) {
   1002                 mBuilder.append(VCardConstants.PROPERTY_ADR);
   1003                 mBuilder.append(VCARD_PARAM_SEPARATOR);
   1004                 mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
   1005                 mBuilder.append(VCARD_DATA_SEPARATOR);
   1006                 mBuilder.append(VCARD_END_OF_LINE);
   1007             }
   1008         } else {
   1009             if (mIsDoCoMo) {
   1010                 appendPostalsForDoCoMo(contentValuesList);
   1011             } else {
   1012                 appendPostalsForGeneric(contentValuesList);
   1013             }
   1014         }
   1015 
   1016         return this;
   1017     }
   1018 
   1019     private static final Map<Integer, Integer> sPostalTypePriorityMap;
   1020 
   1021     static {
   1022         sPostalTypePriorityMap = new HashMap<Integer, Integer>();
   1023         sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
   1024         sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
   1025         sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
   1026         sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
   1027     }
   1028 
   1029     /**
   1030      * Tries to append just one line. If there's no appropriate address
   1031      * information, append an empty line.
   1032      */
   1033     private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
   1034         int currentPriority = Integer.MAX_VALUE;
   1035         int currentType = Integer.MAX_VALUE;
   1036         ContentValues currentContentValues = null;
   1037         for (final ContentValues contentValues : contentValuesList) {
   1038             if (contentValues == null) {
   1039                 continue;
   1040             }
   1041             final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
   1042             final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
   1043             final int priority =
   1044                     (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
   1045             if (priority < currentPriority) {
   1046                 currentPriority = priority;
   1047                 currentType = typeAsInteger;
   1048                 currentContentValues = contentValues;
   1049                 if (priority == 0) {
   1050                     break;
   1051                 }
   1052             }
   1053         }
   1054 
   1055         if (currentContentValues == null) {
   1056             Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
   1057             return;
   1058         }
   1059 
   1060         final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
   1061         appendPostalLine(currentType, label, currentContentValues, false, true);
   1062     }
   1063 
   1064     private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
   1065         for (final ContentValues contentValues : contentValuesList) {
   1066             if (contentValues == null) {
   1067                 continue;
   1068             }
   1069             final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
   1070             final int type = (typeAsInteger != null ?
   1071                     typeAsInteger : DEFAULT_POSTAL_TYPE);
   1072             final String label = contentValues.getAsString(StructuredPostal.LABEL);
   1073             final Integer isPrimaryAsInteger =
   1074                 contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
   1075             final boolean isPrimary = (isPrimaryAsInteger != null ?
   1076                     (isPrimaryAsInteger > 0) : false);
   1077             appendPostalLine(type, label, contentValues, isPrimary, false);
   1078         }
   1079     }
   1080 
   1081     private static class PostalStruct {
   1082         final boolean reallyUseQuotedPrintable;
   1083         final boolean appendCharset;
   1084         final String addressData;
   1085         public PostalStruct(final boolean reallyUseQuotedPrintable,
   1086                 final boolean appendCharset, final String addressData) {
   1087             this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
   1088             this.appendCharset = appendCharset;
   1089             this.addressData = addressData;
   1090         }
   1091     }
   1092 
   1093     /**
   1094      * @return null when there's no information available to construct the data.
   1095      */
   1096     private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
   1097         // adr-value    = 0*6(text-value ";") text-value
   1098         //              ; PO Box, Extended Address, Street, Locality, Region, Postal
   1099         //              ; Code, Country Name
   1100         final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
   1101         final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
   1102         final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
   1103         final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
   1104         final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
   1105         final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
   1106         final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
   1107         final String[] rawAddressArray = new String[]{
   1108                 rawPoBox, rawNeighborhood, rawStreet, rawLocality,
   1109                 rawRegion, rawPostalCode, rawCountry};
   1110         if (!VCardUtils.areAllEmpty(rawAddressArray)) {
   1111             final boolean reallyUseQuotedPrintable =
   1112                 (mShouldUseQuotedPrintable &&
   1113                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
   1114             final boolean appendCharset =
   1115                 !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
   1116             final String encodedPoBox;
   1117             final String encodedStreet;
   1118             final String encodedLocality;
   1119             final String encodedRegion;
   1120             final String encodedPostalCode;
   1121             final String encodedCountry;
   1122             final String encodedNeighborhood;
   1123 
   1124             final String rawLocality2;
   1125             // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
   1126             // but this is intentional.
   1127             //
   1128             // QP encoding may add line feeds when needed and the result of
   1129             // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
   1130             // may be different from
   1131             // - encodedLocality + " " + encodedNeighborhood.
   1132             //
   1133             // We use safer way.
   1134             if (TextUtils.isEmpty(rawLocality)) {
   1135                 if (TextUtils.isEmpty(rawNeighborhood)) {
   1136                     rawLocality2 = "";
   1137                 } else {
   1138                     rawLocality2 = rawNeighborhood;
   1139                 }
   1140             } else {
   1141                 if (TextUtils.isEmpty(rawNeighborhood)) {
   1142                     rawLocality2 = rawLocality;
   1143                 } else {
   1144                     rawLocality2 = rawLocality + " " + rawNeighborhood;
   1145                 }
   1146             }
   1147             if (reallyUseQuotedPrintable) {
   1148                 encodedPoBox = encodeQuotedPrintable(rawPoBox);
   1149                 encodedStreet = encodeQuotedPrintable(rawStreet);
   1150                 encodedLocality = encodeQuotedPrintable(rawLocality2);
   1151                 encodedRegion = encodeQuotedPrintable(rawRegion);
   1152                 encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
   1153                 encodedCountry = encodeQuotedPrintable(rawCountry);
   1154             } else {
   1155                 encodedPoBox = escapeCharacters(rawPoBox);
   1156                 encodedStreet = escapeCharacters(rawStreet);
   1157                 encodedLocality = escapeCharacters(rawLocality2);
   1158                 encodedRegion = escapeCharacters(rawRegion);
   1159                 encodedPostalCode = escapeCharacters(rawPostalCode);
   1160                 encodedCountry = escapeCharacters(rawCountry);
   1161                 encodedNeighborhood = escapeCharacters(rawNeighborhood);
   1162             }
   1163             final StringBuilder addressBuilder = new StringBuilder();
   1164             addressBuilder.append(encodedPoBox);
   1165             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
   1166             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
   1167             addressBuilder.append(encodedStreet);
   1168             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
   1169             addressBuilder.append(encodedLocality);
   1170             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
   1171             addressBuilder.append(encodedRegion);
   1172             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
   1173             addressBuilder.append(encodedPostalCode);
   1174             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
   1175             addressBuilder.append(encodedCountry);
   1176             return new PostalStruct(
   1177                     reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
   1178         } else {  // VCardUtils.areAllEmpty(rawAddressArray) == true
   1179             // Try to use FORMATTED_ADDRESS instead.
   1180             final String rawFormattedAddress =
   1181                 contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
   1182             if (TextUtils.isEmpty(rawFormattedAddress)) {
   1183                 return null;
   1184             }
   1185             final boolean reallyUseQuotedPrintable =
   1186                 (mShouldUseQuotedPrintable &&
   1187                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
   1188             final boolean appendCharset =
   1189                 !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
   1190             final String encodedFormattedAddress;
   1191             if (reallyUseQuotedPrintable) {
   1192                 encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
   1193             } else {
   1194                 encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
   1195             }
   1196 
   1197             // We use the second value ("Extended Address") just because Japanese mobile phones
   1198             // do so. If the other importer expects the value be in the other field, some flag may
   1199             // be needed.
   1200             final StringBuilder addressBuilder = new StringBuilder();
   1201             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
   1202             addressBuilder.append(encodedFormattedAddress);
   1203             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
   1204             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
   1205             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
   1206             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
   1207             addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
   1208             return new PostalStruct(
   1209                     reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
   1210         }
   1211     }
   1212 
   1213     public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
   1214         if (contentValuesList != null) {
   1215             for (ContentValues contentValues : contentValuesList) {
   1216                 final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
   1217                 if (protocolAsObject == null) {
   1218                     continue;
   1219                 }
   1220                 final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
   1221                 if (propertyName == null) {
   1222                     continue;
   1223                 }
   1224                 String data = contentValues.getAsString(Im.DATA);
   1225                 if (data != null) {
   1226                     data = data.trim();
   1227                 }
   1228                 if (TextUtils.isEmpty(data)) {
   1229                     continue;
   1230                 }
   1231                 final String typeAsString;
   1232                 {
   1233                     final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
   1234                     switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
   1235                         case Im.TYPE_HOME: {
   1236                             typeAsString = VCardConstants.PARAM_TYPE_HOME;
   1237                             break;
   1238                         }
   1239                         case Im.TYPE_WORK: {
   1240                             typeAsString = VCardConstants.PARAM_TYPE_WORK;
   1241                             break;
   1242                         }
   1243                         case Im.TYPE_CUSTOM: {
   1244                             final String label = contentValues.getAsString(Im.LABEL);
   1245                             typeAsString = (label != null ? "X-" + label : null);
   1246                             break;
   1247                         }
   1248                         case Im.TYPE_OTHER:  // Ignore
   1249                         default: {
   1250                             typeAsString = null;
   1251                             break;
   1252                         }
   1253                     }
   1254                 }
   1255 
   1256                 final List<String> parameterList = new ArrayList<String>();
   1257                 if (!TextUtils.isEmpty(typeAsString)) {
   1258                     parameterList.add(typeAsString);
   1259                 }
   1260                 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
   1261                 final boolean isPrimary = (isPrimaryAsInteger != null ?
   1262                         (isPrimaryAsInteger > 0) : false);
   1263                 if (isPrimary) {
   1264                     parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1265                 }
   1266 
   1267                 appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
   1268             }
   1269         }
   1270         return this;
   1271     }
   1272 
   1273     public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
   1274         if (contentValuesList != null) {
   1275             for (ContentValues contentValues : contentValuesList) {
   1276                 String website = contentValues.getAsString(Website.URL);
   1277                 if (website != null) {
   1278                     website = website.trim();
   1279                 }
   1280 
   1281                 // Note: vCard 3.0 does not allow any parameter addition toward "URL"
   1282                 //       property, while there's no document in vCard 2.1.
   1283                 if (!TextUtils.isEmpty(website)) {
   1284                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
   1285                 }
   1286             }
   1287         }
   1288         return this;
   1289     }
   1290 
   1291     public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
   1292         if (contentValuesList != null) {
   1293             for (ContentValues contentValues : contentValuesList) {
   1294                 String company = contentValues.getAsString(Organization.COMPANY);
   1295                 if (company != null) {
   1296                     company = company.trim();
   1297                 }
   1298                 String department = contentValues.getAsString(Organization.DEPARTMENT);
   1299                 if (department != null) {
   1300                     department = department.trim();
   1301                 }
   1302                 String title = contentValues.getAsString(Organization.TITLE);
   1303                 if (title != null) {
   1304                     title = title.trim();
   1305                 }
   1306 
   1307                 StringBuilder orgBuilder = new StringBuilder();
   1308                 if (!TextUtils.isEmpty(company)) {
   1309                     orgBuilder.append(company);
   1310                 }
   1311                 if (!TextUtils.isEmpty(department)) {
   1312                     if (orgBuilder.length() > 0) {
   1313                         orgBuilder.append(';');
   1314                     }
   1315                     orgBuilder.append(department);
   1316                 }
   1317                 final String orgline = orgBuilder.toString();
   1318                 appendLine(VCardConstants.PROPERTY_ORG, orgline,
   1319                         !VCardUtils.containsOnlyPrintableAscii(orgline),
   1320                         (mShouldUseQuotedPrintable &&
   1321                                 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
   1322 
   1323                 if (!TextUtils.isEmpty(title)) {
   1324                     appendLine(VCardConstants.PROPERTY_TITLE, title,
   1325                             !VCardUtils.containsOnlyPrintableAscii(title),
   1326                             (mShouldUseQuotedPrintable &&
   1327                                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
   1328                 }
   1329             }
   1330         }
   1331         return this;
   1332     }
   1333 
   1334     public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
   1335         if (contentValuesList != null) {
   1336             for (ContentValues contentValues : contentValuesList) {
   1337                 if (contentValues == null) {
   1338                     continue;
   1339                 }
   1340                 byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
   1341                 if (data == null) {
   1342                     continue;
   1343                 }
   1344                 final String photoType = VCardUtils.guessImageType(data);
   1345                 if (photoType == null) {
   1346                     Log.d(LOG_TAG, "Unknown photo type. Ignored.");
   1347                     continue;
   1348                 }
   1349                 // TODO: check this works fine.
   1350                 final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
   1351                 if (!TextUtils.isEmpty(photoString)) {
   1352                     appendPhotoLine(photoString, photoType);
   1353                 }
   1354             }
   1355         }
   1356         return this;
   1357     }
   1358 
   1359     public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
   1360         if (contentValuesList != null) {
   1361             if (mOnlyOneNoteFieldIsAvailable) {
   1362                 final StringBuilder noteBuilder = new StringBuilder();
   1363                 boolean first = true;
   1364                 for (final ContentValues contentValues : contentValuesList) {
   1365                     String note = contentValues.getAsString(Note.NOTE);
   1366                     if (note == null) {
   1367                         note = "";
   1368                     }
   1369                     if (note.length() > 0) {
   1370                         if (first) {
   1371                             first = false;
   1372                         } else {
   1373                             noteBuilder.append('\n');
   1374                         }
   1375                         noteBuilder.append(note);
   1376                     }
   1377                 }
   1378                 final String noteStr = noteBuilder.toString();
   1379                 // This means we scan noteStr completely twice, which is redundant.
   1380                 // But for now, we assume this is not so time-consuming..
   1381                 final boolean shouldAppendCharsetInfo =
   1382                     !VCardUtils.containsOnlyPrintableAscii(noteStr);
   1383                 final boolean reallyUseQuotedPrintable =
   1384                         (mShouldUseQuotedPrintable &&
   1385                             !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
   1386                 appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
   1387                         shouldAppendCharsetInfo, reallyUseQuotedPrintable);
   1388             } else {
   1389                 for (ContentValues contentValues : contentValuesList) {
   1390                     final String noteStr = contentValues.getAsString(Note.NOTE);
   1391                     if (!TextUtils.isEmpty(noteStr)) {
   1392                         final boolean shouldAppendCharsetInfo =
   1393                                 !VCardUtils.containsOnlyPrintableAscii(noteStr);
   1394                         final boolean reallyUseQuotedPrintable =
   1395                                 (mShouldUseQuotedPrintable &&
   1396                                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
   1397                         appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
   1398                                 shouldAppendCharsetInfo, reallyUseQuotedPrintable);
   1399                     }
   1400                 }
   1401             }
   1402         }
   1403         return this;
   1404     }
   1405 
   1406     public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
   1407         // There's possibility where a given object may have more than one birthday, which
   1408         // is inappropriate. We just build one birthday.
   1409         if (contentValuesList != null) {
   1410             String primaryBirthday = null;
   1411             String secondaryBirthday = null;
   1412             for (final ContentValues contentValues : contentValuesList) {
   1413                 if (contentValues == null) {
   1414                     continue;
   1415                 }
   1416                 final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
   1417                 final int eventType;
   1418                 if (eventTypeAsInteger != null) {
   1419                     eventType = eventTypeAsInteger;
   1420                 } else {
   1421                     eventType = Event.TYPE_OTHER;
   1422                 }
   1423                 if (eventType == Event.TYPE_BIRTHDAY) {
   1424                     final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
   1425                     if (birthdayCandidate == null) {
   1426                         continue;
   1427                     }
   1428                     final Integer isSuperPrimaryAsInteger =
   1429                         contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
   1430                     final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
   1431                             (isSuperPrimaryAsInteger > 0) : false);
   1432                     if (isSuperPrimary) {
   1433                         // "super primary" birthday should the prefered one.
   1434                         primaryBirthday = birthdayCandidate;
   1435                         break;
   1436                     }
   1437                     final Integer isPrimaryAsInteger =
   1438                         contentValues.getAsInteger(Event.IS_PRIMARY);
   1439                     final boolean isPrimary = (isPrimaryAsInteger != null ?
   1440                             (isPrimaryAsInteger > 0) : false);
   1441                     if (isPrimary) {
   1442                         // We don't break here since "super primary" birthday may exist later.
   1443                         primaryBirthday = birthdayCandidate;
   1444                     } else if (secondaryBirthday == null) {
   1445                         // First entry is set to the "secondary" candidate.
   1446                         secondaryBirthday = birthdayCandidate;
   1447                     }
   1448                 } else if (mUsesAndroidProperty) {
   1449                     // Event types other than Birthday is not supported by vCard.
   1450                     appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
   1451                 }
   1452             }
   1453             if (primaryBirthday != null) {
   1454                 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
   1455                         primaryBirthday.trim());
   1456             } else if (secondaryBirthday != null){
   1457                 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
   1458                         secondaryBirthday.trim());
   1459             }
   1460         }
   1461         return this;
   1462     }
   1463 
   1464     public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
   1465         if (mUsesAndroidProperty && contentValuesList != null) {
   1466             for (final ContentValues contentValues : contentValuesList) {
   1467                 if (contentValues == null) {
   1468                     continue;
   1469                 }
   1470                 appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
   1471             }
   1472         }
   1473         return this;
   1474     }
   1475 
   1476     /**
   1477      * @param emitEveryTime If true, builder builds the line even when there's no entry.
   1478      */
   1479     public void appendPostalLine(final int type, final String label,
   1480             final ContentValues contentValues,
   1481             final boolean isPrimary, final boolean emitEveryTime) {
   1482         final boolean reallyUseQuotedPrintable;
   1483         final boolean appendCharset;
   1484         final String addressValue;
   1485         {
   1486             PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
   1487             if (postalStruct == null) {
   1488                 if (emitEveryTime) {
   1489                     reallyUseQuotedPrintable = false;
   1490                     appendCharset = false;
   1491                     addressValue = "";
   1492                 } else {
   1493                     return;
   1494                 }
   1495             } else {
   1496                 reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
   1497                 appendCharset = postalStruct.appendCharset;
   1498                 addressValue = postalStruct.addressData;
   1499             }
   1500         }
   1501 
   1502         List<String> parameterList = new ArrayList<String>();
   1503         if (isPrimary) {
   1504             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1505         }
   1506         switch (type) {
   1507             case StructuredPostal.TYPE_HOME: {
   1508                 parameterList.add(VCardConstants.PARAM_TYPE_HOME);
   1509                 break;
   1510             }
   1511             case StructuredPostal.TYPE_WORK: {
   1512                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1513                 break;
   1514             }
   1515             case StructuredPostal.TYPE_CUSTOM: {
   1516                 if (!TextUtils.isEmpty(label)
   1517                         && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1518                     // We're not sure whether the label is valid in the spec
   1519                     // ("IANA-token" in the vCard 3.0 is unclear...)
   1520                     // Just  for safety, we add "X-" at the beggining of each label.
   1521                     // Also checks the label obeys with vCard 3.0 spec.
   1522                     parameterList.add("X-" + label);
   1523                 }
   1524                 break;
   1525             }
   1526             case StructuredPostal.TYPE_OTHER: {
   1527                 break;
   1528             }
   1529             default: {
   1530                 Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
   1531                 break;
   1532             }
   1533         }
   1534 
   1535         mBuilder.append(VCardConstants.PROPERTY_ADR);
   1536         if (!parameterList.isEmpty()) {
   1537             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1538             appendTypeParameters(parameterList);
   1539         }
   1540         if (appendCharset) {
   1541             // Strictly, vCard 3.0 does not allow exporters to emit charset information,
   1542             // but we will add it since the information should be useful for importers,
   1543             //
   1544             // Assume no parser does not emit error with this parameter in vCard 3.0.
   1545             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1546             mBuilder.append(mVCardCharsetParameter);
   1547         }
   1548         if (reallyUseQuotedPrintable) {
   1549             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1550             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1551         }
   1552         mBuilder.append(VCARD_DATA_SEPARATOR);
   1553         mBuilder.append(addressValue);
   1554         mBuilder.append(VCARD_END_OF_LINE);
   1555     }
   1556 
   1557     public void appendEmailLine(final int type, final String label,
   1558             final String rawValue, final boolean isPrimary) {
   1559         final String typeAsString;
   1560         switch (type) {
   1561             case Email.TYPE_CUSTOM: {
   1562                 if (VCardUtils.isMobilePhoneLabel(label)) {
   1563                     typeAsString = VCardConstants.PARAM_TYPE_CELL;
   1564                 } else if (!TextUtils.isEmpty(label)
   1565                         && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1566                     typeAsString = "X-" + label;
   1567                 } else {
   1568                     typeAsString = null;
   1569                 }
   1570                 break;
   1571             }
   1572             case Email.TYPE_HOME: {
   1573                 typeAsString = VCardConstants.PARAM_TYPE_HOME;
   1574                 break;
   1575             }
   1576             case Email.TYPE_WORK: {
   1577                 typeAsString = VCardConstants.PARAM_TYPE_WORK;
   1578                 break;
   1579             }
   1580             case Email.TYPE_OTHER: {
   1581                 typeAsString = null;
   1582                 break;
   1583             }
   1584             case Email.TYPE_MOBILE: {
   1585                 typeAsString = VCardConstants.PARAM_TYPE_CELL;
   1586                 break;
   1587             }
   1588             default: {
   1589                 Log.e(LOG_TAG, "Unknown Email type: " + type);
   1590                 typeAsString = null;
   1591                 break;
   1592             }
   1593         }
   1594 
   1595         final List<String> parameterList = new ArrayList<String>();
   1596         if (isPrimary) {
   1597             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1598         }
   1599         if (!TextUtils.isEmpty(typeAsString)) {
   1600             parameterList.add(typeAsString);
   1601         }
   1602 
   1603         appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
   1604                 rawValue);
   1605     }
   1606 
   1607     public void appendTelLine(final Integer typeAsInteger, final String label,
   1608             final String encodedValue, boolean isPrimary) {
   1609         mBuilder.append(VCardConstants.PROPERTY_TEL);
   1610         mBuilder.append(VCARD_PARAM_SEPARATOR);
   1611 
   1612         final int type;
   1613         if (typeAsInteger == null) {
   1614             type = Phone.TYPE_OTHER;
   1615         } else {
   1616             type = typeAsInteger;
   1617         }
   1618 
   1619         ArrayList<String> parameterList = new ArrayList<String>();
   1620         switch (type) {
   1621             case Phone.TYPE_HOME: {
   1622                 parameterList.addAll(
   1623                         Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
   1624                 break;
   1625             }
   1626             case Phone.TYPE_WORK: {
   1627                 parameterList.addAll(
   1628                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
   1629                 break;
   1630             }
   1631             case Phone.TYPE_FAX_HOME: {
   1632                 parameterList.addAll(
   1633                         Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
   1634                 break;
   1635             }
   1636             case Phone.TYPE_FAX_WORK: {
   1637                 parameterList.addAll(
   1638                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
   1639                 break;
   1640             }
   1641             case Phone.TYPE_MOBILE: {
   1642                 parameterList.add(VCardConstants.PARAM_TYPE_CELL);
   1643                 break;
   1644             }
   1645             case Phone.TYPE_PAGER: {
   1646                 if (mIsDoCoMo) {
   1647                     // Not sure about the reason, but previous implementation had
   1648                     // used "VOICE" instead of "PAGER"
   1649                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1650                 } else {
   1651                     parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
   1652                 }
   1653                 break;
   1654             }
   1655             case Phone.TYPE_OTHER: {
   1656                 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1657                 break;
   1658             }
   1659             case Phone.TYPE_CAR: {
   1660                 parameterList.add(VCardConstants.PARAM_TYPE_CAR);
   1661                 break;
   1662             }
   1663             case Phone.TYPE_COMPANY_MAIN: {
   1664                 // There's no relevant field in vCard (at least 2.1).
   1665                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1666                 isPrimary = true;
   1667                 break;
   1668             }
   1669             case Phone.TYPE_ISDN: {
   1670                 parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
   1671                 break;
   1672             }
   1673             case Phone.TYPE_MAIN: {
   1674                 isPrimary = true;
   1675                 break;
   1676             }
   1677             case Phone.TYPE_OTHER_FAX: {
   1678                 parameterList.add(VCardConstants.PARAM_TYPE_FAX);
   1679                 break;
   1680             }
   1681             case Phone.TYPE_TELEX: {
   1682                 parameterList.add(VCardConstants.PARAM_TYPE_TLX);
   1683                 break;
   1684             }
   1685             case Phone.TYPE_WORK_MOBILE: {
   1686                 parameterList.addAll(
   1687                         Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
   1688                 break;
   1689             }
   1690             case Phone.TYPE_WORK_PAGER: {
   1691                 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
   1692                 // See above.
   1693                 if (mIsDoCoMo) {
   1694                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1695                 } else {
   1696                     parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
   1697                 }
   1698                 break;
   1699             }
   1700             case Phone.TYPE_MMS: {
   1701                 parameterList.add(VCardConstants.PARAM_TYPE_MSG);
   1702                 break;
   1703             }
   1704             case Phone.TYPE_CUSTOM: {
   1705                 if (TextUtils.isEmpty(label)) {
   1706                     // Just ignore the custom type.
   1707                     parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
   1708                 } else if (VCardUtils.isMobilePhoneLabel(label)) {
   1709                     parameterList.add(VCardConstants.PARAM_TYPE_CELL);
   1710                 } else if (mIsV30OrV40) {
   1711                     // This label is appropriately encoded in appendTypeParameters.
   1712                     parameterList.add(label);
   1713                 } else {
   1714                     final String upperLabel = label.toUpperCase();
   1715                     if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
   1716                         parameterList.add(upperLabel);
   1717                     } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
   1718                         // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
   1719                         //       "TYPE=" string.
   1720                         parameterList.add("X-" + label);
   1721                     }
   1722                 }
   1723                 break;
   1724             }
   1725             case Phone.TYPE_RADIO:
   1726             case Phone.TYPE_TTY_TDD:
   1727             default: {
   1728                 break;
   1729             }
   1730         }
   1731 
   1732         if (isPrimary) {
   1733             parameterList.add(VCardConstants.PARAM_TYPE_PREF);
   1734         }
   1735 
   1736         if (parameterList.isEmpty()) {
   1737             appendUncommonPhoneType(mBuilder, type);
   1738         } else {
   1739             appendTypeParameters(parameterList);
   1740         }
   1741 
   1742         mBuilder.append(VCARD_DATA_SEPARATOR);
   1743         mBuilder.append(encodedValue);
   1744         mBuilder.append(VCARD_END_OF_LINE);
   1745     }
   1746 
   1747     /**
   1748      * Appends phone type string which may not be available in some devices.
   1749      */
   1750     private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
   1751         if (mIsDoCoMo) {
   1752             // The previous implementation for DoCoMo had been conservative
   1753             // about miscellaneous types.
   1754             builder.append(VCardConstants.PARAM_TYPE_VOICE);
   1755         } else {
   1756             String phoneType = VCardUtils.getPhoneTypeString(type);
   1757             if (phoneType != null) {
   1758                 appendTypeParameter(phoneType);
   1759             } else {
   1760                 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
   1761             }
   1762         }
   1763     }
   1764 
   1765     /**
   1766      * @param encodedValue Must be encoded by BASE64
   1767      * @param photoType
   1768      */
   1769     public void appendPhotoLine(final String encodedValue, final String photoType) {
   1770         StringBuilder tmpBuilder = new StringBuilder();
   1771         tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
   1772         tmpBuilder.append(VCARD_PARAM_SEPARATOR);
   1773         if (mIsV30OrV40) {
   1774             tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
   1775         } else {
   1776             tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
   1777         }
   1778         tmpBuilder.append(VCARD_PARAM_SEPARATOR);
   1779         appendTypeParameter(tmpBuilder, photoType);
   1780         tmpBuilder.append(VCARD_DATA_SEPARATOR);
   1781         tmpBuilder.append(encodedValue);
   1782 
   1783         final String tmpStr = tmpBuilder.toString();
   1784         tmpBuilder = new StringBuilder();
   1785         int lineCount = 0;
   1786         final int length = tmpStr.length();
   1787         final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
   1788                 - VCARD_END_OF_LINE.length();
   1789         final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
   1790         int maxNum = maxNumForFirstLine;
   1791         for (int i = 0; i < length; i++) {
   1792             tmpBuilder.append(tmpStr.charAt(i));
   1793             lineCount++;
   1794             if (lineCount > maxNum) {
   1795                 tmpBuilder.append(VCARD_END_OF_LINE);
   1796                 tmpBuilder.append(VCARD_WS);
   1797                 maxNum = maxNumInGeneral;
   1798                 lineCount = 0;
   1799             }
   1800         }
   1801         mBuilder.append(tmpBuilder.toString());
   1802         mBuilder.append(VCARD_END_OF_LINE);
   1803         mBuilder.append(VCARD_END_OF_LINE);
   1804     }
   1805 
   1806     /**
   1807      * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
   1808      * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
   1809      * instead of "IMPP;sip:...".
   1810      *
   1811      * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
   1812      */
   1813     public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
   1814         final boolean useXProperty;
   1815         if (mIsV30OrV40) {
   1816             useXProperty = false;
   1817         } else if (mUsesDefactProperty){
   1818             useXProperty = true;
   1819         } else {
   1820             return this;
   1821         }
   1822 
   1823         if (contentValuesList != null) {
   1824             for (ContentValues contentValues : contentValuesList) {
   1825                 String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
   1826                 if (TextUtils.isEmpty(sipAddress)) {
   1827                     continue;
   1828                 }
   1829                 if (useXProperty) {
   1830                     // X-SIP does not contain "sip:" prefix.
   1831                     if (sipAddress.startsWith("sip:")) {
   1832                         if (sipAddress.length() == 4) {
   1833                             continue;
   1834                         }
   1835                         sipAddress = sipAddress.substring(4);
   1836                     }
   1837                     // No type is available yet.
   1838                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
   1839                 } else {
   1840                     if (!sipAddress.startsWith("sip:")) {
   1841                         sipAddress = "sip:" + sipAddress;
   1842                     }
   1843                     final String propertyName;
   1844                     if (VCardConfig.isVersion40(mVCardType)) {
   1845                         // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
   1846                         // TEL seems appropriate but may change in the future.
   1847                         propertyName = VCardConstants.PROPERTY_TEL;
   1848                     } else {
   1849                         // RFC 4770 (for vCard 3.0)
   1850                         propertyName = VCardConstants.PROPERTY_IMPP;
   1851                     }
   1852                     appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
   1853                 }
   1854             }
   1855         }
   1856         return this;
   1857     }
   1858 
   1859     public void appendAndroidSpecificProperty(
   1860             final String mimeType, ContentValues contentValues) {
   1861         if (!sAllowedAndroidPropertySet.contains(mimeType)) {
   1862             return;
   1863         }
   1864         final List<String> rawValueList = new ArrayList<String>();
   1865         for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
   1866             String value = contentValues.getAsString("data" + i);
   1867             if (value == null) {
   1868                 value = "";
   1869             }
   1870             rawValueList.add(value);
   1871         }
   1872 
   1873         boolean needCharset =
   1874             (mShouldAppendCharsetParam &&
   1875                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1876         boolean reallyUseQuotedPrintable =
   1877             (mShouldUseQuotedPrintable &&
   1878                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1879         mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
   1880         if (needCharset) {
   1881             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1882             mBuilder.append(mVCardCharsetParameter);
   1883         }
   1884         if (reallyUseQuotedPrintable) {
   1885             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1886             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1887         }
   1888         mBuilder.append(VCARD_DATA_SEPARATOR);
   1889         mBuilder.append(mimeType);  // Should not be encoded.
   1890         for (String rawValue : rawValueList) {
   1891             final String encodedValue;
   1892             if (reallyUseQuotedPrintable) {
   1893                 encodedValue = encodeQuotedPrintable(rawValue);
   1894             } else {
   1895                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
   1896                 //        (which says "When generating a content line, lines longer than
   1897                 //        75 characters SHOULD be folded"), though several
   1898                 //        (even well-known) applications do not care this.
   1899                 encodedValue = escapeCharacters(rawValue);
   1900             }
   1901             mBuilder.append(VCARD_ITEM_SEPARATOR);
   1902             mBuilder.append(encodedValue);
   1903         }
   1904         mBuilder.append(VCARD_END_OF_LINE);
   1905     }
   1906 
   1907     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1908             final String rawValue) {
   1909         appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
   1910     }
   1911 
   1912     public void appendLineWithCharsetAndQPDetection(
   1913             final String propertyName, final List<String> rawValueList) {
   1914         appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
   1915     }
   1916 
   1917     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1918             final List<String> parameterList, final String rawValue) {
   1919         final boolean needCharset =
   1920                 !VCardUtils.containsOnlyPrintableAscii(rawValue);
   1921         final boolean reallyUseQuotedPrintable =
   1922                 (mShouldUseQuotedPrintable &&
   1923                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
   1924         appendLine(propertyName, parameterList,
   1925                 rawValue, needCharset, reallyUseQuotedPrintable);
   1926     }
   1927 
   1928     public void appendLineWithCharsetAndQPDetection(final String propertyName,
   1929             final List<String> parameterList, final List<String> rawValueList) {
   1930         boolean needCharset =
   1931             (mShouldAppendCharsetParam &&
   1932                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1933         boolean reallyUseQuotedPrintable =
   1934             (mShouldUseQuotedPrintable &&
   1935                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
   1936         appendLine(propertyName, parameterList, rawValueList,
   1937                 needCharset, reallyUseQuotedPrintable);
   1938     }
   1939 
   1940     /**
   1941      * Appends one line with a given property name and value.
   1942      */
   1943     public void appendLine(final String propertyName, final String rawValue) {
   1944         appendLine(propertyName, rawValue, false, false);
   1945     }
   1946 
   1947     public void appendLine(final String propertyName, final List<String> rawValueList) {
   1948         appendLine(propertyName, rawValueList, false, false);
   1949     }
   1950 
   1951     public void appendLine(final String propertyName,
   1952             final String rawValue, final boolean needCharset,
   1953             boolean reallyUseQuotedPrintable) {
   1954         appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
   1955     }
   1956 
   1957     public void appendLine(final String propertyName, final List<String> parameterList,
   1958             final String rawValue) {
   1959         appendLine(propertyName, parameterList, rawValue, false, false);
   1960     }
   1961 
   1962     public void appendLine(final String propertyName, final List<String> parameterList,
   1963             final String rawValue, final boolean needCharset,
   1964             boolean reallyUseQuotedPrintable) {
   1965         mBuilder.append(propertyName);
   1966         if (parameterList != null && parameterList.size() > 0) {
   1967             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1968             appendTypeParameters(parameterList);
   1969         }
   1970         if (needCharset) {
   1971             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1972             mBuilder.append(mVCardCharsetParameter);
   1973         }
   1974 
   1975         final String encodedValue;
   1976         if (reallyUseQuotedPrintable) {
   1977             mBuilder.append(VCARD_PARAM_SEPARATOR);
   1978             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   1979             encodedValue = encodeQuotedPrintable(rawValue);
   1980         } else {
   1981             // TODO: one line may be too huge, which may be invalid in vCard spec, though
   1982             //       several (even well-known) applications do not care that violation.
   1983             encodedValue = escapeCharacters(rawValue);
   1984         }
   1985 
   1986         mBuilder.append(VCARD_DATA_SEPARATOR);
   1987         mBuilder.append(encodedValue);
   1988         mBuilder.append(VCARD_END_OF_LINE);
   1989     }
   1990 
   1991     public void appendLine(final String propertyName, final List<String> rawValueList,
   1992             final boolean needCharset, boolean needQuotedPrintable) {
   1993         appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
   1994     }
   1995 
   1996     public void appendLine(final String propertyName, final List<String> parameterList,
   1997             final List<String> rawValueList, final boolean needCharset,
   1998             final boolean needQuotedPrintable) {
   1999         mBuilder.append(propertyName);
   2000         if (parameterList != null && parameterList.size() > 0) {
   2001             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2002             appendTypeParameters(parameterList);
   2003         }
   2004         if (needCharset) {
   2005             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2006             mBuilder.append(mVCardCharsetParameter);
   2007         }
   2008         if (needQuotedPrintable) {
   2009             mBuilder.append(VCARD_PARAM_SEPARATOR);
   2010             mBuilder.append(VCARD_PARAM_ENCODING_QP);
   2011         }
   2012 
   2013         mBuilder.append(VCARD_DATA_SEPARATOR);
   2014         boolean first = true;
   2015         for (String rawValue : rawValueList) {
   2016             final String encodedValue;
   2017             if (needQuotedPrintable) {
   2018                 encodedValue = encodeQuotedPrintable(rawValue);
   2019             } else {
   2020                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
   2021                 //        (which says "When generating a content line, lines longer than
   2022                 //        75 characters SHOULD be folded"), though several
   2023                 //        (even well-known) applications do not care this.
   2024                 encodedValue = escapeCharacters(rawValue);
   2025             }
   2026 
   2027             if (first) {
   2028                 first = false;
   2029             } else {
   2030                 mBuilder.append(VCARD_ITEM_SEPARATOR);
   2031             }
   2032             mBuilder.append(encodedValue);
   2033         }
   2034         mBuilder.append(VCARD_END_OF_LINE);
   2035     }
   2036 
   2037     /**
   2038      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
   2039      */
   2040     private void appendTypeParameters(final List<String> types) {
   2041         // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
   2042         // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
   2043         boolean first = true;
   2044         for (final String typeValue : types) {
   2045             if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
   2046                 final String encoded = (VCardConfig.isVersion40(mVCardType) ?
   2047                         VCardUtils.toStringAsV40ParamValue(typeValue) :
   2048                         VCardUtils.toStringAsV30ParamValue(typeValue));
   2049                 if (TextUtils.isEmpty(encoded)) {
   2050                     continue;
   2051                 }
   2052 
   2053                 if (first) {
   2054                     first = false;
   2055                 } else {
   2056                     mBuilder.append(VCARD_PARAM_SEPARATOR);
   2057                 }
   2058                 appendTypeParameter(encoded);
   2059             } else {  // vCard 2.1
   2060                 if (!VCardUtils.isV21Word(typeValue)) {
   2061                     continue;
   2062                 }
   2063                 if (first) {
   2064                     first = false;
   2065                 } else {
   2066                     mBuilder.append(VCARD_PARAM_SEPARATOR);
   2067                 }
   2068                 appendTypeParameter(typeValue);
   2069             }
   2070         }
   2071     }
   2072 
   2073     /**
   2074      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
   2075      */
   2076     private void appendTypeParameter(final String type) {
   2077         appendTypeParameter(mBuilder, type);
   2078     }
   2079 
   2080     private void appendTypeParameter(final StringBuilder builder, final String type) {
   2081         // Refrain from using appendType() so that "TYPE=" is not be appended when the
   2082         // device is DoCoMo's (just for safety).
   2083         //
   2084         // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
   2085         if (VCardConfig.isVersion40(mVCardType) ||
   2086                 ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
   2087             builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
   2088         }
   2089         builder.append(type);
   2090     }
   2091 
   2092     /**
   2093      * Returns true when the property line should contain charset parameter
   2094      * information. This method may return true even when vCard version is 3.0.
   2095      *
   2096      * Strictly, adding charset information is invalid in VCard 3.0.
   2097      * However we'll add the info only when charset we use is not UTF-8
   2098      * in vCard 3.0 format, since parser side may be able to use the charset
   2099      * via this field, though we may encounter another problem by adding it.
   2100      *
   2101      * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
   2102      * recommends UTF-8. By adding this field, parsers may be able
   2103      * to know this text is NOT UTF-8 but Shift_Jis.
   2104      */
   2105     private boolean shouldAppendCharsetParam(String...propertyValueList) {
   2106         if (!mShouldAppendCharsetParam) {
   2107             return false;
   2108         }
   2109         for (String propertyValue : propertyValueList) {
   2110             if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
   2111                 return true;
   2112             }
   2113         }
   2114         return false;
   2115     }
   2116 
   2117     private String encodeQuotedPrintable(final String str) {
   2118         if (TextUtils.isEmpty(str)) {
   2119             return "";
   2120         }
   2121 
   2122         final StringBuilder builder = new StringBuilder();
   2123         int index = 0;
   2124         int lineCount = 0;
   2125         byte[] strArray = null;
   2126 
   2127         try {
   2128             strArray = str.getBytes(mCharset);
   2129         } catch (UnsupportedEncodingException e) {
   2130             Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
   2131                     + "Try default charset");
   2132             strArray = str.getBytes();
   2133         }
   2134         while (index < strArray.length) {
   2135             builder.append(String.format("=%02X", strArray[index]));
   2136             index += 1;
   2137             lineCount += 3;
   2138 
   2139             if (lineCount >= 67) {
   2140                 // Specification requires CRLF must be inserted before the
   2141                 // length of the line
   2142                 // becomes more than 76.
   2143                 // Assuming that the next character is a multi-byte character,
   2144                 // it will become
   2145                 // 6 bytes.
   2146                 // 76 - 6 - 3 = 67
   2147                 builder.append("=\r\n");
   2148                 lineCount = 0;
   2149             }
   2150         }
   2151 
   2152         return builder.toString();
   2153     }
   2154 
   2155     /**
   2156      * Append '\' to the characters which should be escaped. The character set is different
   2157      * not only between vCard 2.1 and vCard 3.0 but also among each device.
   2158      *
   2159      * Note that Quoted-Printable string must not be input here.
   2160      */
   2161     @SuppressWarnings("fallthrough")
   2162     private String escapeCharacters(final String unescaped) {
   2163         if (TextUtils.isEmpty(unescaped)) {
   2164             return "";
   2165         }
   2166 
   2167         final StringBuilder tmpBuilder = new StringBuilder();
   2168         final int length = unescaped.length();
   2169         for (int i = 0; i < length; i++) {
   2170             final char ch = unescaped.charAt(i);
   2171             switch (ch) {
   2172                 case ';': {
   2173                     tmpBuilder.append('\\');
   2174                     tmpBuilder.append(';');
   2175                     break;
   2176                 }
   2177                 case '\r': {
   2178                     if (i + 1 < length) {
   2179                         char nextChar = unescaped.charAt(i);
   2180                         if (nextChar == '\n') {
   2181                             break;
   2182                         } else {
   2183                             // fall through
   2184                         }
   2185                     } else {
   2186                         // fall through
   2187                     }
   2188                 }
   2189                 case '\n': {
   2190                     // In vCard 2.1, there's no specification about this, while
   2191                     // vCard 3.0 explicitly requires this should be encoded to "\n".
   2192                     tmpBuilder.append("\\n");
   2193                     break;
   2194                 }
   2195                 case '\\': {
   2196                     if (mIsV30OrV40) {
   2197                         tmpBuilder.append("\\\\");
   2198                         break;
   2199                     } else {
   2200                         // fall through
   2201                     }
   2202                 }
   2203                 case '<':
   2204                 case '>': {
   2205                     if (mIsDoCoMo) {
   2206                         tmpBuilder.append('\\');
   2207                         tmpBuilder.append(ch);
   2208                     } else {
   2209                         tmpBuilder.append(ch);
   2210                     }
   2211                     break;
   2212                 }
   2213                 case ',': {
   2214                     if (mIsV30OrV40) {
   2215                         tmpBuilder.append("\\,");
   2216                     } else {
   2217                         tmpBuilder.append(ch);
   2218                     }
   2219                     break;
   2220                 }
   2221                 default: {
   2222                     tmpBuilder.append(ch);
   2223                     break;
   2224                 }
   2225             }
   2226         }
   2227         return tmpBuilder.toString();
   2228     }
   2229 
   2230     @Override
   2231     public String toString() {
   2232         if (!mEndAppended) {
   2233             if (mIsDoCoMo) {
   2234                 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
   2235                 appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
   2236                 appendLine(VCardConstants.PROPERTY_X_NO, "");
   2237                 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
   2238             }
   2239             appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
   2240             mEndAppended = true;
   2241         }
   2242         return mBuilder.toString();
   2243     }
   2244 }
   2245