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