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