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