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