OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / packages / apps / Tag / src / com / android / vcard / VCardBuilder.java
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  */
16 package com.android.vcard;
17
18 import android.content.ContentValues;
19 import android.provider.ContactsContract.CommonDataKinds.Email;
20 import android.provider.ContactsContract.CommonDataKinds.Event;
21 import android.provider.ContactsContract.CommonDataKinds.Im;
22 import android.provider.ContactsContract.CommonDataKinds.Nickname;
23 import android.provider.ContactsContract.CommonDataKinds.Note;
24 import android.provider.ContactsContract.CommonDataKinds.Organization;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.CommonDataKinds.Photo;
27 import android.provider.ContactsContract.CommonDataKinds.Relation;
28 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
30 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
31 import android.provider.ContactsContract.CommonDataKinds.Website;
32 import android.telephony.PhoneNumberUtils;
33 import android.text.TextUtils;
34 import android.util.Base64;
35 //import android.util.CharsetUtils;
36 import android.util.Log;
37
38 import java.io.UnsupportedEncodingException;
39 import java.nio.charset.UnsupportedCharsetException;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Map;
47 import 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  */
68 public 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);
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     public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
1778         final boolean useXProperty;
1779         if (mIsV30OrV40) {
1780             useXProperty = false;
1781         } else if (mUsesDefactProperty){
1782             useXProperty = true;
1783         } else {
1784             return this;
1785         }
1786
1787         if (contentValuesList != null) {
1788             for (ContentValues contentValues : contentValuesList) {
1789                 String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
1790                 if (TextUtils.isEmpty(sipAddress)) {
1791                     continue;
1792                 }
1793                 if (useXProperty) {
1794                     // X-SIP does not contain "sip:" prefix.
1795                     if (sipAddress.startsWith("sip:")) {
1796                         if (sipAddress.length() == 4) {
1797                             continue;
1798                         }
1799                         sipAddress = sipAddress.substring(4);
1800                     }
1801                     // No type is available yet.
1802                     appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
1803                 } else {
1804                     if (!sipAddress.startsWith("sip:")) {
1805                         sipAddress = "sip:" + sipAddress;
1806                     }
1807                     final String propertyName;
1808                     if (VCardConfig.isVersion40(mVCardType)) {
1809                         // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
1810                         // TEL seems appropriate but may change in the future.
1811                         propertyName = VCardConstants.PROPERTY_TEL;
1812                     } else {
1813                         // RFC 4770 (for vCard 3.0)
1814                         propertyName = VCardConstants.PROPERTY_IMPP;
1815                     }
1816                     appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
1817                 }
1818             }
1819         }
1820         return this;
1821     }
1822
1823     public void appendAndroidSpecificProperty(
1824             final String mimeType, ContentValues contentValues) {
1825         if (!sAllowedAndroidPropertySet.contains(mimeType)) {
1826             return;
1827         }
1828         final List<String> rawValueList = new ArrayList<String>();
1829         for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
1830             String value = contentValues.getAsString("data" + i);
1831             if (value == null) {
1832                 value = "";
1833             }
1834             rawValueList.add(value);
1835         }
1836
1837         boolean needCharset =
1838             (mShouldAppendCharsetParam &&
1839                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1840         boolean reallyUseQuotedPrintable =
1841             (mShouldUseQuotedPrintable &&
1842                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1843         mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
1844         if (needCharset) {
1845             mBuilder.append(VCARD_PARAM_SEPARATOR);
1846             mBuilder.append(mVCardCharsetParameter);
1847         }
1848         if (reallyUseQuotedPrintable) {
1849             mBuilder.append(VCARD_PARAM_SEPARATOR);
1850             mBuilder.append(VCARD_PARAM_ENCODING_QP);
1851         }
1852         mBuilder.append(VCARD_DATA_SEPARATOR);
1853         mBuilder.append(mimeType);  // Should not be encoded.
1854         for (String rawValue : rawValueList) {
1855             final String encodedValue;
1856             if (reallyUseQuotedPrintable) {
1857                 encodedValue = encodeQuotedPrintable(rawValue);
1858             } else {
1859                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1860                 //        (which says "When generating a content line, lines longer than
1861                 //        75 characters SHOULD be folded"), though several
1862                 //        (even well-known) applications do not care this.
1863                 encodedValue = escapeCharacters(rawValue);
1864             }
1865             mBuilder.append(VCARD_ITEM_SEPARATOR);
1866             mBuilder.append(encodedValue);
1867         }
1868         mBuilder.append(VCARD_END_OF_LINE);
1869     }
1870
1871     public void appendLineWithCharsetAndQPDetection(final String propertyName,
1872             final String rawValue) {
1873         appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
1874     }
1875
1876     public void appendLineWithCharsetAndQPDetection(
1877             final String propertyName, final List<String> rawValueList) {
1878         appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
1879     }
1880
1881     public void appendLineWithCharsetAndQPDetection(final String propertyName,
1882             final List<String> parameterList, final String rawValue) {
1883         final boolean needCharset =
1884                 !VCardUtils.containsOnlyPrintableAscii(rawValue);
1885         final boolean reallyUseQuotedPrintable =
1886                 (mShouldUseQuotedPrintable &&
1887                         !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
1888         appendLine(propertyName, parameterList,
1889                 rawValue, needCharset, reallyUseQuotedPrintable);
1890     }
1891
1892     public void appendLineWithCharsetAndQPDetection(final String propertyName,
1893             final List<String> parameterList, final List<String> rawValueList) {
1894         boolean needCharset =
1895             (mShouldAppendCharsetParam &&
1896                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1897         boolean reallyUseQuotedPrintable =
1898             (mShouldUseQuotedPrintable &&
1899                     !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1900         appendLine(propertyName, parameterList, rawValueList,
1901                 needCharset, reallyUseQuotedPrintable);
1902     }
1903
1904     /**
1905      * Appends one line with a given property name and value.  
1906      */
1907     public void appendLine(final String propertyName, final String rawValue) {
1908         appendLine(propertyName, rawValue, false, false);
1909     }
1910
1911     public void appendLine(final String propertyName, final List<String> rawValueList) {
1912         appendLine(propertyName, rawValueList, false, false);
1913     }
1914
1915     public void appendLine(final String propertyName,
1916             final String rawValue, final boolean needCharset,
1917             boolean reallyUseQuotedPrintable) {
1918         appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
1919     }
1920
1921     public void appendLine(final String propertyName, final List<String> parameterList,
1922             final String rawValue) {
1923         appendLine(propertyName, parameterList, rawValue, false, false);
1924     }
1925
1926     public void appendLine(final String propertyName, final List<String> parameterList,
1927             final String rawValue, final boolean needCharset,
1928             boolean reallyUseQuotedPrintable) {
1929         mBuilder.append(propertyName);
1930         if (parameterList != null && parameterList.size() > 0) {
1931             mBuilder.append(VCARD_PARAM_SEPARATOR);
1932             appendTypeParameters(parameterList);
1933         }
1934         if (needCharset) {
1935             mBuilder.append(VCARD_PARAM_SEPARATOR);
1936             mBuilder.append(mVCardCharsetParameter);
1937         }
1938
1939         final String encodedValue;
1940         if (reallyUseQuotedPrintable) {
1941             mBuilder.append(VCARD_PARAM_SEPARATOR);
1942             mBuilder.append(VCARD_PARAM_ENCODING_QP);
1943             encodedValue = encodeQuotedPrintable(rawValue);
1944         } else {
1945             // TODO: one line may be too huge, which may be invalid in vCard spec, though
1946             //       several (even well-known) applications do not care that violation.
1947             encodedValue = escapeCharacters(rawValue);
1948         }
1949
1950         mBuilder.append(VCARD_DATA_SEPARATOR);
1951         mBuilder.append(encodedValue);
1952         mBuilder.append(VCARD_END_OF_LINE);
1953     }
1954
1955     public void appendLine(final String propertyName, final List<String> rawValueList,
1956             final boolean needCharset, boolean needQuotedPrintable) {
1957         appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
1958     }
1959
1960     public void appendLine(final String propertyName, final List<String> parameterList,
1961             final List<String> rawValueList, final boolean needCharset,
1962             final boolean needQuotedPrintable) {
1963         mBuilder.append(propertyName);
1964         if (parameterList != null && parameterList.size() > 0) {
1965             mBuilder.append(VCARD_PARAM_SEPARATOR);
1966             appendTypeParameters(parameterList);
1967         }
1968         if (needCharset) {
1969             mBuilder.append(VCARD_PARAM_SEPARATOR);
1970             mBuilder.append(mVCardCharsetParameter);
1971         }
1972         if (needQuotedPrintable) {
1973             mBuilder.append(VCARD_PARAM_SEPARATOR);
1974             mBuilder.append(VCARD_PARAM_ENCODING_QP);
1975         }
1976
1977         mBuilder.append(VCARD_DATA_SEPARATOR);
1978         boolean first = true;
1979         for (String rawValue : rawValueList) {
1980             final String encodedValue;
1981             if (needQuotedPrintable) {
1982                 encodedValue = encodeQuotedPrintable(rawValue);
1983             } else {
1984                 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1985                 //        (which says "When generating a content line, lines longer than
1986                 //        75 characters SHOULD be folded"), though several
1987                 //        (even well-known) applications do not care this.
1988                 encodedValue = escapeCharacters(rawValue);
1989             }
1990
1991             if (first) {
1992                 first = false;
1993             } else {
1994                 mBuilder.append(VCARD_ITEM_SEPARATOR);
1995             }
1996             mBuilder.append(encodedValue);
1997         }
1998         mBuilder.append(VCARD_END_OF_LINE);
1999     }
2000
2001     /**
2002      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2003      */
2004     private void appendTypeParameters(final List<String> types) {
2005         // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
2006         // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
2007         boolean first = true;
2008         for (final String typeValue : types) {
2009             if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
2010                 final String encoded = (VCardConfig.isVersion40(mVCardType) ?
2011                         VCardUtils.toStringAsV40ParamValue(typeValue) :
2012                         VCardUtils.toStringAsV30ParamValue(typeValue));
2013                 if (TextUtils.isEmpty(encoded)) {
2014                     continue;
2015                 }
2016
2017                 if (first) {
2018                     first = false;
2019                 } else {
2020                     mBuilder.append(VCARD_PARAM_SEPARATOR);
2021                 }
2022                 appendTypeParameter(encoded);
2023             } else {  // vCard 2.1
2024                 if (!VCardUtils.isV21Word(typeValue)) {
2025                     continue;
2026                 }
2027                 if (first) {
2028                     first = false;
2029                 } else {
2030                     mBuilder.append(VCARD_PARAM_SEPARATOR);
2031                 }
2032                 appendTypeParameter(typeValue);
2033             }
2034         }
2035     }
2036
2037     /**
2038      * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2039      */
2040     private void appendTypeParameter(final String type) {
2041         appendTypeParameter(mBuilder, type);
2042     }
2043
2044     private void appendTypeParameter(final StringBuilder builder, final String type) {
2045         // Refrain from using appendType() so that "TYPE=" is not be appended when the
2046         // device is DoCoMo's (just for safety).
2047         //
2048         // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
2049         if (VCardConfig.isVersion40(mVCardType) ||
2050                 ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
2051             builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
2052         }
2053         builder.append(type);
2054     }
2055
2056     /**
2057      * Returns true when the property line should contain charset parameter
2058      * information. This method may return true even when vCard version is 3.0.
2059      *
2060      * Strictly, adding charset information is invalid in VCard 3.0.
2061      * However we'll add the info only when charset we use is not UTF-8
2062      * in vCard 3.0 format, since parser side may be able to use the charset
2063      * via this field, though we may encounter another problem by adding it.
2064      *
2065      * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
2066      * recommends UTF-8. By adding this field, parsers may be able
2067      * to know this text is NOT UTF-8 but Shift_Jis.
2068      */
2069     private boolean shouldAppendCharsetParam(String...propertyValueList) {
2070         if (!mShouldAppendCharsetParam) {
2071             return false;
2072         }
2073         for (String propertyValue : propertyValueList) {
2074             if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
2075                 return true;
2076             }
2077         }
2078         return false;
2079     }
2080
2081     private String encodeQuotedPrintable(final String str) {
2082         if (TextUtils.isEmpty(str)) {
2083             return "";
2084         }
2085
2086         final StringBuilder builder = new StringBuilder();
2087         int index = 0;
2088         int lineCount = 0;
2089         byte[] strArray = null;
2090
2091         try {
2092             strArray = str.getBytes(mCharset);
2093         } catch (UnsupportedEncodingException e) {
2094             Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
2095                     + "Try default charset");
2096             strArray = str.getBytes();
2097         }
2098         while (index < strArray.length) {
2099             builder.append(String.format("=%02X", strArray[index]));
2100             index += 1;
2101             lineCount += 3;
2102
2103             if (lineCount >= 67) {
2104                 // Specification requires CRLF must be inserted before the
2105                 // length of the line
2106                 // becomes more than 76.
2107                 // Assuming that the next character is a multi-byte character,
2108                 // it will become
2109                 // 6 bytes.
2110                 // 76 - 6 - 3 = 67
2111                 builder.append("=\r\n");
2112                 lineCount = 0;
2113             }
2114         }
2115
2116         return builder.toString();
2117     }
2118
2119     /**
2120      * Append '\' to the characters which should be escaped. The character set is different
2121      * not only between vCard 2.1 and vCard 3.0 but also among each device.
2122      *
2123      * Note that Quoted-Printable string must not be input here.
2124      */
2125     @SuppressWarnings("fallthrough")
2126     private String escapeCharacters(final String unescaped) {
2127         if (TextUtils.isEmpty(unescaped)) {
2128             return "";
2129         }
2130
2131         final StringBuilder tmpBuilder = new StringBuilder();
2132         final int length = unescaped.length();
2133         for (int i = 0; i < length; i++) {
2134             final char ch = unescaped.charAt(i);
2135             switch (ch) {
2136                 case ';': {
2137                     tmpBuilder.append('\\');
2138                     tmpBuilder.append(';');
2139                     break;
2140                 }
2141                 case '\r': {
2142                     if (i + 1 < length) {
2143                         char nextChar = unescaped.charAt(i);
2144                         if (nextChar == '\n') {
2145                             break;
2146                         } else {
2147                             // fall through
2148                         }
2149                     } else {
2150                         // fall through
2151                     }
2152                 }
2153                 case '\n': {
2154                     // In vCard 2.1, there's no specification about this, while
2155                     // vCard 3.0 explicitly requires this should be encoded to "\n".
2156                     tmpBuilder.append("\\n");
2157                     break;
2158                 }
2159                 case '\\': {
2160                     if (mIsV30OrV40) {
2161                         tmpBuilder.append("\\\\");
2162                         break;
2163                     } else {
2164                         // fall through
2165                     }
2166                 }
2167                 case '<':
2168                 case '>': {
2169                     if (mIsDoCoMo) {
2170                         tmpBuilder.append('\\');
2171                         tmpBuilder.append(ch);
2172                     } else {
2173                         tmpBuilder.append(ch);
2174                     }
2175                     break;
2176                 }
2177                 case ',': {
2178                     if (mIsV30OrV40) {
2179                         tmpBuilder.append("\\,");
2180                     } else {
2181                         tmpBuilder.append(ch);
2182                     }
2183                     break;
2184                 }
2185                 default: {
2186                     tmpBuilder.append(ch);
2187                     break;
2188                 }
2189             }
2190         }
2191         return tmpBuilder.toString();
2192     }
2193
2194     @Override
2195     public String toString() {
2196         if (!mEndAppended) {
2197             if (mIsDoCoMo) {
2198                 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
2199                 appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
2200                 appendLine(VCardConstants.PROPERTY_X_NO, "");
2201                 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
2202             }
2203             appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
2204             mEndAppended = true;
2205         }
2206         return mBuilder.toString();
2207     }
2208 }