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