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