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