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