2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
16 package com.android.vcard;
18 import android.content.ContentValues;
19 import android.provider.ContactsContract.CommonDataKinds.Email;
20 import android.provider.ContactsContract.CommonDataKinds.Event;
21 import android.provider.ContactsContract.CommonDataKinds.Im;
22 import android.provider.ContactsContract.CommonDataKinds.Nickname;
23 import android.provider.ContactsContract.CommonDataKinds.Note;
24 import android.provider.ContactsContract.CommonDataKinds.Organization;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.CommonDataKinds.Photo;
27 import android.provider.ContactsContract.CommonDataKinds.Relation;
28 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
30 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
31 import android.provider.ContactsContract.CommonDataKinds.Website;
32 import android.telephony.PhoneNumberUtils;
33 import android.text.TextUtils;
34 import android.util.Base64;
35 //import android.util.CharsetUtils;
36 import android.util.Log;
38 import java.io.UnsupportedEncodingException;
39 import java.nio.charset.UnsupportedCharsetException;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
51 * The class which lets users create their own vCard String. Typical usage is as follows:
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>
68 public class VCardBuilder {
69 private static final String LOG_TAG = "VCardBuilder";
71 // If you add the other element, please check all the columns are able to be
72 // converted to String.
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)));
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;
84 private static final String VCARD_DATA_VCARD = "VCARD";
85 private static final String VCARD_DATA_PUBLIC = "PUBLIC";
87 private static final String VCARD_PARAM_SEPARATOR = ";";
88 private static final String VCARD_END_OF_LINE = "\r\n";
89 private static final String VCARD_DATA_SEPARATOR = ":";
90 private static final String VCARD_ITEM_SEPARATOR = ";";
91 private static final String VCARD_WS = " ";
92 private static final String VCARD_PARAM_EQUAL = "=";
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;
101 private static final String SHIFT_JIS = "SHIFT_JIS";
103 private final int mVCardType;
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;
116 private final boolean mShouldAppendCharsetParam;
118 private final String mCharset;
119 private final String mVCardCharsetParameter;
121 private StringBuilder mBuilder;
122 private boolean mEndAppended;
124 public VCardBuilder(final int vcardType) {
125 // Default charset should be used
126 this(vcardType, null);
131 * @param charset If null, we use default charset for export.
134 public VCardBuilder(final int vcardType, String charset) {
135 mVCardType = vcardType;
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.");
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);
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));
160 if (VCardConfig.isDoCoMo(vcardType)) {
161 if (!SHIFT_JIS.equalsIgnoreCase(charset)) {
163 "The charset \"" + charset + "\" is used while "
164 + SHIFT_JIS + " is needed to be used."); */
165 if (TextUtils.isEmpty(charset)) {
166 mCharset = SHIFT_JIS;
169 charset = CharsetUtils.charsetForVendor(charset).name();
170 } catch (UnsupportedCharsetException e) {
172 "Career-specific \"" + charset + "\" was not found (as usual). "
180 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
181 } catch (UnsupportedCharsetException e) {
183 "DoCoMo-specific SHIFT_JIS was not found. "
184 + "Use SHIFT_JIS as is.");
189 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
190 } catch (UnsupportedCharsetException e) {
192 "Career-specific SHIFT_JIS was not found. "
193 + "Use SHIFT_JIS as is.");
199 mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
201 if (TextUtils.isEmpty(charset)) {
203 "Use the charset \"" + VCardConfig.DEFAULT_EXPORT_CHARSET
205 mCharset = VCardConfig.DEFAULT_EXPORT_CHARSET;
206 mVCardCharsetParameter = "CHARSET=" + VCardConfig.DEFAULT_EXPORT_CHARSET;
209 charset = CharsetUtils.charsetForVendor(charset).name();
210 } catch (UnsupportedCharsetException e) {
212 "Career-specific \"" + charset + "\" was not found (as usual). "
216 mVCardCharsetParameter = "CHARSET=" + charset;
222 public void clear() {
223 mBuilder = new StringBuilder();
224 mEndAppended = false;
225 appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
226 if (VCardConfig.isVersion40(mVCardType)) {
227 appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V40);
228 } else if (VCardConfig.isVersion30(mVCardType)) {
229 appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30);
231 if (!VCardConfig.isVersion21(mVCardType)) {
232 Log.w(LOG_TAG, "Unknown vCard version detected.");
234 appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21);
238 private boolean containsNonEmptyName(final ContentValues contentValues) {
239 final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
240 final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
241 final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
242 final String prefix = contentValues.getAsString(StructuredName.PREFIX);
243 final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
244 final String phoneticFamilyName =
245 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
246 final String phoneticMiddleName =
247 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
248 final String phoneticGivenName =
249 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
250 final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
251 return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
252 TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
253 TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
254 TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
255 TextUtils.isEmpty(displayName));
258 private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) {
259 ContentValues primaryContentValues = null;
260 ContentValues subprimaryContentValues = null;
261 for (ContentValues contentValues : contentValuesList) {
262 if (contentValues == null){
265 Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
266 if (isSuperPrimary != null && isSuperPrimary > 0) {
267 // We choose "super primary" ContentValues.
268 primaryContentValues = contentValues;
270 } else if (primaryContentValues == null) {
271 // We choose the first "primary" ContentValues
272 // if "super primary" ContentValues does not exist.
273 final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
274 if (isPrimary != null && isPrimary > 0 &&
275 containsNonEmptyName(contentValues)) {
276 primaryContentValues = contentValues;
277 // Do not break, since there may be ContentValues with "super primary"
279 } else if (subprimaryContentValues == null &&
280 containsNonEmptyName(contentValues)) {
281 subprimaryContentValues = contentValues;
286 if (primaryContentValues == null) {
287 if (subprimaryContentValues != null) {
288 // We choose the first ContentValues if any "primary" ContentValues does not exist.
289 primaryContentValues = subprimaryContentValues;
291 Log.e(LOG_TAG, "All ContentValues given from database is empty.");
292 primaryContentValues = new ContentValues();
296 return primaryContentValues;
300 * To avoid unnecessary complication in logic, we use this method to construct N, FN
301 * properties for vCard 4.0.
303 private VCardBuilder appendNamePropertiesV40(final List<ContentValues> contentValuesList) {
304 if (mIsDoCoMo || mNeedsToConvertPhoneticString) {
305 // Ignore all flags that look stale from the view of vCard 4.0 to
306 // simplify construction algorithm. Actually we don't have any vCard file
307 // available from real world yet, so we may need to re-enable some of these
309 Log.w(LOG_TAG, "Invalid flag is used in vCard 4.0 construction. Ignored.");
312 if (contentValuesList == null || contentValuesList.isEmpty()) {
313 appendLine(VCardConstants.PROPERTY_FN, "");
317 // We have difficulty here. How can we appropriately handle StructuredName with
318 // missing parts necessary for displaying while it has suppremental information.
320 // e.g. How to handle non-empty phonetic names with empty structured names?
322 final ContentValues contentValues = getPrimaryContentValue(contentValuesList);
323 String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
324 final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
325 final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
326 final String prefix = contentValues.getAsString(StructuredName.PREFIX);
327 final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
328 final String formattedName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
329 if (TextUtils.isEmpty(familyName)
330 && TextUtils.isEmpty(givenName)
331 && TextUtils.isEmpty(middleName)
332 && TextUtils.isEmpty(prefix)
333 && TextUtils.isEmpty(suffix)) {
334 if (TextUtils.isEmpty(formattedName)) {
335 appendLine(VCardConstants.PROPERTY_FN, "");
338 familyName = formattedName;
341 final String phoneticFamilyName =
342 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
343 final String phoneticMiddleName =
344 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
345 final String phoneticGivenName =
346 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
347 final String escapedFamily = escapeCharacters(familyName);
348 final String escapedGiven = escapeCharacters(givenName);
349 final String escapedMiddle = escapeCharacters(middleName);
350 final String escapedPrefix = escapeCharacters(prefix);
351 final String escapedSuffix = escapeCharacters(suffix);
353 mBuilder.append(VCardConstants.PROPERTY_N);
355 if (!(TextUtils.isEmpty(phoneticFamilyName) &&
356 TextUtils.isEmpty(phoneticMiddleName) &&
357 TextUtils.isEmpty(phoneticGivenName))) {
358 mBuilder.append(VCARD_PARAM_SEPARATOR);
359 final String sortAs = escapeCharacters(phoneticFamilyName)
360 + ';' + escapeCharacters(phoneticGivenName)
361 + ';' + escapeCharacters(phoneticMiddleName);
362 mBuilder.append("SORT-AS=").append(
363 VCardUtils.toStringAsV40ParamValue(sortAs));
366 mBuilder.append(VCARD_DATA_SEPARATOR);
367 mBuilder.append(escapedFamily);
368 mBuilder.append(VCARD_ITEM_SEPARATOR);
369 mBuilder.append(escapedGiven);
370 mBuilder.append(VCARD_ITEM_SEPARATOR);
371 mBuilder.append(escapedMiddle);
372 mBuilder.append(VCARD_ITEM_SEPARATOR);
373 mBuilder.append(escapedPrefix);
374 mBuilder.append(VCARD_ITEM_SEPARATOR);
375 mBuilder.append(escapedSuffix);
376 mBuilder.append(VCARD_END_OF_LINE);
378 if (TextUtils.isEmpty(formattedName)) {
380 // DISPLAY_NAME doesn't exist while some other elements do, which is usually
381 // weird in Android, as DISPLAY_NAME should (usually) be constructed
382 // from the others using locale information and its code points.
383 Log.w(LOG_TAG, "DISPLAY_NAME is empty.");
385 final String escaped = escapeCharacters(VCardUtils.constructNameFromElements(
386 VCardConfig.getNameOrderType(mVCardType),
387 familyName, middleName, givenName, prefix, suffix));
388 appendLine(VCardConstants.PROPERTY_FN, escaped);
390 final String escapedFormatted = escapeCharacters(formattedName);
391 mBuilder.append(VCardConstants.PROPERTY_FN);
392 mBuilder.append(VCARD_DATA_SEPARATOR);
393 mBuilder.append(escapedFormatted);
394 mBuilder.append(VCARD_END_OF_LINE);
397 // We may need X- properties for phonetic names.
398 appendPhoneticNameFields(contentValues);
403 * For safety, we'll emit just one value around StructuredName, as external importers
404 * may get confused with multiple "N", "FN", etc. properties, though it is valid in
407 public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) {
408 if (VCardConfig.isVersion40(mVCardType)) {
409 return appendNamePropertiesV40(contentValuesList);
412 if (contentValuesList == null || contentValuesList.isEmpty()) {
413 if (VCardConfig.isVersion30(mVCardType)) {
414 // vCard 3.0 requires "N" and "FN" properties.
415 // vCard 4.0 does NOT require N, but we take care of possible backward
416 // compatibility issues.
417 appendLine(VCardConstants.PROPERTY_N, "");
418 appendLine(VCardConstants.PROPERTY_FN, "");
419 } else if (mIsDoCoMo) {
420 appendLine(VCardConstants.PROPERTY_N, "");
425 final ContentValues contentValues = getPrimaryContentValue(contentValuesList);
426 final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
427 final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
428 final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
429 final String prefix = contentValues.getAsString(StructuredName.PREFIX);
430 final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
431 final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
433 if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
434 final boolean reallyAppendCharsetParameterToName =
435 shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix);
436 final boolean reallyUseQuotedPrintableToName =
437 (!mRefrainsQPToNameProperties &&
438 !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
439 VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
440 VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
441 VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
442 VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
444 final String formattedName;
445 if (!TextUtils.isEmpty(displayName)) {
446 formattedName = displayName;
448 formattedName = VCardUtils.constructNameFromElements(
449 VCardConfig.getNameOrderType(mVCardType),
450 familyName, middleName, givenName, prefix, suffix);
452 final boolean reallyAppendCharsetParameterToFN =
453 shouldAppendCharsetParam(formattedName);
454 final boolean reallyUseQuotedPrintableToFN =
455 !mRefrainsQPToNameProperties &&
456 !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
458 final String encodedFamily;
459 final String encodedGiven;
460 final String encodedMiddle;
461 final String encodedPrefix;
462 final String encodedSuffix;
463 if (reallyUseQuotedPrintableToName) {
464 encodedFamily = encodeQuotedPrintable(familyName);
465 encodedGiven = encodeQuotedPrintable(givenName);
466 encodedMiddle = encodeQuotedPrintable(middleName);
467 encodedPrefix = encodeQuotedPrintable(prefix);
468 encodedSuffix = encodeQuotedPrintable(suffix);
470 encodedFamily = escapeCharacters(familyName);
471 encodedGiven = escapeCharacters(givenName);
472 encodedMiddle = escapeCharacters(middleName);
473 encodedPrefix = escapeCharacters(prefix);
474 encodedSuffix = escapeCharacters(suffix);
477 final String encodedFormattedname =
478 (reallyUseQuotedPrintableToFN ?
479 encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
481 mBuilder.append(VCardConstants.PROPERTY_N);
483 if (reallyAppendCharsetParameterToName) {
484 mBuilder.append(VCARD_PARAM_SEPARATOR);
485 mBuilder.append(mVCardCharsetParameter);
487 if (reallyUseQuotedPrintableToName) {
488 mBuilder.append(VCARD_PARAM_SEPARATOR);
489 mBuilder.append(VCARD_PARAM_ENCODING_QP);
491 mBuilder.append(VCARD_DATA_SEPARATOR);
492 // DoCoMo phones require that all the elements in the "family name" field.
493 mBuilder.append(formattedName);
494 mBuilder.append(VCARD_ITEM_SEPARATOR);
495 mBuilder.append(VCARD_ITEM_SEPARATOR);
496 mBuilder.append(VCARD_ITEM_SEPARATOR);
497 mBuilder.append(VCARD_ITEM_SEPARATOR);
499 if (reallyAppendCharsetParameterToName) {
500 mBuilder.append(VCARD_PARAM_SEPARATOR);
501 mBuilder.append(mVCardCharsetParameter);
503 if (reallyUseQuotedPrintableToName) {
504 mBuilder.append(VCARD_PARAM_SEPARATOR);
505 mBuilder.append(VCARD_PARAM_ENCODING_QP);
507 mBuilder.append(VCARD_DATA_SEPARATOR);
508 mBuilder.append(encodedFamily);
509 mBuilder.append(VCARD_ITEM_SEPARATOR);
510 mBuilder.append(encodedGiven);
511 mBuilder.append(VCARD_ITEM_SEPARATOR);
512 mBuilder.append(encodedMiddle);
513 mBuilder.append(VCARD_ITEM_SEPARATOR);
514 mBuilder.append(encodedPrefix);
515 mBuilder.append(VCARD_ITEM_SEPARATOR);
516 mBuilder.append(encodedSuffix);
518 mBuilder.append(VCARD_END_OF_LINE);
521 mBuilder.append(VCardConstants.PROPERTY_FN);
522 if (reallyAppendCharsetParameterToFN) {
523 mBuilder.append(VCARD_PARAM_SEPARATOR);
524 mBuilder.append(mVCardCharsetParameter);
526 if (reallyUseQuotedPrintableToFN) {
527 mBuilder.append(VCARD_PARAM_SEPARATOR);
528 mBuilder.append(VCARD_PARAM_ENCODING_QP);
530 mBuilder.append(VCARD_DATA_SEPARATOR);
531 mBuilder.append(encodedFormattedname);
532 mBuilder.append(VCARD_END_OF_LINE);
533 } else if (!TextUtils.isEmpty(displayName)) {
534 final boolean reallyUseQuotedPrintableToDisplayName =
535 (!mRefrainsQPToNameProperties &&
536 !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
537 final String encodedDisplayName =
538 reallyUseQuotedPrintableToDisplayName ?
539 encodeQuotedPrintable(displayName) :
540 escapeCharacters(displayName);
543 mBuilder.append(VCardConstants.PROPERTY_N);
544 if (shouldAppendCharsetParam(displayName)) {
545 mBuilder.append(VCARD_PARAM_SEPARATOR);
546 mBuilder.append(mVCardCharsetParameter);
548 if (reallyUseQuotedPrintableToDisplayName) {
549 mBuilder.append(VCARD_PARAM_SEPARATOR);
550 mBuilder.append(VCARD_PARAM_ENCODING_QP);
552 mBuilder.append(VCARD_DATA_SEPARATOR);
553 mBuilder.append(encodedDisplayName);
554 mBuilder.append(VCARD_ITEM_SEPARATOR);
555 mBuilder.append(VCARD_ITEM_SEPARATOR);
556 mBuilder.append(VCARD_ITEM_SEPARATOR);
557 mBuilder.append(VCARD_ITEM_SEPARATOR);
558 mBuilder.append(VCARD_END_OF_LINE);
561 mBuilder.append(VCardConstants.PROPERTY_FN);
563 // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
564 // when it would be useful or necessary for external importers,
565 // assuming the external importer allows this vioration of the spec.
566 if (shouldAppendCharsetParam(displayName)) {
567 mBuilder.append(VCARD_PARAM_SEPARATOR);
568 mBuilder.append(mVCardCharsetParameter);
570 mBuilder.append(VCARD_DATA_SEPARATOR);
571 mBuilder.append(encodedDisplayName);
572 mBuilder.append(VCARD_END_OF_LINE);
573 } else if (VCardConfig.isVersion30(mVCardType)) {
574 appendLine(VCardConstants.PROPERTY_N, "");
575 appendLine(VCardConstants.PROPERTY_FN, "");
576 } else if (mIsDoCoMo) {
577 appendLine(VCardConstants.PROPERTY_N, "");
580 appendPhoneticNameFields(contentValues);
585 * Emits SOUND;IRMC, SORT-STRING, and de-fact values for phonetic names like X-PHONETIC-FAMILY.
587 private void appendPhoneticNameFields(final ContentValues contentValues) {
588 final String phoneticFamilyName;
589 final String phoneticMiddleName;
590 final String phoneticGivenName;
592 final String tmpPhoneticFamilyName =
593 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
594 final String tmpPhoneticMiddleName =
595 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
596 final String tmpPhoneticGivenName =
597 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
598 if (mNeedsToConvertPhoneticString) {
599 phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
600 phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
601 phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
603 phoneticFamilyName = tmpPhoneticFamilyName;
604 phoneticMiddleName = tmpPhoneticMiddleName;
605 phoneticGivenName = tmpPhoneticGivenName;
609 if (TextUtils.isEmpty(phoneticFamilyName)
610 && TextUtils.isEmpty(phoneticMiddleName)
611 && TextUtils.isEmpty(phoneticGivenName)) {
613 mBuilder.append(VCardConstants.PROPERTY_SOUND);
614 mBuilder.append(VCARD_PARAM_SEPARATOR);
615 mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
616 mBuilder.append(VCARD_DATA_SEPARATOR);
617 mBuilder.append(VCARD_ITEM_SEPARATOR);
618 mBuilder.append(VCARD_ITEM_SEPARATOR);
619 mBuilder.append(VCARD_ITEM_SEPARATOR);
620 mBuilder.append(VCARD_ITEM_SEPARATOR);
621 mBuilder.append(VCARD_END_OF_LINE);
626 if (VCardConfig.isVersion40(mVCardType)) {
627 // We don't want SORT-STRING anyway.
628 } else if (VCardConfig.isVersion30(mVCardType)) {
629 final String sortString =
630 VCardUtils.constructNameFromElements(mVCardType,
631 phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
632 mBuilder.append(VCardConstants.PROPERTY_SORT_STRING);
633 if (VCardConfig.isVersion30(mVCardType) && shouldAppendCharsetParam(sortString)) {
634 // vCard 3.0 does not force us to use UTF-8 and actually we see some
635 // programs which emit this value. It is incorrect from the view of
636 // specification, but actually necessary for parsing vCard with non-UTF-8
637 // charsets, expecting other parsers not get confused with this value.
638 mBuilder.append(VCARD_PARAM_SEPARATOR);
639 mBuilder.append(mVCardCharsetParameter);
641 mBuilder.append(VCARD_DATA_SEPARATOR);
642 mBuilder.append(escapeCharacters(sortString));
643 mBuilder.append(VCARD_END_OF_LINE);
644 } else if (mIsJapaneseMobilePhone) {
645 // Note: There is no appropriate property for expressing
646 // phonetic name (Yomigana in Japanese) in vCard 2.1, while there is in
647 // vCard 3.0 (SORT-STRING).
648 // We use DoCoMo's way when the device is Japanese one since it is already
649 // supported by a lot of Japanese mobile phones.
650 // This is "X-" property, so any parser hopefully would not get
651 // confused with this.
653 // Also, DoCoMo's specification requires vCard composer to use just the first
656 // good: SOUND;X-IRMC-N:Miyakawa Daisuke;;;;
657 // bad : SOUND;X-IRMC-N:Miyakawa;Daisuke;;;
658 mBuilder.append(VCardConstants.PROPERTY_SOUND);
659 mBuilder.append(VCARD_PARAM_SEPARATOR);
660 mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N);
662 boolean reallyUseQuotedPrintable =
663 (!mRefrainsQPToNameProperties
664 && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
666 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
668 && VCardUtils.containsOnlyNonCrLfPrintableAscii(
669 phoneticGivenName)));
671 final String encodedPhoneticFamilyName;
672 final String encodedPhoneticMiddleName;
673 final String encodedPhoneticGivenName;
674 if (reallyUseQuotedPrintable) {
675 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
676 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
677 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
679 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
680 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
681 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
684 if (shouldAppendCharsetParam(encodedPhoneticFamilyName,
685 encodedPhoneticMiddleName, encodedPhoneticGivenName)) {
686 mBuilder.append(VCARD_PARAM_SEPARATOR);
687 mBuilder.append(mVCardCharsetParameter);
689 mBuilder.append(VCARD_DATA_SEPARATOR);
691 boolean first = true;
692 if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
693 mBuilder.append(encodedPhoneticFamilyName);
696 if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
700 mBuilder.append(' ');
702 mBuilder.append(encodedPhoneticMiddleName);
704 if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
706 mBuilder.append(' ');
708 mBuilder.append(encodedPhoneticGivenName);
711 mBuilder.append(VCARD_ITEM_SEPARATOR); // family;given
712 mBuilder.append(VCARD_ITEM_SEPARATOR); // given;middle
713 mBuilder.append(VCARD_ITEM_SEPARATOR); // middle;prefix
714 mBuilder.append(VCARD_ITEM_SEPARATOR); // prefix;suffix
715 mBuilder.append(VCARD_END_OF_LINE);
718 if (mUsesDefactProperty) {
719 if (!TextUtils.isEmpty(phoneticGivenName)) {
720 final boolean reallyUseQuotedPrintable =
721 (mShouldUseQuotedPrintable &&
722 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
723 final String encodedPhoneticGivenName;
724 if (reallyUseQuotedPrintable) {
725 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
727 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
729 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME);
730 if (shouldAppendCharsetParam(phoneticGivenName)) {
731 mBuilder.append(VCARD_PARAM_SEPARATOR);
732 mBuilder.append(mVCardCharsetParameter);
734 if (reallyUseQuotedPrintable) {
735 mBuilder.append(VCARD_PARAM_SEPARATOR);
736 mBuilder.append(VCARD_PARAM_ENCODING_QP);
738 mBuilder.append(VCARD_DATA_SEPARATOR);
739 mBuilder.append(encodedPhoneticGivenName);
740 mBuilder.append(VCARD_END_OF_LINE);
741 } // if (!TextUtils.isEmpty(phoneticGivenName))
742 if (!TextUtils.isEmpty(phoneticMiddleName)) {
743 final boolean reallyUseQuotedPrintable =
744 (mShouldUseQuotedPrintable &&
745 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
746 final String encodedPhoneticMiddleName;
747 if (reallyUseQuotedPrintable) {
748 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
750 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
752 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
753 if (shouldAppendCharsetParam(phoneticMiddleName)) {
754 mBuilder.append(VCARD_PARAM_SEPARATOR);
755 mBuilder.append(mVCardCharsetParameter);
757 if (reallyUseQuotedPrintable) {
758 mBuilder.append(VCARD_PARAM_SEPARATOR);
759 mBuilder.append(VCARD_PARAM_ENCODING_QP);
761 mBuilder.append(VCARD_DATA_SEPARATOR);
762 mBuilder.append(encodedPhoneticMiddleName);
763 mBuilder.append(VCARD_END_OF_LINE);
764 } // if (!TextUtils.isEmpty(phoneticGivenName))
765 if (!TextUtils.isEmpty(phoneticFamilyName)) {
766 final boolean reallyUseQuotedPrintable =
767 (mShouldUseQuotedPrintable &&
768 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
769 final String encodedPhoneticFamilyName;
770 if (reallyUseQuotedPrintable) {
771 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
773 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
775 mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME);
776 if (shouldAppendCharsetParam(phoneticFamilyName)) {
777 mBuilder.append(VCARD_PARAM_SEPARATOR);
778 mBuilder.append(mVCardCharsetParameter);
780 if (reallyUseQuotedPrintable) {
781 mBuilder.append(VCARD_PARAM_SEPARATOR);
782 mBuilder.append(VCARD_PARAM_ENCODING_QP);
784 mBuilder.append(VCARD_DATA_SEPARATOR);
785 mBuilder.append(encodedPhoneticFamilyName);
786 mBuilder.append(VCARD_END_OF_LINE);
787 } // if (!TextUtils.isEmpty(phoneticFamilyName))
791 public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) {
792 final boolean useAndroidProperty;
793 if (mIsV30OrV40) { // These specifications have NICKNAME property.
794 useAndroidProperty = false;
795 } else if (mUsesAndroidProperty) {
796 useAndroidProperty = true;
798 // There's no way to add this field.
801 if (contentValuesList != null) {
802 for (ContentValues contentValues : contentValuesList) {
803 final String nickname = contentValues.getAsString(Nickname.NAME);
804 if (TextUtils.isEmpty(nickname)) {
807 if (useAndroidProperty) {
808 appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues);
810 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname);
817 public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) {
818 boolean phoneLineExists = false;
819 if (contentValuesList != null) {
820 Set<String> phoneSet = new HashSet<String>();
821 for (ContentValues contentValues : contentValuesList) {
822 final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
823 final String label = contentValues.getAsString(Phone.LABEL);
824 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
825 final boolean isPrimary = (isPrimaryAsInteger != null ?
826 (isPrimaryAsInteger > 0) : false);
827 String phoneNumber = contentValues.getAsString(Phone.NUMBER);
828 if (phoneNumber != null) {
829 phoneNumber = phoneNumber.trim();
831 if (TextUtils.isEmpty(phoneNumber)) {
835 // PAGER number needs unformatted "phone number".
836 // TODO: It would be better to have this logic as optional.
837 final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
838 if (type == Phone.TYPE_PAGER ||
839 VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
840 phoneLineExists = true;
841 if (!phoneSet.contains(phoneNumber)) {
842 phoneSet.add(phoneNumber);
843 appendTelLine(type, label, phoneNumber, isPrimary);
846 final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber);
847 if (phoneNumberList.isEmpty()) {
850 phoneLineExists = true;
851 for (String actualPhoneNumber : phoneNumberList) {
852 if (!phoneSet.contains(actualPhoneNumber)) {
853 final int phoneFormat = VCardUtils.getPhoneNumberFormat(mVCardType);
855 PhoneNumberUtils.formatNumber(actualPhoneNumber);
857 // In vCard 4.0, value type must be "a single URI value",
858 // not just a phone number. (Based on vCard 4.0 rev.13)
859 if (VCardConfig.isVersion40(mVCardType)
860 && !TextUtils.isEmpty(formatted)
861 && !formatted.startsWith("tel:")) {
862 formatted = "tel:" + formatted;
865 // Pre-formatted string should be stored.
866 phoneSet.add(actualPhoneNumber);
867 appendTelLine(type, label, formatted, isPrimary);
869 } // for (String actualPhoneNumber : phoneNumberList) {
871 // TODO: TEL with SIP URI?
876 if (!phoneLineExists && mIsDoCoMo) {
877 appendTelLine(Phone.TYPE_HOME, "", "", false);
885 * Splits a given string expressing phone numbers into several strings, and remove
886 * unnecessary characters inside them. The size of a returned list becomes 1 when
887 * no split is needed.
890 * The given number "may" have several phone numbers when the contact entry is corrupted
891 * because of its original source.
892 * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)"
895 * This kind of "phone numbers" will not be created with Android vCard implementation,
896 * but we may encounter them if the source of the input data has already corrupted
900 * To handle this case, this method first splits its input into multiple parts
901 * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and
902 * removes unnecessary strings like "(Miami)".
905 * Do not call this method when trimming is inappropriate for its receivers.
908 private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) {
909 final List<String> phoneList = new ArrayList<String>();
911 StringBuilder builder = new StringBuilder();
912 final int length = phoneNumber.length();
913 for (int i = 0; i < length; i++) {
914 final char ch = phoneNumber.charAt(i);
915 if (Character.isDigit(ch) || ch == '+') {
917 } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
918 phoneList.add(builder.toString());
919 builder = new StringBuilder();
922 if (builder.length() > 0) {
923 phoneList.add(builder.toString());
929 public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) {
930 boolean emailAddressExists = false;
931 if (contentValuesList != null) {
932 final Set<String> addressSet = new HashSet<String>();
933 for (ContentValues contentValues : contentValuesList) {
934 String emailAddress = contentValues.getAsString(Email.DATA);
935 if (emailAddress != null) {
936 emailAddress = emailAddress.trim();
938 if (TextUtils.isEmpty(emailAddress)) {
941 Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
942 final int type = (typeAsObject != null ?
943 typeAsObject : DEFAULT_EMAIL_TYPE);
944 final String label = contentValues.getAsString(Email.LABEL);
945 Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
946 final boolean isPrimary = (isPrimaryAsInteger != null ?
947 (isPrimaryAsInteger > 0) : false);
948 emailAddressExists = true;
949 if (!addressSet.contains(emailAddress)) {
950 addressSet.add(emailAddress);
951 appendEmailLine(type, label, emailAddress, isPrimary);
956 if (!emailAddressExists && mIsDoCoMo) {
957 appendEmailLine(Email.TYPE_HOME, "", "", false);
963 public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) {
964 if (contentValuesList == null || contentValuesList.isEmpty()) {
966 mBuilder.append(VCardConstants.PROPERTY_ADR);
967 mBuilder.append(VCARD_PARAM_SEPARATOR);
968 mBuilder.append(VCardConstants.PARAM_TYPE_HOME);
969 mBuilder.append(VCARD_DATA_SEPARATOR);
970 mBuilder.append(VCARD_END_OF_LINE);
974 appendPostalsForDoCoMo(contentValuesList);
976 appendPostalsForGeneric(contentValuesList);
983 private static final Map<Integer, Integer> sPostalTypePriorityMap;
986 sPostalTypePriorityMap = new HashMap<Integer, Integer>();
987 sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0);
988 sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1);
989 sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2);
990 sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3);
994 * Tries to append just one line. If there's no appropriate address
995 * information, append an empty line.
997 private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) {
998 int currentPriority = Integer.MAX_VALUE;
999 int currentType = Integer.MAX_VALUE;
1000 ContentValues currentContentValues = null;
1001 for (final ContentValues contentValues : contentValuesList) {
1002 if (contentValues == null) {
1005 final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
1006 final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger);
1007 final int priority =
1008 (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE);
1009 if (priority < currentPriority) {
1010 currentPriority = priority;
1011 currentType = typeAsInteger;
1012 currentContentValues = contentValues;
1013 if (priority == 0) {
1019 if (currentContentValues == null) {
1020 Log.w(LOG_TAG, "Should not come here. Must have at least one postal data.");
1024 final String label = currentContentValues.getAsString(StructuredPostal.LABEL);
1025 appendPostalLine(currentType, label, currentContentValues, false, true);
1028 private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) {
1029 for (final ContentValues contentValues : contentValuesList) {
1030 if (contentValues == null) {
1033 final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE);
1034 final int type = (typeAsInteger != null ?
1035 typeAsInteger : DEFAULT_POSTAL_TYPE);
1036 final String label = contentValues.getAsString(StructuredPostal.LABEL);
1037 final Integer isPrimaryAsInteger =
1038 contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
1039 final boolean isPrimary = (isPrimaryAsInteger != null ?
1040 (isPrimaryAsInteger > 0) : false);
1041 appendPostalLine(type, label, contentValues, isPrimary, false);
1045 private static class PostalStruct {
1046 final boolean reallyUseQuotedPrintable;
1047 final boolean appendCharset;
1048 final String addressData;
1049 public PostalStruct(final boolean reallyUseQuotedPrintable,
1050 final boolean appendCharset, final String addressData) {
1051 this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
1052 this.appendCharset = appendCharset;
1053 this.addressData = addressData;
1058 * @return null when there's no information available to construct the data.
1060 private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
1061 // adr-value = 0*6(text-value ";") text-value
1062 // ; PO Box, Extended Address, Street, Locality, Region, Postal
1063 // ; Code, Country Name
1064 final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX);
1065 final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD);
1066 final String rawStreet = contentValues.getAsString(StructuredPostal.STREET);
1067 final String rawLocality = contentValues.getAsString(StructuredPostal.CITY);
1068 final String rawRegion = contentValues.getAsString(StructuredPostal.REGION);
1069 final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE);
1070 final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY);
1071 final String[] rawAddressArray = new String[]{
1072 rawPoBox, rawNeighborhood, rawStreet, rawLocality,
1073 rawRegion, rawPostalCode, rawCountry};
1074 if (!VCardUtils.areAllEmpty(rawAddressArray)) {
1075 final boolean reallyUseQuotedPrintable =
1076 (mShouldUseQuotedPrintable &&
1077 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray));
1078 final boolean appendCharset =
1079 !VCardUtils.containsOnlyPrintableAscii(rawAddressArray);
1080 final String encodedPoBox;
1081 final String encodedStreet;
1082 final String encodedLocality;
1083 final String encodedRegion;
1084 final String encodedPostalCode;
1085 final String encodedCountry;
1086 final String encodedNeighborhood;
1088 final String rawLocality2;
1089 // This looks inefficient since we encode rawLocality and rawNeighborhood twice,
1090 // but this is intentional.
1092 // QP encoding may add line feeds when needed and the result of
1093 // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood)
1094 // may be different from
1095 // - encodedLocality + " " + encodedNeighborhood.
1097 // We use safer way.
1098 if (TextUtils.isEmpty(rawLocality)) {
1099 if (TextUtils.isEmpty(rawNeighborhood)) {
1102 rawLocality2 = rawNeighborhood;
1105 if (TextUtils.isEmpty(rawNeighborhood)) {
1106 rawLocality2 = rawLocality;
1108 rawLocality2 = rawLocality + " " + rawNeighborhood;
1111 if (reallyUseQuotedPrintable) {
1112 encodedPoBox = encodeQuotedPrintable(rawPoBox);
1113 encodedStreet = encodeQuotedPrintable(rawStreet);
1114 encodedLocality = encodeQuotedPrintable(rawLocality2);
1115 encodedRegion = encodeQuotedPrintable(rawRegion);
1116 encodedPostalCode = encodeQuotedPrintable(rawPostalCode);
1117 encodedCountry = encodeQuotedPrintable(rawCountry);
1119 encodedPoBox = escapeCharacters(rawPoBox);
1120 encodedStreet = escapeCharacters(rawStreet);
1121 encodedLocality = escapeCharacters(rawLocality2);
1122 encodedRegion = escapeCharacters(rawRegion);
1123 encodedPostalCode = escapeCharacters(rawPostalCode);
1124 encodedCountry = escapeCharacters(rawCountry);
1125 encodedNeighborhood = escapeCharacters(rawNeighborhood);
1127 final StringBuilder addressBuilder = new StringBuilder();
1128 addressBuilder.append(encodedPoBox);
1129 addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address
1130 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street
1131 addressBuilder.append(encodedStreet);
1132 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality
1133 addressBuilder.append(encodedLocality);
1134 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region
1135 addressBuilder.append(encodedRegion);
1136 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code
1137 addressBuilder.append(encodedPostalCode);
1138 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country
1139 addressBuilder.append(encodedCountry);
1140 return new PostalStruct(
1141 reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
1142 } else { // VCardUtils.areAllEmpty(rawAddressArray) == true
1143 // Try to use FORMATTED_ADDRESS instead.
1144 final String rawFormattedAddress =
1145 contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1146 if (TextUtils.isEmpty(rawFormattedAddress)) {
1149 final boolean reallyUseQuotedPrintable =
1150 (mShouldUseQuotedPrintable &&
1151 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress));
1152 final boolean appendCharset =
1153 !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress);
1154 final String encodedFormattedAddress;
1155 if (reallyUseQuotedPrintable) {
1156 encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress);
1158 encodedFormattedAddress = escapeCharacters(rawFormattedAddress);
1161 // We use the second value ("Extended Address") just because Japanese mobile phones
1162 // do so. If the other importer expects the value be in the other field, some flag may
1164 final StringBuilder addressBuilder = new StringBuilder();
1165 addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address
1166 addressBuilder.append(encodedFormattedAddress);
1167 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street
1168 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality
1169 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region
1170 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code
1171 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country
1172 return new PostalStruct(
1173 reallyUseQuotedPrintable, appendCharset, addressBuilder.toString());
1177 public VCardBuilder appendIms(final List<ContentValues> contentValuesList) {
1178 if (contentValuesList != null) {
1179 for (ContentValues contentValues : contentValuesList) {
1180 final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
1181 if (protocolAsObject == null) {
1184 final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
1185 if (propertyName == null) {
1188 String data = contentValues.getAsString(Im.DATA);
1192 if (TextUtils.isEmpty(data)) {
1195 final String typeAsString;
1197 final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
1198 switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
1199 case Im.TYPE_HOME: {
1200 typeAsString = VCardConstants.PARAM_TYPE_HOME;
1203 case Im.TYPE_WORK: {
1204 typeAsString = VCardConstants.PARAM_TYPE_WORK;
1207 case Im.TYPE_CUSTOM: {
1208 final String label = contentValues.getAsString(Im.LABEL);
1209 typeAsString = (label != null ? "X-" + label : null);
1212 case Im.TYPE_OTHER: // Ignore
1214 typeAsString = null;
1220 final List<String> parameterList = new ArrayList<String>();
1221 if (!TextUtils.isEmpty(typeAsString)) {
1222 parameterList.add(typeAsString);
1224 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
1225 final boolean isPrimary = (isPrimaryAsInteger != null ?
1226 (isPrimaryAsInteger > 0) : false);
1228 parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1231 appendLineWithCharsetAndQPDetection(propertyName, parameterList, data);
1237 public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) {
1238 if (contentValuesList != null) {
1239 for (ContentValues contentValues : contentValuesList) {
1240 String website = contentValues.getAsString(Website.URL);
1241 if (website != null) {
1242 website = website.trim();
1245 // Note: vCard 3.0 does not allow any parameter addition toward "URL"
1246 // property, while there's no document in vCard 2.1.
1247 if (!TextUtils.isEmpty(website)) {
1248 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website);
1255 public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) {
1256 if (contentValuesList != null) {
1257 for (ContentValues contentValues : contentValuesList) {
1258 String company = contentValues.getAsString(Organization.COMPANY);
1259 if (company != null) {
1260 company = company.trim();
1262 String department = contentValues.getAsString(Organization.DEPARTMENT);
1263 if (department != null) {
1264 department = department.trim();
1266 String title = contentValues.getAsString(Organization.TITLE);
1267 if (title != null) {
1268 title = title.trim();
1271 StringBuilder orgBuilder = new StringBuilder();
1272 if (!TextUtils.isEmpty(company)) {
1273 orgBuilder.append(company);
1275 if (!TextUtils.isEmpty(department)) {
1276 if (orgBuilder.length() > 0) {
1277 orgBuilder.append(';');
1279 orgBuilder.append(department);
1281 final String orgline = orgBuilder.toString();
1282 appendLine(VCardConstants.PROPERTY_ORG, orgline,
1283 !VCardUtils.containsOnlyPrintableAscii(orgline),
1284 (mShouldUseQuotedPrintable &&
1285 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
1287 if (!TextUtils.isEmpty(title)) {
1288 appendLine(VCardConstants.PROPERTY_TITLE, title,
1289 !VCardUtils.containsOnlyPrintableAscii(title),
1290 (mShouldUseQuotedPrintable &&
1291 !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
1298 public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) {
1299 if (contentValuesList != null) {
1300 for (ContentValues contentValues : contentValuesList) {
1301 if (contentValues == null) {
1304 byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
1308 final String photoType = VCardUtils.guessImageType(data);
1309 if (photoType == null) {
1310 Log.d(LOG_TAG, "Unknown photo type. Ignored.");
1313 // TODO: check this works fine.
1314 final String photoString = new String(Base64.encode(data, Base64.NO_WRAP));
1315 if (!TextUtils.isEmpty(photoString)) {
1316 appendPhotoLine(photoString, photoType);
1323 public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) {
1324 if (contentValuesList != null) {
1325 if (mOnlyOneNoteFieldIsAvailable) {
1326 final StringBuilder noteBuilder = new StringBuilder();
1327 boolean first = true;
1328 for (final ContentValues contentValues : contentValuesList) {
1329 String note = contentValues.getAsString(Note.NOTE);
1333 if (note.length() > 0) {
1337 noteBuilder.append('\n');
1339 noteBuilder.append(note);
1342 final String noteStr = noteBuilder.toString();
1343 // This means we scan noteStr completely twice, which is redundant.
1344 // But for now, we assume this is not so time-consuming..
1345 final boolean shouldAppendCharsetInfo =
1346 !VCardUtils.containsOnlyPrintableAscii(noteStr);
1347 final boolean reallyUseQuotedPrintable =
1348 (mShouldUseQuotedPrintable &&
1349 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1350 appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
1351 shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1353 for (ContentValues contentValues : contentValuesList) {
1354 final String noteStr = contentValues.getAsString(Note.NOTE);
1355 if (!TextUtils.isEmpty(noteStr)) {
1356 final boolean shouldAppendCharsetInfo =
1357 !VCardUtils.containsOnlyPrintableAscii(noteStr);
1358 final boolean reallyUseQuotedPrintable =
1359 (mShouldUseQuotedPrintable &&
1360 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1361 appendLine(VCardConstants.PROPERTY_NOTE, noteStr,
1362 shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1370 public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) {
1371 // There's possibility where a given object may have more than one birthday, which
1372 // is inappropriate. We just build one birthday.
1373 if (contentValuesList != null) {
1374 String primaryBirthday = null;
1375 String secondaryBirthday = null;
1376 for (final ContentValues contentValues : contentValuesList) {
1377 if (contentValues == null) {
1380 final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE);
1381 final int eventType;
1382 if (eventTypeAsInteger != null) {
1383 eventType = eventTypeAsInteger;
1385 eventType = Event.TYPE_OTHER;
1387 if (eventType == Event.TYPE_BIRTHDAY) {
1388 final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
1389 if (birthdayCandidate == null) {
1392 final Integer isSuperPrimaryAsInteger =
1393 contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
1394 final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
1395 (isSuperPrimaryAsInteger > 0) : false);
1396 if (isSuperPrimary) {
1397 // "super primary" birthday should the prefered one.
1398 primaryBirthday = birthdayCandidate;
1401 final Integer isPrimaryAsInteger =
1402 contentValues.getAsInteger(Event.IS_PRIMARY);
1403 final boolean isPrimary = (isPrimaryAsInteger != null ?
1404 (isPrimaryAsInteger > 0) : false);
1406 // We don't break here since "super primary" birthday may exist later.
1407 primaryBirthday = birthdayCandidate;
1408 } else if (secondaryBirthday == null) {
1409 // First entry is set to the "secondary" candidate.
1410 secondaryBirthday = birthdayCandidate;
1412 } else if (mUsesAndroidProperty) {
1413 // Event types other than Birthday is not supported by vCard.
1414 appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues);
1417 if (primaryBirthday != null) {
1418 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
1419 primaryBirthday.trim());
1420 } else if (secondaryBirthday != null){
1421 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY,
1422 secondaryBirthday.trim());
1428 public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) {
1429 if (mUsesAndroidProperty && contentValuesList != null) {
1430 for (final ContentValues contentValues : contentValuesList) {
1431 if (contentValues == null) {
1434 appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues);
1441 * @param emitEveryTime If true, builder builds the line even when there's no entry.
1443 public void appendPostalLine(final int type, final String label,
1444 final ContentValues contentValues,
1445 final boolean isPrimary, final boolean emitEveryTime) {
1446 final boolean reallyUseQuotedPrintable;
1447 final boolean appendCharset;
1448 final String addressValue;
1450 PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
1451 if (postalStruct == null) {
1452 if (emitEveryTime) {
1453 reallyUseQuotedPrintable = false;
1454 appendCharset = false;
1460 reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
1461 appendCharset = postalStruct.appendCharset;
1462 addressValue = postalStruct.addressData;
1466 List<String> parameterList = new ArrayList<String>();
1468 parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1471 case StructuredPostal.TYPE_HOME: {
1472 parameterList.add(VCardConstants.PARAM_TYPE_HOME);
1475 case StructuredPostal.TYPE_WORK: {
1476 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1479 case StructuredPostal.TYPE_CUSTOM: {
1480 if (!TextUtils.isEmpty(label)
1481 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1482 // We're not sure whether the label is valid in the spec
1483 // ("IANA-token" in the vCard 3.0 is unclear...)
1484 // Just for safety, we add "X-" at the beggining of each label.
1485 // Also checks the label obeys with vCard 3.0 spec.
1486 parameterList.add("X-" + label);
1490 case StructuredPostal.TYPE_OTHER: {
1494 Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
1499 mBuilder.append(VCardConstants.PROPERTY_ADR);
1500 if (!parameterList.isEmpty()) {
1501 mBuilder.append(VCARD_PARAM_SEPARATOR);
1502 appendTypeParameters(parameterList);
1504 if (appendCharset) {
1505 // Strictly, vCard 3.0 does not allow exporters to emit charset information,
1506 // but we will add it since the information should be useful for importers,
1508 // Assume no parser does not emit error with this parameter in vCard 3.0.
1509 mBuilder.append(VCARD_PARAM_SEPARATOR);
1510 mBuilder.append(mVCardCharsetParameter);
1512 if (reallyUseQuotedPrintable) {
1513 mBuilder.append(VCARD_PARAM_SEPARATOR);
1514 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1516 mBuilder.append(VCARD_DATA_SEPARATOR);
1517 mBuilder.append(addressValue);
1518 mBuilder.append(VCARD_END_OF_LINE);
1521 public void appendEmailLine(final int type, final String label,
1522 final String rawValue, final boolean isPrimary) {
1523 final String typeAsString;
1525 case Email.TYPE_CUSTOM: {
1526 if (VCardUtils.isMobilePhoneLabel(label)) {
1527 typeAsString = VCardConstants.PARAM_TYPE_CELL;
1528 } else if (!TextUtils.isEmpty(label)
1529 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1530 typeAsString = "X-" + label;
1532 typeAsString = null;
1536 case Email.TYPE_HOME: {
1537 typeAsString = VCardConstants.PARAM_TYPE_HOME;
1540 case Email.TYPE_WORK: {
1541 typeAsString = VCardConstants.PARAM_TYPE_WORK;
1544 case Email.TYPE_OTHER: {
1545 typeAsString = null;
1548 case Email.TYPE_MOBILE: {
1549 typeAsString = VCardConstants.PARAM_TYPE_CELL;
1553 Log.e(LOG_TAG, "Unknown Email type: " + type);
1554 typeAsString = null;
1559 final List<String> parameterList = new ArrayList<String>();
1561 parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1563 if (!TextUtils.isEmpty(typeAsString)) {
1564 parameterList.add(typeAsString);
1567 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList,
1571 public void appendTelLine(final Integer typeAsInteger, final String label,
1572 final String encodedValue, boolean isPrimary) {
1573 mBuilder.append(VCardConstants.PROPERTY_TEL);
1574 mBuilder.append(VCARD_PARAM_SEPARATOR);
1577 if (typeAsInteger == null) {
1578 type = Phone.TYPE_OTHER;
1580 type = typeAsInteger;
1583 ArrayList<String> parameterList = new ArrayList<String>();
1585 case Phone.TYPE_HOME: {
1586 parameterList.addAll(
1587 Arrays.asList(VCardConstants.PARAM_TYPE_HOME));
1590 case Phone.TYPE_WORK: {
1591 parameterList.addAll(
1592 Arrays.asList(VCardConstants.PARAM_TYPE_WORK));
1595 case Phone.TYPE_FAX_HOME: {
1596 parameterList.addAll(
1597 Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX));
1600 case Phone.TYPE_FAX_WORK: {
1601 parameterList.addAll(
1602 Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX));
1605 case Phone.TYPE_MOBILE: {
1606 parameterList.add(VCardConstants.PARAM_TYPE_CELL);
1609 case Phone.TYPE_PAGER: {
1611 // Not sure about the reason, but previous implementation had
1612 // used "VOICE" instead of "PAGER"
1613 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1615 parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
1619 case Phone.TYPE_OTHER: {
1620 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1623 case Phone.TYPE_CAR: {
1624 parameterList.add(VCardConstants.PARAM_TYPE_CAR);
1627 case Phone.TYPE_COMPANY_MAIN: {
1628 // There's no relevant field in vCard (at least 2.1).
1629 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1633 case Phone.TYPE_ISDN: {
1634 parameterList.add(VCardConstants.PARAM_TYPE_ISDN);
1637 case Phone.TYPE_MAIN: {
1641 case Phone.TYPE_OTHER_FAX: {
1642 parameterList.add(VCardConstants.PARAM_TYPE_FAX);
1645 case Phone.TYPE_TELEX: {
1646 parameterList.add(VCardConstants.PARAM_TYPE_TLX);
1649 case Phone.TYPE_WORK_MOBILE: {
1650 parameterList.addAll(
1651 Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL));
1654 case Phone.TYPE_WORK_PAGER: {
1655 parameterList.add(VCardConstants.PARAM_TYPE_WORK);
1658 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1660 parameterList.add(VCardConstants.PARAM_TYPE_PAGER);
1664 case Phone.TYPE_MMS: {
1665 parameterList.add(VCardConstants.PARAM_TYPE_MSG);
1668 case Phone.TYPE_CUSTOM: {
1669 if (TextUtils.isEmpty(label)) {
1670 // Just ignore the custom type.
1671 parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
1672 } else if (VCardUtils.isMobilePhoneLabel(label)) {
1673 parameterList.add(VCardConstants.PARAM_TYPE_CELL);
1674 } else if (mIsV30OrV40) {
1675 // This label is appropriately encoded in appendTypeParameters.
1676 parameterList.add(label);
1678 final String upperLabel = label.toUpperCase();
1679 if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
1680 parameterList.add(upperLabel);
1681 } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1682 // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
1684 parameterList.add("X-" + label);
1689 case Phone.TYPE_RADIO:
1690 case Phone.TYPE_TTY_TDD:
1697 parameterList.add(VCardConstants.PARAM_TYPE_PREF);
1700 if (parameterList.isEmpty()) {
1701 appendUncommonPhoneType(mBuilder, type);
1703 appendTypeParameters(parameterList);
1706 mBuilder.append(VCARD_DATA_SEPARATOR);
1707 mBuilder.append(encodedValue);
1708 mBuilder.append(VCARD_END_OF_LINE);
1712 * Appends phone type string which may not be available in some devices.
1714 private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
1716 // The previous implementation for DoCoMo had been conservative
1717 // about miscellaneous types.
1718 builder.append(VCardConstants.PARAM_TYPE_VOICE);
1720 String phoneType = VCardUtils.getPhoneTypeString(type);
1721 if (phoneType != null) {
1722 appendTypeParameter(phoneType);
1724 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
1730 * @param encodedValue Must be encoded by BASE64
1733 public void appendPhotoLine(final String encodedValue, final String photoType) {
1734 StringBuilder tmpBuilder = new StringBuilder();
1735 tmpBuilder.append(VCardConstants.PROPERTY_PHOTO);
1736 tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1738 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B);
1740 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
1742 tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1743 appendTypeParameter(tmpBuilder, photoType);
1744 tmpBuilder.append(VCARD_DATA_SEPARATOR);
1745 tmpBuilder.append(encodedValue);
1747 final String tmpStr = tmpBuilder.toString();
1748 tmpBuilder = new StringBuilder();
1750 final int length = tmpStr.length();
1751 final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30
1752 - VCARD_END_OF_LINE.length();
1753 final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length();
1754 int maxNum = maxNumForFirstLine;
1755 for (int i = 0; i < length; i++) {
1756 tmpBuilder.append(tmpStr.charAt(i));
1758 if (lineCount > maxNum) {
1759 tmpBuilder.append(VCARD_END_OF_LINE);
1760 tmpBuilder.append(VCARD_WS);
1761 maxNum = maxNumInGeneral;
1765 mBuilder.append(tmpBuilder.toString());
1766 mBuilder.append(VCARD_END_OF_LINE);
1767 mBuilder.append(VCARD_END_OF_LINE);
1771 * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP
1772 * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP
1773 * instead of "IMPP;sip:...".
1775 * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all.
1777 public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) {
1778 final boolean useXProperty;
1780 useXProperty = false;
1781 } else if (mUsesDefactProperty){
1782 useXProperty = true;
1787 if (contentValuesList != null) {
1788 for (ContentValues contentValues : contentValuesList) {
1789 String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS);
1790 if (TextUtils.isEmpty(sipAddress)) {
1794 // X-SIP does not contain "sip:" prefix.
1795 if (sipAddress.startsWith("sip:")) {
1796 if (sipAddress.length() == 4) {
1799 sipAddress = sipAddress.substring(4);
1801 // No type is available yet.
1802 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress);
1804 if (!sipAddress.startsWith("sip:")) {
1805 sipAddress = "sip:" + sipAddress;
1807 final String propertyName;
1808 if (VCardConfig.isVersion40(mVCardType)) {
1809 // We have two ways to emit sip address: TEL and IMPP. Currently (rev.13)
1810 // TEL seems appropriate but may change in the future.
1811 propertyName = VCardConstants.PROPERTY_TEL;
1813 // RFC 4770 (for vCard 3.0)
1814 propertyName = VCardConstants.PROPERTY_IMPP;
1816 appendLineWithCharsetAndQPDetection(propertyName, sipAddress);
1823 public void appendAndroidSpecificProperty(
1824 final String mimeType, ContentValues contentValues) {
1825 if (!sAllowedAndroidPropertySet.contains(mimeType)) {
1828 final List<String> rawValueList = new ArrayList<String>();
1829 for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) {
1830 String value = contentValues.getAsString("data" + i);
1831 if (value == null) {
1834 rawValueList.add(value);
1837 boolean needCharset =
1838 (mShouldAppendCharsetParam &&
1839 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1840 boolean reallyUseQuotedPrintable =
1841 (mShouldUseQuotedPrintable &&
1842 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1843 mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM);
1845 mBuilder.append(VCARD_PARAM_SEPARATOR);
1846 mBuilder.append(mVCardCharsetParameter);
1848 if (reallyUseQuotedPrintable) {
1849 mBuilder.append(VCARD_PARAM_SEPARATOR);
1850 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1852 mBuilder.append(VCARD_DATA_SEPARATOR);
1853 mBuilder.append(mimeType); // Should not be encoded.
1854 for (String rawValue : rawValueList) {
1855 final String encodedValue;
1856 if (reallyUseQuotedPrintable) {
1857 encodedValue = encodeQuotedPrintable(rawValue);
1859 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1860 // (which says "When generating a content line, lines longer than
1861 // 75 characters SHOULD be folded"), though several
1862 // (even well-known) applications do not care this.
1863 encodedValue = escapeCharacters(rawValue);
1865 mBuilder.append(VCARD_ITEM_SEPARATOR);
1866 mBuilder.append(encodedValue);
1868 mBuilder.append(VCARD_END_OF_LINE);
1871 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1872 final String rawValue) {
1873 appendLineWithCharsetAndQPDetection(propertyName, null, rawValue);
1876 public void appendLineWithCharsetAndQPDetection(
1877 final String propertyName, final List<String> rawValueList) {
1878 appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList);
1881 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1882 final List<String> parameterList, final String rawValue) {
1883 final boolean needCharset =
1884 !VCardUtils.containsOnlyPrintableAscii(rawValue);
1885 final boolean reallyUseQuotedPrintable =
1886 (mShouldUseQuotedPrintable &&
1887 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue));
1888 appendLine(propertyName, parameterList,
1889 rawValue, needCharset, reallyUseQuotedPrintable);
1892 public void appendLineWithCharsetAndQPDetection(final String propertyName,
1893 final List<String> parameterList, final List<String> rawValueList) {
1894 boolean needCharset =
1895 (mShouldAppendCharsetParam &&
1896 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1897 boolean reallyUseQuotedPrintable =
1898 (mShouldUseQuotedPrintable &&
1899 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList));
1900 appendLine(propertyName, parameterList, rawValueList,
1901 needCharset, reallyUseQuotedPrintable);
1905 * Appends one line with a given property name and value.
1907 public void appendLine(final String propertyName, final String rawValue) {
1908 appendLine(propertyName, rawValue, false, false);
1911 public void appendLine(final String propertyName, final List<String> rawValueList) {
1912 appendLine(propertyName, rawValueList, false, false);
1915 public void appendLine(final String propertyName,
1916 final String rawValue, final boolean needCharset,
1917 boolean reallyUseQuotedPrintable) {
1918 appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable);
1921 public void appendLine(final String propertyName, final List<String> parameterList,
1922 final String rawValue) {
1923 appendLine(propertyName, parameterList, rawValue, false, false);
1926 public void appendLine(final String propertyName, final List<String> parameterList,
1927 final String rawValue, final boolean needCharset,
1928 boolean reallyUseQuotedPrintable) {
1929 mBuilder.append(propertyName);
1930 if (parameterList != null && parameterList.size() > 0) {
1931 mBuilder.append(VCARD_PARAM_SEPARATOR);
1932 appendTypeParameters(parameterList);
1935 mBuilder.append(VCARD_PARAM_SEPARATOR);
1936 mBuilder.append(mVCardCharsetParameter);
1939 final String encodedValue;
1940 if (reallyUseQuotedPrintable) {
1941 mBuilder.append(VCARD_PARAM_SEPARATOR);
1942 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1943 encodedValue = encodeQuotedPrintable(rawValue);
1945 // TODO: one line may be too huge, which may be invalid in vCard spec, though
1946 // several (even well-known) applications do not care that violation.
1947 encodedValue = escapeCharacters(rawValue);
1950 mBuilder.append(VCARD_DATA_SEPARATOR);
1951 mBuilder.append(encodedValue);
1952 mBuilder.append(VCARD_END_OF_LINE);
1955 public void appendLine(final String propertyName, final List<String> rawValueList,
1956 final boolean needCharset, boolean needQuotedPrintable) {
1957 appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable);
1960 public void appendLine(final String propertyName, final List<String> parameterList,
1961 final List<String> rawValueList, final boolean needCharset,
1962 final boolean needQuotedPrintable) {
1963 mBuilder.append(propertyName);
1964 if (parameterList != null && parameterList.size() > 0) {
1965 mBuilder.append(VCARD_PARAM_SEPARATOR);
1966 appendTypeParameters(parameterList);
1969 mBuilder.append(VCARD_PARAM_SEPARATOR);
1970 mBuilder.append(mVCardCharsetParameter);
1972 if (needQuotedPrintable) {
1973 mBuilder.append(VCARD_PARAM_SEPARATOR);
1974 mBuilder.append(VCARD_PARAM_ENCODING_QP);
1977 mBuilder.append(VCARD_DATA_SEPARATOR);
1978 boolean first = true;
1979 for (String rawValue : rawValueList) {
1980 final String encodedValue;
1981 if (needQuotedPrintable) {
1982 encodedValue = encodeQuotedPrintable(rawValue);
1984 // TODO: one line may be too huge, which may be invalid in vCard 3.0
1985 // (which says "When generating a content line, lines longer than
1986 // 75 characters SHOULD be folded"), though several
1987 // (even well-known) applications do not care this.
1988 encodedValue = escapeCharacters(rawValue);
1994 mBuilder.append(VCARD_ITEM_SEPARATOR);
1996 mBuilder.append(encodedValue);
1998 mBuilder.append(VCARD_END_OF_LINE);
2002 * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2004 private void appendTypeParameters(final List<String> types) {
2005 // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
2006 // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
2007 boolean first = true;
2008 for (final String typeValue : types) {
2009 if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) {
2010 final String encoded = (VCardConfig.isVersion40(mVCardType) ?
2011 VCardUtils.toStringAsV40ParamValue(typeValue) :
2012 VCardUtils.toStringAsV30ParamValue(typeValue));
2013 if (TextUtils.isEmpty(encoded)) {
2020 mBuilder.append(VCARD_PARAM_SEPARATOR);
2022 appendTypeParameter(encoded);
2023 } else { // vCard 2.1
2024 if (!VCardUtils.isV21Word(typeValue)) {
2030 mBuilder.append(VCARD_PARAM_SEPARATOR);
2032 appendTypeParameter(typeValue);
2038 * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2040 private void appendTypeParameter(final String type) {
2041 appendTypeParameter(mBuilder, type);
2044 private void appendTypeParameter(final StringBuilder builder, final String type) {
2045 // Refrain from using appendType() so that "TYPE=" is not be appended when the
2046 // device is DoCoMo's (just for safety).
2048 // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
2049 if (VCardConfig.isVersion40(mVCardType) ||
2050 ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) {
2051 builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
2053 builder.append(type);
2057 * Returns true when the property line should contain charset parameter
2058 * information. This method may return true even when vCard version is 3.0.
2060 * Strictly, adding charset information is invalid in VCard 3.0.
2061 * However we'll add the info only when charset we use is not UTF-8
2062 * in vCard 3.0 format, since parser side may be able to use the charset
2063 * via this field, though we may encounter another problem by adding it.
2065 * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
2066 * recommends UTF-8. By adding this field, parsers may be able
2067 * to know this text is NOT UTF-8 but Shift_Jis.
2069 private boolean shouldAppendCharsetParam(String...propertyValueList) {
2070 if (!mShouldAppendCharsetParam) {
2073 for (String propertyValue : propertyValueList) {
2074 if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
2081 private String encodeQuotedPrintable(final String str) {
2082 if (TextUtils.isEmpty(str)) {
2086 final StringBuilder builder = new StringBuilder();
2089 byte[] strArray = null;
2092 strArray = str.getBytes(mCharset);
2093 } catch (UnsupportedEncodingException e) {
2094 Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. "
2095 + "Try default charset");
2096 strArray = str.getBytes();
2098 while (index < strArray.length) {
2099 builder.append(String.format("=%02X", strArray[index]));
2103 if (lineCount >= 67) {
2104 // Specification requires CRLF must be inserted before the
2105 // length of the line
2106 // becomes more than 76.
2107 // Assuming that the next character is a multi-byte character,
2111 builder.append("=\r\n");
2116 return builder.toString();
2120 * Append '\' to the characters which should be escaped. The character set is different
2121 * not only between vCard 2.1 and vCard 3.0 but also among each device.
2123 * Note that Quoted-Printable string must not be input here.
2125 @SuppressWarnings("fallthrough")
2126 private String escapeCharacters(final String unescaped) {
2127 if (TextUtils.isEmpty(unescaped)) {
2131 final StringBuilder tmpBuilder = new StringBuilder();
2132 final int length = unescaped.length();
2133 for (int i = 0; i < length; i++) {
2134 final char ch = unescaped.charAt(i);
2137 tmpBuilder.append('\\');
2138 tmpBuilder.append(';');
2142 if (i + 1 < length) {
2143 char nextChar = unescaped.charAt(i);
2144 if (nextChar == '\n') {
2154 // In vCard 2.1, there's no specification about this, while
2155 // vCard 3.0 explicitly requires this should be encoded to "\n".
2156 tmpBuilder.append("\\n");
2161 tmpBuilder.append("\\\\");
2170 tmpBuilder.append('\\');
2171 tmpBuilder.append(ch);
2173 tmpBuilder.append(ch);
2179 tmpBuilder.append("\\,");
2181 tmpBuilder.append(ch);
2186 tmpBuilder.append(ch);
2191 return tmpBuilder.toString();
2195 public String toString() {
2196 if (!mEndAppended) {
2198 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
2199 appendLine(VCardConstants.PROPERTY_X_REDUCTION, "");
2200 appendLine(VCardConstants.PROPERTY_X_NO, "");
2201 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, "");
2203 appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD);
2204 mEndAppended = true;
2206 return mBuilder.toString();