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