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