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