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