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