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