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