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