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