VCardBuilder.java revision 02117b3d19787ff65486b9f9db8abd338ae4c9f9
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 com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
19
20import android.content.ContentValues;
21import android.provider.ContactsContract.CommonDataKinds.Email;
22import android.provider.ContactsContract.CommonDataKinds.Event;
23import android.provider.ContactsContract.CommonDataKinds.Im;
24import android.provider.ContactsContract.CommonDataKinds.Nickname;
25import android.provider.ContactsContract.CommonDataKinds.Note;
26import android.provider.ContactsContract.CommonDataKinds.Organization;
27import android.provider.ContactsContract.CommonDataKinds.Phone;
28import android.provider.ContactsContract.CommonDataKinds.Photo;
29import android.provider.ContactsContract.CommonDataKinds.Relation;
30import android.provider.ContactsContract.CommonDataKinds.SipAddress;
31import android.provider.ContactsContract.CommonDataKinds.StructuredName;
32import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
33import android.provider.ContactsContract.CommonDataKinds.Website;
34import android.text.TextUtils;
35import android.util.Base64;
36import android.util.Log;
37
38import java.io.UnsupportedEncodingException;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.Collections;
42import java.util.HashMap;
43import java.util.HashSet;
44import java.util.List;
45import java.util.Map;
46import java.util.Set;
47
48/**
49 * <p>
50 * The class which lets users create their own vCard String. Typical usage is as follows:
51 * </p>
52 * <pre class="prettyprint">final VCardBuilder builder = new VCardBuilder(vcardType);
53 * builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
54 *     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
55 *     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
56 *     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
57 *     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
58 *     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
59 *     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
60 *     .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
61 *     .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
62 *     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
63 *     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
64 *     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
65 * return builder.toString();</pre>
66 */
67public class VCardBuilder {
68    private static final String LOG_TAG = VCardConstants.LOG_TAG;
69
70    // If you add the other element, please check all the columns are able to be
71    // converted to String.
72    //
73    // e.g. BLOB is not what we can handle here now.
74    private static final Set<String> sAllowedAndroidPropertySet =
75            Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
76                    Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
77                    Relation.CONTENT_ITEM_TYPE)));
78
79    public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
80    public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
81    public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
82
83    private static final String VCARD_DATA_VCARD = "VCARD";
84    private static final String VCARD_DATA_PUBLIC = "PUBLIC";
85
86    private static final String VCARD_PARAM_SEPARATOR = ";";
87    private static final String VCARD_END_OF_LINE = "\r\n";
88    private static final String VCARD_DATA_SEPARATOR = ":";
89    private static final String VCARD_ITEM_SEPARATOR = ";";
90    private static final String VCARD_WS = " ";
91    private static final String VCARD_PARAM_EQUAL = "=";
92
93    private static final String VCARD_PARAM_ENCODING_QP =
94            "ENCODING=" + VCardConstants.PARAM_ENCODING_QP;
95    private static final String VCARD_PARAM_ENCODING_BASE64_V21 =
96            "ENCODING=" + VCardConstants.PARAM_ENCODING_BASE64;
97    private static final String VCARD_PARAM_ENCODING_BASE64_AS_B =
98            "ENCODING=" + VCardConstants.PARAM_ENCODING_B;
99
100    private static final String SHIFT_JIS = "SHIFT_JIS";
101
102    private final int mVCardType;
103
104    private final boolean mIsV30OrV40;
105    private final boolean mIsJapaneseMobilePhone;
106    private final boolean mOnlyOneNoteFieldIsAvailable;
107    private final boolean mIsDoCoMo;
108    private final boolean mShouldUseQuotedPrintable;
109    private final boolean mUsesAndroidProperty;
110    private final boolean mUsesDefactProperty;
111    private final boolean mAppendTypeParamName;
112    private final boolean mRefrainsQPToNameProperties;
113    private final boolean mNeedsToConvertPhoneticString;
114
115    private final boolean mShouldAppendCharsetParam;
116
117    private final String mCharset;
118    private final String mVCardCharsetParameter;
119
120    private StringBuilder mBuilder;
121    private boolean mEndAppended;
122
123    public VCardBuilder(final int vcardType) {
124        // Default charset should be used
125        this(vcardType, null);
126    }
127
128    /**
129     * @param vcardType
130     * @param charset If null, we use default charset for export.
131     * @hide
132     */
133    public VCardBuilder(final int vcardType, String charset) {
134        mVCardType = vcardType;
135
136        if (VCardConfig.isVersion40(vcardType)) {
137            Log.w(LOG_TAG, "Should not use vCard 4.0 when building vCard. " +
138                    "It is not officially published yet.");
139        }
140
141        mIsV30OrV40 = VCardConfig.isVersion30(vcardType) || VCardConfig.isVersion40(vcardType);
142        mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType);
143        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
144        mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
145        mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
146        mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
147        mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
148        mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType);
149        mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
150        mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
151
152        // vCard 2.1 requires charset.
153        // vCard 3.0 does not allow it but we found some devices use it to determine
154        // the exact charset.
155        // We currently append it only when charset other than UTF_8 is used.
156        mShouldAppendCharsetParam =
157                !(VCardConfig.isVersion30(vcardType) && "UTF-8".equalsIgnoreCase(charset));
158
159        if (VCardConfig.isDoCoMo(vcardType)) {
160            if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
161                /* Log.w(LOG_TAG,
162                        "The charset \"" + charset + "\" is used while "
163                        + SHIFT_JIS + " is needed to be used."); */
164                if (TextUtils.isEmpty(charset)) {
165                    mCharset = SHIFT_JIS;
166                } else {
167                    /*try {
168                        charset = CharsetUtils.charsetForVendor(charset).name();
169                    } catch (UnsupportedCharsetException e) {
170                        Log.i(LOG_TAG,
171                                "Career-specific \"" + charset + "\" was not found (as usual). "
172                                + "Use it as is.");
173                    }*/
174                    mCharset = charset;
175                }
176            } else {
177                /*if (mIsDoCoMo) {
178                    try {
179                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
180                    } catch (UnsupportedCharsetException e) {
181                        Log.e(LOG_TAG,
182                                "DoCoMo-specific SHIFT_JIS was not found. "
183                                + "Use SHIFT_JIS as is.");
184                        charset = SHIFT_JIS;
185                    }
186                } else {
187                    try {
188                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
189                    } catch (UnsupportedCharsetException e) {
190                        Log.e(LOG_TAG,
191                                "Career-specific SHIFT_JIS was not found. "
192                                + "Use SHIFT_JIS as is.");
193                        charset = SHIFT_JIS;
194                    }
195                }*/
196                mCharset = charset;
197            }
198            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
199        } else {
200            if (TextUtils.isEmpty(charset)) {
201                Log.i(LOG_TAG,
202                        "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET
203                        + "\" for export.");
204                mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET;
205                mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET;
206            } else {
207                /*
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                                    PhoneNumberUtilsPort.formatNumber(
859                                            actualPhoneNumber, phoneFormat);
860
861                            // In vCard 4.0, value type must be "a single URI value",
862                            // not just a phone number. (Based on vCard 4.0 rev.13)
863                            if (VCardConfig.isVersion40(mVCardType)
864                                    && !TextUtils.isEmpty(formatted)
865                                    && !formatted.startsWith("tel:")) {
866                                formatted = "tel:" + formatted;
867                            }
868
869                            // Pre-formatted string should be stored.
870                            phoneSet.add(actualPhoneNumber);
871                            appendTelLine(type, label, formatted, isPrimary);
872                        }
873                    }  // for (String actualPhoneNumber : phoneNumberList) {
874
875                    // TODO: TEL with SIP URI?
876                }
877            }
878        }
879
880        if (!phoneLineExists && mIsDoCoMo) {
881            appendTelLine(Phone.TYPE_HOME, "", "", false);
882        }
883
884        return this;
885    }
886
887    /**
888     * <p>
889     * Splits a given string expressing phone numbers into several strings, and remove
890     * unnecessary characters inside them. The size of a returned list becomes 1 when
891     * no split is needed.
892     * </p>
893     * <p>
894     * The given number "may" have several phone numbers when the contact entry is corrupted
895     * because of its original source.
896     * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
897     * </p>
898     * <p>
899     * This kind of "phone numbers" will not be created with Android vCard implementation,
900     * but we may encounter them if the source of the input data has already corrupted
901     * implementation.
902     * </p>
903     * <p>
904     * To handle this case, this method first splits its input into multiple parts
905     * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
906     * removes unnecessary strings like "(Miami)".
907     * </p>
908     * <p>
909     * Do not call this method when trimming is inappropriate for its receivers.
910     * </p>
911     */
912    private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) {
913        final List<String> phoneList = new ArrayList<String>();
914
915        StringBuilder builder = new StringBuilder();
916        final int length = phoneNumber.length();
917        for (int i = 0; i < length; i++) {
918            final char ch = phoneNumber.charAt(i);
919            if (Character.isDigit(ch) || ch == '+') {
920                builder.append(ch);
921            } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
922                phoneList.add(builder.toString());
923                builder = new StringBuilder();
924            }
925        }
926        if (builder.length() > 0) {
927            phoneList.add(builder.toString());
928        }
929
930        return phoneList;
931    }
932
933    public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
934        boolean emailAddressExists = false;
935        if (contentValuesList != null) {
936            final Set<String> addressSet = new HashSet<String>();
937            for (ContentValues contentValues : contentValuesList) {
938                String emailAddress = contentValues.getAsString(Email.DATA);
939                if (emailAddress != null) {
940                    emailAddress = emailAddress.trim();
941                }
942                if (TextUtils.isEmpty(emailAddress)) {
943                    continue;
944                }
945                Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
946                final int type = (typeAsObject != null ?
947                        typeAsObject : DEFAULT_EMAIL_TYPE);
948                final String label = contentValues.getAsString(Email.LABEL);
949                Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
950                final boolean isPrimary = (isPrimaryAsInteger != null ?
951                        (isPrimaryAsInteger > 0) : false);
952                emailAddressExists = true;
953                if (!addressSet.contains(emailAddress)) {
954                    addressSet.add(emailAddress);
955                    appendEmailLine(type, label, emailAddress, isPrimary);
956                }
957            }
958        }
959
960        if (!emailAddressExists && mIsDoCoMo) {
961            appendEmailLine(Email.TYPE_HOME, "", "", false);
962        }
963
964        return this;
965    }
966
967    public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
968        if (contentValuesList == null || contentValuesList.isEmpty()) {
969            if (mIsDoCoMo) {
970                mBuilder.append(VCardConstants.PROPERTY_ADR);
971                mBuilder.append(VCARD_PARAM_SEPARATOR);
972                mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
973                mBuilder.append(VCARD_DATA_SEPARATOR);
974                mBuilder.append(VCARD_END_OF_LINE);
975            }
976        } else {
977            if (mIsDoCoMo) {
978                appendPostalsForDoCoMo(contentValuesList);
979            } else {
980                appendPostalsForGeneric(contentValuesList);
981            }
982        }
983
984        return this;
985    }
986
987    private static final Map<Integer, Integer> sPostalTypePriorityMap;
988
989    static {
990        sPostalTypePriorityMap = new HashMap<Integer, Integer>();
991        sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
992        sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
993        sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
994        sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
995    }
996
997    /**
998     * Tries to append just one line. If there's no appropriate address
999     * information, append an empty line.
1000     */
1001    private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
1002        int currentPriority = Integer.MAX_VALUE;
1003        int currentType = Integer.MAX_VALUE;
1004        ContentValues currentContentValues = null;
1005        for (final ContentValues contentValues : contentValuesList) {
1006            if (contentValues == null) {
1007                continue;
1008            }
1009            final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
1010            final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
1011            final int priority =
1012                    (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
1013            if (priority < currentPriority) {
1014                currentPriority = priority;
1015                currentType = typeAsInteger;
1016                currentContentValues = contentValues;
1017                if (priority == 0) {
1018                    break;
1019                }
1020            }
1021        }
1022
1023        if (currentContentValues == null) {
1024            Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
1025            return;
1026        }
1027
1028        final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
1029        appendPostalLine(currentType, label, currentContentValues, false, true);
1030    }
1031
1032    private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
1033        for (final ContentValues contentValues : contentValuesList) {
1034            if (contentValues == null) {
1035                continue;
1036            }
1037            final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
1038            final int type = (typeAsInteger != null ?
1039                    typeAsInteger : DEFAULT_POSTAL_TYPE);
1040            final String label = contentValues.getAsString(StructuredPostal.LABEL);
1041            final Integer isPrimaryAsInteger =
1042                contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
1043            final boolean isPrimary = (isPrimaryAsInteger != null ?
1044                    (isPrimaryAsInteger > 0) : false);
1045            appendPostalLine(type, label, contentValues, isPrimary, false);
1046        }
1047    }
1048
1049    private static class PostalStruct {
1050        final boolean reallyUseQuotedPrintable;
1051        final boolean appendCharset;
1052        final String addressData;
1053        public PostalStruct(final boolean reallyUseQuotedPrintable,
1054                final boolean appendCharset, final String addressData) {
1055            this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
1056            this.appendCharset = appendCharset;
1057            this.addressData = addressData;
1058        }
1059    }
1060
1061    /**
1062     * @return null when there's no information available to construct the data.
1063     */
1064    private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
1065        // adr-value    = 0*6(text-value ";") text-value
1066        //              ; PO Box, Extended Address, Street, Locality, Region, Postal
1067        //              ; Code, Country Name
1068        final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
1069        final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
1070        final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
1071        final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
1072        final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
1073        final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
1074        final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
1075        final String[] rawAddressArray = new String[]{
1076                rawPoBox, rawNeighborhood, rawStreet, rawLocality,
1077                rawRegion, rawPostalCode, rawCountry};
1078        if (!VCardUtils.areAllEmpty(rawAddressArray)) {
1079            final boolean reallyUseQuotedPrintable =
1080                (mShouldUseQuotedPrintable &&
1081                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
1082            final boolean appendCharset =
1083                !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
1084            final String encodedPoBox;
1085            final String encodedStreet;
1086            final String encodedLocality;
1087            final String encodedRegion;
1088            final String encodedPostalCode;
1089            final String encodedCountry;
1090            final String encodedNeighborhood;
1091
1092            final String rawLocality2;
1093            // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
1094            // but this is intentional.
1095            //
1096            // QP encoding may add line feeds when needed and the result of
1097            // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
1098            // may be different from
1099            // - encodedLocality + " " + encodedNeighborhood.
1100            //
1101            // We use safer way.
1102            if (TextUtils.isEmpty(rawLocality)) {
1103                if (TextUtils.isEmpty(rawNeighborhood)) {
1104                    rawLocality2 = "";
1105                } else {
1106                    rawLocality2 = rawNeighborhood;
1107                }
1108            } else {
1109                if (TextUtils.isEmpty(rawNeighborhood)) {
1110                    rawLocality2 = rawLocality;
1111                } else {
1112                    rawLocality2 = rawLocality + " " + rawNeighborhood;
1113                }
1114            }
1115            if (reallyUseQuotedPrintable) {
1116                encodedPoBox = encodeQuotedPrintable(rawPoBox);
1117                encodedStreet = encodeQuotedPrintable(rawStreet);
1118                encodedLocality = encodeQuotedPrintable(rawLocality2);
1119                encodedRegion = encodeQuotedPrintable(rawRegion);
1120                encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
1121                encodedCountry = encodeQuotedPrintable(rawCountry);
1122            } else {
1123                encodedPoBox = escapeCharacters(rawPoBox);
1124                encodedStreet = escapeCharacters(rawStreet);
1125                encodedLocality = escapeCharacters(rawLocality2);
1126                encodedRegion = escapeCharacters(rawRegion);
1127                encodedPostalCode = escapeCharacters(rawPostalCode);
1128                encodedCountry = escapeCharacters(rawCountry);
1129                encodedNeighborhood = escapeCharacters(rawNeighborhood);
1130            }
1131            final StringBuilder addressBuilder = new StringBuilder();
1132            addressBuilder.append(encodedPoBox);
1133            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
1134            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
1135            addressBuilder.append(encodedStreet);
1136            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
1137            addressBuilder.append(encodedLocality);
1138            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
1139            addressBuilder.append(encodedRegion);
1140            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
1141            addressBuilder.append(encodedPostalCode);
1142            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
1143            addressBuilder.append(encodedCountry);
1144            return new PostalStruct(
1145                    reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
1146        } else {  // VCardUtils.areAllEmpty(rawAddressArray) == true
1147            // Try to use FORMATTED_ADDRESS instead.
1148            final String rawFormattedAddress =
1149                contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1150            if (TextUtils.isEmpty(rawFormattedAddress)) {
1151                return null;
1152            }
1153            final boolean reallyUseQuotedPrintable =
1154                (mShouldUseQuotedPrintable &&
1155                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
1156            final boolean appendCharset =
1157                !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
1158            final String encodedFormattedAddress;
1159            if (reallyUseQuotedPrintable) {
1160                encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
1161            } else {
1162                encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
1163            }
1164
1165            // We use the second value ("Extended Address") just because Japanese mobile phones
1166            // do so. If the other importer expects the value be in the other field, some flag may
1167            // be needed.
1168            final StringBuilder addressBuilder = new StringBuilder();
1169            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // PO BOX ; Extended Address
1170            addressBuilder.append(encodedFormattedAddress);
1171            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Extended Address : Street
1172            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Street : Locality
1173            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Locality : Region
1174            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Region : Postal Code
1175            addressBuilder.append(VCARD_ITEM_SEPARATOR);  // Postal Code : Country
1176            return new PostalStruct(
1177                    reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
1178        }
1179    }
1180
1181    public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
1182        if (contentValuesList != null) {
1183            for (ContentValues contentValues : contentValuesList) {
1184                final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
1185                if (protocolAsObject == null) {
1186                    continue;
1187                }
1188                final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
1189                if (propertyName == null) {
1190                    continue;
1191                }
1192                String data = contentValues.getAsString(Im.DATA);
1193                if (data != null) {
1194                    data = data.trim();
1195                }
1196                if (TextUtils.isEmpty(data)) {
1197                    continue;
1198                }
1199                final String typeAsString;
1200                {
1201                    final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
1202                    switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
1203                        case Im.TYPE_HOME: {
1204                            typeAsString = VCardConstants.PARAM_TYPE_HOME;
1205                            break;
1206                        }
1207                        case Im.TYPE_WORK: {
1208                            typeAsString = VCardConstants.PARAM_TYPE_WORK;
1209                            break;
1210                        }
1211                        case Im.TYPE_CUSTOM: {
1212                            final String label = contentValues.getAsString(Im.LABEL);
1213                            typeAsString = (label != null ? "X-" + label : null);
1214                            break;
1215                        }
1216                        case Im.TYPE_OTHER:  // Ignore
1217                        default: {
1218                            typeAsString = null;
1219                            break;
1220                        }
1221                    }
1222                }
1223
1224                final List<String> parameterList = new ArrayList<String>();
1225                if (!TextUtils.isEmpty(typeAsString)) {
1226                    parameterList.add(typeAsString);
1227                }
1228                final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
1229                final boolean isPrimary = (isPrimaryAsInteger != null ?
1230                        (isPrimaryAsInteger > 0) : false);
1231                if (isPrimary) {
1232                    parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1233                }
1234
1235                appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
1236            }
1237        }
1238        return this;
1239    }
1240
1241    public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
1242        if (contentValuesList != null) {
1243            for (ContentValues contentValues : contentValuesList) {
1244                String website = contentValues.getAsString(Website.URL);
1245                if (website != null) {
1246                    website = website.trim();
1247                }
1248
1249                // Note: vCard 3.0 does not allow any parameter addition toward "URL"
1250                //       property, while there's no document in vCard 2.1.
1251                if (!TextUtils.isEmpty(website)) {
1252                    appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
1253                }
1254            }
1255        }
1256        return this;
1257    }
1258
1259    public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
1260        if (contentValuesList != null) {
1261            for (ContentValues contentValues : contentValuesList) {
1262                String company = contentValues.getAsString(Organization.COMPANY);
1263                if (company != null) {
1264                    company = company.trim();
1265                }
1266                String department = contentValues.getAsString(Organization.DEPARTMENT);
1267                if (department != null) {
1268                    department = department.trim();
1269                }
1270                String title = contentValues.getAsString(Organization.TITLE);
1271                if (title != null) {
1272                    title = title.trim();
1273                }
1274
1275                StringBuilder orgBuilder = new StringBuilder();
1276                if (!TextUtils.isEmpty(company)) {
1277                    orgBuilder.append(company);
1278                }
1279                if (!TextUtils.isEmpty(department)) {
1280                    if (orgBuilder.length() > 0) {
1281                        orgBuilder.append(';');
1282                    }
1283                    orgBuilder.append(department);
1284                }
1285                final String orgline = orgBuilder.toString();
1286                appendLine(VCardConstants.PROPERTY_ORG, orgline,
1287                        !VCardUtils.containsOnlyPrintableAscii(orgline),
1288                        (mShouldUseQuotedPrintable &&
1289                                !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
1290
1291                if (!TextUtils.isEmpty(title)) {
1292                    appendLine(VCardConstants.PROPERTY_TITLE, title,
1293                            !VCardUtils.containsOnlyPrintableAscii(title),
1294                            (mShouldUseQuotedPrintable &&
1295                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
1296                }
1297            }
1298        }
1299        return this;
1300    }
1301
1302    public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
1303        if (contentValuesList != null) {
1304            for (ContentValues contentValues : contentValuesList) {
1305                if (contentValues == null) {
1306                    continue;
1307                }
1308                byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
1309                if (data == null) {
1310                    continue;
1311                }
1312                final String photoType = VCardUtils.guessImageType(data);
1313                if (photoType == null) {
1314                    Log.d(LOG_TAG, "Unknown photo type. Ignored.");
1315                    continue;
1316                }
1317                // TODO: check this works fine.
1318                final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
1319                if (!TextUtils.isEmpty(photoString)) {
1320                    appendPhotoLine(photoString, photoType);
1321                }
1322            }
1323        }
1324        return this;
1325    }
1326
1327    public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
1328        if (contentValuesList != null) {
1329            if (mOnlyOneNoteFieldIsAvailable) {
1330                final StringBuilder noteBuilder = new StringBuilder();
1331                boolean first = true;
1332                for (final ContentValues contentValues : contentValuesList) {
1333                    String note = contentValues.getAsString(Note.NOTE);
1334                    if (note == null) {
1335                        note = "";
1336                    }
1337                    if (note.length() > 0) {
1338                        if (first) {
1339                            first = false;
1340                        } else {
1341                            noteBuilder.append('\n');
1342                        }
1343                        noteBuilder.append(note);
1344                    }
1345                }
1346                final String noteStr = noteBuilder.toString();
1347                // This means we scan noteStr completely twice, which is redundant.
1348                // But for now, we assume this is not so time-consuming..
1349                final boolean shouldAppendCharsetInfo =
1350                    !VCardUtils.containsOnlyPrintableAscii(noteStr);
1351                final boolean reallyUseQuotedPrintable =
1352                        (mShouldUseQuotedPrintable &&
1353                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1354                appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
1355                        shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1356            } else {
1357                for (ContentValues contentValues : contentValuesList) {
1358                    final String noteStr = contentValues.getAsString(Note.NOTE);
1359                    if (!TextUtils.isEmpty(noteStr)) {
1360                        final boolean shouldAppendCharsetInfo =
1361                                !VCardUtils.containsOnlyPrintableAscii(noteStr);
1362                        final boolean reallyUseQuotedPrintable =
1363                                (mShouldUseQuotedPrintable &&
1364                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1365                        appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
1366                                shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1367                    }
1368                }
1369            }
1370        }
1371        return this;
1372    }
1373
1374    public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
1375        // There's possibility where a given object may have more than one birthday, which
1376        // is inappropriate. We just build one birthday.
1377        if (contentValuesList != null) {
1378            String primaryBirthday = null;
1379            String secondaryBirthday = null;
1380            for (final ContentValues contentValues : contentValuesList) {
1381                if (contentValues == null) {
1382                    continue;
1383                }
1384                final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
1385                final int eventType;
1386                if (eventTypeAsInteger != null) {
1387                    eventType = eventTypeAsInteger;
1388                } else {
1389                    eventType = Event.TYPE_OTHER;
1390                }
1391                if (eventType == Event.TYPE_BIRTHDAY) {
1392                    final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
1393                    if (birthdayCandidate == null) {
1394                        continue;
1395                    }
1396                    final Integer isSuperPrimaryAsInteger =
1397                        contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
1398                    final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
1399                            (isSuperPrimaryAsInteger > 0) : false);
1400                    if (isSuperPrimary) {
1401                        // "super primary" birthday should the prefered one.
1402                        primaryBirthday = birthdayCandidate;
1403                        break;
1404                    }
1405                    final Integer isPrimaryAsInteger =
1406                        contentValues.getAsInteger(Event.IS_PRIMARY);
1407                    final boolean isPrimary = (isPrimaryAsInteger != null ?
1408                            (isPrimaryAsInteger > 0) : false);
1409                    if (isPrimary) {
1410                        // We don't break here since "super primary" birthday may exist later.
1411                        primaryBirthday = birthdayCandidate;
1412                    } else if (secondaryBirthday == null) {
1413                        // First entry is set to the "secondary" candidate.
1414                        secondaryBirthday = birthdayCandidate;
1415                    }
1416                } else if (mUsesAndroidProperty) {
1417                    // Event types other than Birthday is not supported by vCard.
1418                    appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
1419                }
1420            }
1421            if (primaryBirthday != null) {
1422                appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
1423                        primaryBirthday.trim());
1424            } else if (secondaryBirthday != null){
1425                appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
1426                        secondaryBirthday.trim());
1427            }
1428        }
1429        return this;
1430    }
1431
1432    public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
1433        if (mUsesAndroidProperty && contentValuesList != null) {
1434            for (final ContentValues contentValues : contentValuesList) {
1435                if (contentValues == null) {
1436                    continue;
1437                }
1438                appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
1439            }
1440        }
1441        return this;
1442    }
1443
1444    /**
1445     * @param emitEveryTime If true, builder builds the line even when there's no entry.
1446     */
1447    public void appendPostalLine(final int type, final String label,
1448            final ContentValues contentValues,
1449            final boolean isPrimary, final boolean emitEveryTime) {
1450        final boolean reallyUseQuotedPrintable;
1451        final boolean appendCharset;
1452        final String addressValue;
1453        {
1454            PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
1455            if (postalStruct == null) {
1456                if (emitEveryTime) {
1457                    reallyUseQuotedPrintable = false;
1458                    appendCharset = false;
1459                    addressValue = "";
1460                } else {
1461                    return;
1462                }
1463            } else {
1464                reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
1465                appendCharset = postalStruct.appendCharset;
1466                addressValue = postalStruct.addressData;
1467            }
1468        }
1469
1470        List<String> parameterList = new ArrayList<String>();
1471        if (isPrimary) {
1472            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1473        }
1474        switch (type) {
1475            case StructuredPostal.TYPE_HOME: {
1476                parameterList.add(VCardConstants.PARAM_TYPE_HOME);
1477                break;
1478            }
1479            case StructuredPostal.TYPE_WORK: {
1480                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1481                break;
1482            }
1483            case StructuredPostal.TYPE_CUSTOM: {
1484                if (!TextUtils.isEmpty(label)
1485                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1486                    // We're not sure whether the label is valid in the spec
1487                    // ("IANA-token" in the vCard 3.0 is unclear...)
1488                    // Just  for safety, we add "X-" at the beggining of each label.
1489                    // Also checks the label obeys with vCard 3.0 spec.
1490                    parameterList.add("X-" + label);
1491                }
1492                break;
1493            }
1494            case StructuredPostal.TYPE_OTHER: {
1495                break;
1496            }
1497            default: {
1498                Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
1499                break;
1500            }
1501        }
1502
1503        mBuilder.append(VCardConstants.PROPERTY_ADR);
1504        if (!parameterList.isEmpty()) {
1505            mBuilder.append(VCARD_PARAM_SEPARATOR);
1506            appendTypeParameters(parameterList);
1507        }
1508        if (appendCharset) {
1509            // Strictly, vCard 3.0 does not allow exporters to emit charset information,
1510            // but we will add it since the information should be useful for importers,
1511            //
1512            // Assume no parser does not emit error with this parameter in vCard 3.0.
1513            mBuilder.append(VCARD_PARAM_SEPARATOR);
1514            mBuilder.append(mVCardCharsetParameter);
1515        }
1516        if (reallyUseQuotedPrintable) {
1517            mBuilder.append(VCARD_PARAM_SEPARATOR);
1518            mBuilder.append(VCARD_PARAM_ENCODING_QP);
1519        }
1520        mBuilder.append(VCARD_DATA_SEPARATOR);
1521        mBuilder.append(addressValue);
1522        mBuilder.append(VCARD_END_OF_LINE);
1523    }
1524
1525    public void appendEmailLine(final int type, final String label,
1526            final String rawValue, final boolean isPrimary) {
1527        final String typeAsString;
1528        switch (type) {
1529            case Email.TYPE_CUSTOM: {
1530                if (VCardUtils.isMobilePhoneLabel(label)) {
1531                    typeAsString = VCardConstants.PARAM_TYPE_CELL;
1532                } else if (!TextUtils.isEmpty(label)
1533                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1534                    typeAsString = "X-" + label;
1535                } else {
1536                    typeAsString = null;
1537                }
1538                break;
1539            }
1540            case Email.TYPE_HOME: {
1541                typeAsString = VCardConstants.PARAM_TYPE_HOME;
1542                break;
1543            }
1544            case Email.TYPE_WORK: {
1545                typeAsString = VCardConstants.PARAM_TYPE_WORK;
1546                break;
1547            }
1548            case Email.TYPE_OTHER: {
1549                typeAsString = null;
1550                break;
1551            }
1552            case Email.TYPE_MOBILE: {
1553                typeAsString = VCardConstants.PARAM_TYPE_CELL;
1554                break;
1555            }
1556            default: {
1557                Log.e(LOG_TAG, "Unknown Email type: " + type);
1558                typeAsString = null;
1559                break;
1560            }
1561        }
1562
1563        final List<String> parameterList = new ArrayList<String>();
1564        if (isPrimary) {
1565            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1566        }
1567        if (!TextUtils.isEmpty(typeAsString)) {
1568            parameterList.add(typeAsString);
1569        }
1570
1571        appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
1572                rawValue);
1573    }
1574
1575    public void appendTelLine(final Integer typeAsInteger, final String label,
1576            final String encodedValue, boolean isPrimary) {
1577        mBuilder.append(VCardConstants.PROPERTY_TEL);
1578        mBuilder.append(VCARD_PARAM_SEPARATOR);
1579
1580        final int type;
1581        if (typeAsInteger == null) {
1582            type = Phone.TYPE_OTHER;
1583        } else {
1584            type = typeAsInteger;
1585        }
1586
1587        ArrayList<String> parameterList = new ArrayList<String>();
1588        switch (type) {
1589            case Phone.TYPE_HOME: {
1590                parameterList.addAll(
1591                        Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
1592                break;
1593            }
1594            case Phone.TYPE_WORK: {
1595                parameterList.addAll(
1596                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
1597                break;
1598            }
1599            case Phone.TYPE_FAX_HOME: {
1600                parameterList.addAll(
1601                        Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
1602                break;
1603            }
1604            case Phone.TYPE_FAX_WORK: {
1605                parameterList.addAll(
1606                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
1607                break;
1608            }
1609            case Phone.TYPE_MOBILE: {
1610                parameterList.add(VCardConstants.PARAM_TYPE_CELL);
1611                break;
1612            }
1613            case Phone.TYPE_PAGER: {
1614                if (mIsDoCoMo) {
1615                    // Not sure about the reason, but previous implementation had
1616                    // used "VOICE" instead of "PAGER"
1617                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1618                } else {
1619                    parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
1620                }
1621                break;
1622            }
1623            case Phone.TYPE_OTHER: {
1624                parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1625                break;
1626            }
1627            case Phone.TYPE_CAR: {
1628                parameterList.add(VCardConstants.PARAM_TYPE_CAR);
1629                break;
1630            }
1631            case Phone.TYPE_COMPANY_MAIN: {
1632                // There's no relevant field in vCard (at least 2.1).
1633                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1634                isPrimary = true;
1635                break;
1636            }
1637            case Phone.TYPE_ISDN: {
1638                parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
1639                break;
1640            }
1641            case Phone.TYPE_MAIN: {
1642                isPrimary = true;
1643                break;
1644            }
1645            case Phone.TYPE_OTHER_FAX: {
1646                parameterList.add(VCardConstants.PARAM_TYPE_FAX);
1647                break;
1648            }
1649            case Phone.TYPE_TELEX: {
1650                parameterList.add(VCardConstants.PARAM_TYPE_TLX);
1651                break;
1652            }
1653            case Phone.TYPE_WORK_MOBILE: {
1654                parameterList.addAll(
1655                        Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
1656                break;
1657            }
1658            case Phone.TYPE_WORK_PAGER: {
1659                parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1660                // See above.
1661                if (mIsDoCoMo) {
1662                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1663                } else {
1664                    parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
1665                }
1666                break;
1667            }
1668            case Phone.TYPE_MMS: {
1669                parameterList.add(VCardConstants.PARAM_TYPE_MSG);
1670                break;
1671            }
1672            case Phone.TYPE_CUSTOM: {
1673                if (TextUtils.isEmpty(label)) {
1674                    // Just ignore the custom type.
1675                    parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1676                } else if (VCardUtils.isMobilePhoneLabel(label)) {
1677                    parameterList.add(VCardConstants.PARAM_TYPE_CELL);
1678                } else if (mIsV30OrV40) {
1679                    // This label is appropriately encoded in appendTypeParameters.
1680                    parameterList.add(label);
1681                } else {
1682                    final String upperLabel = label.toUpperCase();
1683                    if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
1684                        parameterList.add(upperLabel);
1685                    } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1686                        // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
1687                        //       "TYPE=" string.
1688                        parameterList.add("X-" + label);
1689                    }
1690                }
1691                break;
1692            }
1693            case Phone.TYPE_RADIO:
1694            case Phone.TYPE_TTY_TDD:
1695            default: {
1696                break;
1697            }
1698        }
1699
1700        if (isPrimary) {
1701            parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1702        }
1703
1704        if (parameterList.isEmpty()) {
1705            appendUncommonPhoneType(mBuilder, type);
1706        } else {
1707            appendTypeParameters(parameterList);
1708        }
1709
1710        mBuilder.append(VCARD_DATA_SEPARATOR);
1711        mBuilder.append(encodedValue);
1712        mBuilder.append(VCARD_END_OF_LINE);
1713    }
1714
1715    /**
1716     * Appends phone type string which may not be available in some devices.
1717     */
1718    private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
1719        if (mIsDoCoMo) {
1720            // The previous implementation for DoCoMo had been conservative
1721            // about miscellaneous types.
1722            builder.append(VCardConstants.PARAM_TYPE_VOICE);
1723        } else {
1724            String phoneType = VCardUtils.getPhoneTypeString(type);
1725            if (phoneType != null) {
1726                appendTypeParameter(phoneType);
1727            } else {
1728                Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
1729            }
1730        }
1731    }
1732
1733    /**
1734     * @param encodedValue Must be encoded by BASE64
1735     * @param photoType
1736     */
1737    public void appendPhotoLine(final String encodedValue, final String photoType) {
1738        StringBuilder tmpBuilder = new StringBuilder();
1739        tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
1740        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1741        if (mIsV30OrV40) {
1742            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
1743        } else {
1744            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
1745        }
1746        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1747        appendTypeParameter(tmpBuilder, photoType);
1748        tmpBuilder.append(VCARD_DATA_SEPARATOR);
1749        tmpBuilder.append(encodedValue);
1750
1751        final String tmpStr = tmpBuilder.toString();
1752        tmpBuilder = new StringBuilder();
1753        int lineCount = 0;
1754        final int length = tmpStr.length();
1755        final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
1756                - VCARD_END_OF_LINE.length();
1757        final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
1758        int maxNum = maxNumForFirstLine;
1759        for (int i = 0; i < length; i++) {
1760            tmpBuilder.append(tmpStr.charAt(i));
1761            lineCount++;
1762            if (lineCount > maxNum) {
1763                tmpBuilder.append(VCARD_END_OF_LINE);
1764                tmpBuilder.append(VCARD_WS);
1765                maxNum = maxNumInGeneral;
1766                lineCount = 0;
1767            }
1768        }
1769        mBuilder.append(tmpBuilder.toString());
1770        mBuilder.append(VCARD_END_OF_LINE);
1771        mBuilder.append(VCARD_END_OF_LINE);
1772    }
1773
1774    /**
1775     * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
1776     * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
1777     * instead of "IMPP;sip:...".
1778     *
1779     * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
1780     */
1781    public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
1782        final boolean useXProperty;
1783        if (mIsV30OrV40) {
1784            useXProperty = false;
1785        } else if (mUsesDefactProperty){
1786            useXProperty = true;
1787        } else {
1788            return this;
1789        }
1790
1791        if (contentValuesList != null) {
1792            for (ContentValues contentValues : contentValuesList) {
1793                String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
1794                if (TextUtils.isEmpty(sipAddress)) {
1795                    continue;
1796                }
1797                if (useXProperty) {
1798                    // X-SIP does not contain "sip:" prefix.
1799                    if (sipAddress.startsWith("sip:")) {
1800                        if (sipAddress.length() == 4) {
1801                            continue;
1802                        }
1803                        sipAddress = sipAddress.substring(4);
1804                    }
1805                    // No type is available yet.
1806                    appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
1807                } else {
1808                    if (!sipAddress.startsWith("sip:")) {
1809                        sipAddress = "sip:" + sipAddress;
1810                    }
1811                    final String propertyName;
1812                    if (VCardConfig.isVersion40(mVCardType)) {
1813                        // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
1814                        // TEL seems appropriate but may change in the future.
1815                        propertyName = VCardConstants.PROPERTY_TEL;
1816                    } else {
1817                        // RFC 4770 (for vCard 3.0)
1818                        propertyName = VCardConstants.PROPERTY_IMPP;
1819                    }
1820                    appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
1821                }
1822            }
1823        }
1824        return this;
1825    }
1826
1827    public void appendAndroidSpecificProperty(
1828            final String mimeType, ContentValues contentValues) {
1829        if (!sAllowedAndroidPropertySet.contains(mimeType)) {
1830            return;
1831        }
1832        final List<String> rawValueList = new ArrayList<String>();
1833        for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
1834            String value = contentValues.getAsString("data" + i);
1835            if (value == null) {
1836                value = "";
1837            }
1838            rawValueList.add(value);
1839        }
1840
1841        boolean needCharset =
1842            (mShouldAppendCharsetParam &&
1843                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1844        boolean reallyUseQuotedPrintable =
1845            (mShouldUseQuotedPrintable &&
1846                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1847        mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
1848        if (needCharset) {
1849            mBuilder.append(VCARD_PARAM_SEPARATOR);
1850            mBuilder.append(mVCardCharsetParameter);
1851        }
1852        if (reallyUseQuotedPrintable) {
1853            mBuilder.append(VCARD_PARAM_SEPARATOR);
1854            mBuilder.append(VCARD_PARAM_ENCODING_QP);
1855        }
1856        mBuilder.append(VCARD_DATA_SEPARATOR);
1857        mBuilder.append(mimeType);  // Should not be encoded.
1858        for (String rawValue : rawValueList) {
1859            final String encodedValue;
1860            if (reallyUseQuotedPrintable) {
1861                encodedValue = encodeQuotedPrintable(rawValue);
1862            } else {
1863                // TODO: one line may be too huge, which may be invalid in vCard 3.0
1864                //        (which says "When generating a content line, lines longer than
1865                //        75 characters SHOULD be folded"), though several
1866                //        (even well-known) applications do not care this.
1867                encodedValue = escapeCharacters(rawValue);
1868            }
1869            mBuilder.append(VCARD_ITEM_SEPARATOR);
1870            mBuilder.append(encodedValue);
1871        }
1872        mBuilder.append(VCARD_END_OF_LINE);
1873    }
1874
1875    public void appendLineWithCharsetAndQPDetection(final String propertyName,
1876            final String rawValue) {
1877        appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
1878    }
1879
1880    public void appendLineWithCharsetAndQPDetection(
1881            final String propertyName, final List<String> rawValueList) {
1882        appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
1883    }
1884
1885    public void appendLineWithCharsetAndQPDetection(final String propertyName,
1886            final List<String> parameterList, final String rawValue) {
1887        final boolean needCharset =
1888                !VCardUtils.containsOnlyPrintableAscii(rawValue);
1889        final boolean reallyUseQuotedPrintable =
1890                (mShouldUseQuotedPrintable &&
1891                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
1892        appendLine(propertyName, parameterList,
1893                rawValue, needCharset, reallyUseQuotedPrintable);
1894    }
1895
1896    public void appendLineWithCharsetAndQPDetection(final String propertyName,
1897            final List<String> parameterList, final List<String> rawValueList) {
1898        boolean needCharset =
1899            (mShouldAppendCharsetParam &&
1900                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1901        boolean reallyUseQuotedPrintable =
1902            (mShouldUseQuotedPrintable &&
1903                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1904        appendLine(propertyName, parameterList, rawValueList,
1905                needCharset, reallyUseQuotedPrintable);
1906    }
1907
1908    /**
1909     * Appends one line with a given property name and value.
1910     */
1911    public void appendLine(final String propertyName, final String rawValue) {
1912        appendLine(propertyName, rawValue, false, false);
1913    }
1914
1915    public void appendLine(final String propertyName, final List<String> rawValueList) {
1916        appendLine(propertyName, rawValueList, false, false);
1917    }
1918
1919    public void appendLine(final String propertyName,
1920            final String rawValue, final boolean needCharset,
1921            boolean reallyUseQuotedPrintable) {
1922        appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
1923    }
1924
1925    public void appendLine(final String propertyName, final List<String> parameterList,
1926            final String rawValue) {
1927        appendLine(propertyName, parameterList, rawValue, false, false);
1928    }
1929
1930    public void appendLine(final String propertyName, final List<String> parameterList,
1931            final String rawValue, final boolean needCharset,
1932            boolean reallyUseQuotedPrintable) {
1933        mBuilder.append(propertyName);
1934        if (parameterList != null && parameterList.size() > 0) {
1935            mBuilder.append(VCARD_PARAM_SEPARATOR);
1936            appendTypeParameters(parameterList);
1937        }
1938        if (needCharset) {
1939            mBuilder.append(VCARD_PARAM_SEPARATOR);
1940            mBuilder.append(mVCardCharsetParameter);
1941        }
1942
1943        final String encodedValue;
1944        if (reallyUseQuotedPrintable) {
1945            mBuilder.append(VCARD_PARAM_SEPARATOR);
1946            mBuilder.append(VCARD_PARAM_ENCODING_QP);
1947            encodedValue = encodeQuotedPrintable(rawValue);
1948        } else {
1949            // TODO: one line may be too huge, which may be invalid in vCard spec, though
1950            //       several (even well-known) applications do not care that violation.
1951            encodedValue = escapeCharacters(rawValue);
1952        }
1953
1954        mBuilder.append(VCARD_DATA_SEPARATOR);
1955        mBuilder.append(encodedValue);
1956        mBuilder.append(VCARD_END_OF_LINE);
1957    }
1958
1959    public void appendLine(final String propertyName, final List<String> rawValueList,
1960            final boolean needCharset, boolean needQuotedPrintable) {
1961        appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
1962    }
1963
1964    public void appendLine(final String propertyName, final List<String> parameterList,
1965            final List<String> rawValueList, final boolean needCharset,
1966            final boolean needQuotedPrintable) {
1967        mBuilder.append(propertyName);
1968        if (parameterList != null && parameterList.size() > 0) {
1969            mBuilder.append(VCARD_PARAM_SEPARATOR);
1970            appendTypeParameters(parameterList);
1971        }
1972        if (needCharset) {
1973            mBuilder.append(VCARD_PARAM_SEPARATOR);
1974            mBuilder.append(mVCardCharsetParameter);
1975        }
1976        if (needQuotedPrintable) {
1977            mBuilder.append(VCARD_PARAM_SEPARATOR);
1978            mBuilder.append(VCARD_PARAM_ENCODING_QP);
1979        }
1980
1981        mBuilder.append(VCARD_DATA_SEPARATOR);
1982        boolean first = true;
1983        for (String rawValue : rawValueList) {
1984            final String encodedValue;
1985            if (needQuotedPrintable) {
1986                encodedValue = encodeQuotedPrintable(rawValue);
1987            } else {
1988                // TODO: one line may be too huge, which may be invalid in vCard 3.0
1989                //        (which says "When generating a content line, lines longer than
1990                //        75 characters SHOULD be folded"), though several
1991                //        (even well-known) applications do not care this.
1992                encodedValue = escapeCharacters(rawValue);
1993            }
1994
1995            if (first) {
1996                first = false;
1997            } else {
1998                mBuilder.append(VCARD_ITEM_SEPARATOR);
1999            }
2000            mBuilder.append(encodedValue);
2001        }
2002        mBuilder.append(VCARD_END_OF_LINE);
2003    }
2004
2005    /**
2006     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2007     */
2008    private void appendTypeParameters(final List<String> types) {
2009        // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
2010        // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
2011        boolean first = true;
2012        for (final String typeValue : types) {
2013            if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
2014                final String encoded = (VCardConfig.isVersion40(mVCardType) ?
2015                        VCardUtils.toStringAsV40ParamValue(typeValue) :
2016                        VCardUtils.toStringAsV30ParamValue(typeValue));
2017                if (TextUtils.isEmpty(encoded)) {
2018                    continue;
2019                }
2020
2021                if (first) {
2022                    first = false;
2023                } else {
2024                    mBuilder.append(VCARD_PARAM_SEPARATOR);
2025                }
2026                appendTypeParameter(encoded);
2027            } else {  // vCard 2.1
2028                if (!VCardUtils.isV21Word(typeValue)) {
2029                    continue;
2030                }
2031                if (first) {
2032                    first = false;
2033                } else {
2034                    mBuilder.append(VCARD_PARAM_SEPARATOR);
2035                }
2036                appendTypeParameter(typeValue);
2037            }
2038        }
2039    }
2040
2041    /**
2042     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2043     */
2044    private void appendTypeParameter(final String type) {
2045        appendTypeParameter(mBuilder, type);
2046    }
2047
2048    private void appendTypeParameter(final StringBuilder builder, final String type) {
2049        // Refrain from using appendType() so that "TYPE=" is not be appended when the
2050        // device is DoCoMo's (just for safety).
2051        //
2052        // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
2053        if (VCardConfig.isVersion40(mVCardType) ||
2054                ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
2055            builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
2056        }
2057        builder.append(type);
2058    }
2059
2060    /**
2061     * Returns true when the property line should contain charset parameter
2062     * information. This method may return true even when vCard version is 3.0.
2063     *
2064     * Strictly, adding charset information is invalid in VCard 3.0.
2065     * However we'll add the info only when charset we use is not UTF-8
2066     * in vCard 3.0 format, since parser side may be able to use the charset
2067     * via this field, though we may encounter another problem by adding it.
2068     *
2069     * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
2070     * recommends UTF-8. By adding this field, parsers may be able
2071     * to know this text is NOT UTF-8 but Shift_Jis.
2072     */
2073    private boolean shouldAppendCharsetParam(String...propertyValueList) {
2074        if (!mShouldAppendCharsetParam) {
2075            return false;
2076        }
2077        for (String propertyValue : propertyValueList) {
2078            if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
2079                return true;
2080            }
2081        }
2082        return false;
2083    }
2084
2085    private String encodeQuotedPrintable(final String str) {
2086        if (TextUtils.isEmpty(str)) {
2087            return "";
2088        }
2089
2090        final StringBuilder builder = new StringBuilder();
2091        int index = 0;
2092        int lineCount = 0;
2093        byte[] strArray = null;
2094
2095        try {
2096            strArray = str.getBytes(mCharset);
2097        } catch (UnsupportedEncodingException e) {
2098            Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
2099                    + "Try default charset");
2100            strArray = str.getBytes();
2101        }
2102        while (index < strArray.length) {
2103            builder.append(String.format("=%02X", strArray[index]));
2104            index += 1;
2105            lineCount += 3;
2106
2107            if (lineCount >= 67) {
2108                // Specification requires CRLF must be inserted before the
2109                // length of the line
2110                // becomes more than 76.
2111                // Assuming that the next character is a multi-byte character,
2112                // it will become
2113                // 6 bytes.
2114                // 76 - 6 - 3 = 67
2115                builder.append("=\r\n");
2116                lineCount = 0;
2117            }
2118        }
2119
2120        return builder.toString();
2121    }
2122
2123    /**
2124     * Append '\' to the characters which should be escaped. The character set is different
2125     * not only between vCard 2.1 and vCard 3.0 but also among each device.
2126     *
2127     * Note that Quoted-Printable string must not be input here.
2128     */
2129    @SuppressWarnings("fallthrough")
2130    private String escapeCharacters(final String unescaped) {
2131        if (TextUtils.isEmpty(unescaped)) {
2132            return "";
2133        }
2134
2135        final StringBuilder tmpBuilder = new StringBuilder();
2136        final int length = unescaped.length();
2137        for (int i = 0; i < length; i++) {
2138            final char ch = unescaped.charAt(i);
2139            switch (ch) {
2140                case ';': {
2141                    tmpBuilder.append('\\');
2142                    tmpBuilder.append(';');
2143                    break;
2144                }
2145                case '\r': {
2146                    if (i + 1 < length) {
2147                        char nextChar = unescaped.charAt(i);
2148                        if (nextChar == '\n') {
2149                            break;
2150                        } else {
2151                            // fall through
2152                        }
2153                    } else {
2154                        // fall through
2155                    }
2156                }
2157                case '\n': {
2158                    // In vCard 2.1, there's no specification about this, while
2159                    // vCard 3.0 explicitly requires this should be encoded to "\n".
2160                    tmpBuilder.append("\\n");
2161                    break;
2162                }
2163                case '\\': {
2164                    if (mIsV30OrV40) {
2165                        tmpBuilder.append("\\\\");
2166                        break;
2167                    } else {
2168                        // fall through
2169                    }
2170                }
2171                case '<':
2172                case '>': {
2173                    if (mIsDoCoMo) {
2174                        tmpBuilder.append('\\');
2175                        tmpBuilder.append(ch);
2176                    } else {
2177                        tmpBuilder.append(ch);
2178                    }
2179                    break;
2180                }
2181                case ',': {
2182                    if (mIsV30OrV40) {
2183                        tmpBuilder.append("\\,");
2184                    } else {
2185                        tmpBuilder.append(ch);
2186                    }
2187                    break;
2188                }
2189                default: {
2190                    tmpBuilder.append(ch);
2191                    break;
2192                }
2193            }
2194        }
2195        return tmpBuilder.toString();
2196    }
2197
2198    @Override
2199    public String toString() {
2200        if (!mEndAppended) {
2201            if (mIsDoCoMo) {
2202                appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
2203                appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
2204                appendLine(VCardConstants.PROPERTY_X_NO, "");
2205                appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
2206            }
2207            appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
2208            mEndAppended = true;
2209        }
2210        return mBuilder.toString();
2211    }
2212}
2213