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