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