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