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