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