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