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