VCardBuilder.java revision 465aa5d5706ba56c74c1249a17a6df80b0f42972
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 // TODO: It would be better to have this logic as optional. 837 final int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); 838 if (type == Phone.TYPE_PAGER || 839 VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { 840 phoneLineExists = true; 841 if (!phoneSet.contains(phoneNumber)) { 842 phoneSet.add(phoneNumber); 843 appendTelLine(type, label, phoneNumber, isPrimary); 844 } 845 } else { 846 final List<String> phoneNumberList = splitAndTrimPhoneNumbers(phoneNumber); 847 if (phoneNumberList.isEmpty()) { 848 continue; 849 } 850 phoneLineExists = true; 851 for (String actualPhoneNumber : phoneNumberList) { 852 if (!phoneSet.contains(actualPhoneNumber)) { 853 final int phoneFormat = VCardUtils.getPhoneNumberFormat(mVCardType); 854 String formatted = 855 PhoneNumberUtils.formatNumber(actualPhoneNumber, phoneFormat); 856 857 // In vCard 4.0, value type must be "a single URI value", 858 // not just a phone number. (Based on vCard 4.0 rev.13) 859 if (VCardConfig.isVersion40(mVCardType) 860 && !TextUtils.isEmpty(formatted) 861 && !formatted.startsWith("tel:")) { 862 formatted = "tel:" + formatted; 863 } 864 865 // Pre-formatted string should be stored. 866 phoneSet.add(actualPhoneNumber); 867 appendTelLine(type, label, formatted, isPrimary); 868 } 869 } // for (String actualPhoneNumber : phoneNumberList) { 870 871 // TODO: TEL with SIP URI? 872 } 873 } 874 } 875 876 if (!phoneLineExists && mIsDoCoMo) { 877 appendTelLine(Phone.TYPE_HOME, "", "", false); 878 } 879 880 return this; 881 } 882 883 /** 884 * <p> 885 * Splits a given string expressing phone numbers into several strings, and remove 886 * unnecessary characters inside them. The size of a returned list becomes 1 when 887 * no split is needed. 888 * </p> 889 * <p> 890 * The given number "may" have several phone numbers when the contact entry is corrupted 891 * because of its original source. 892 * e.g. "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami)" 893 * </p> 894 * <p> 895 * This kind of "phone numbers" will not be created with Android vCard implementation, 896 * but we may encounter them if the source of the input data has already corrupted 897 * implementation. 898 * </p> 899 * <p> 900 * To handle this case, this method first splits its input into multiple parts 901 * (e.g. "111-222-3333 (Miami)", "444-555-6666 (Broward", and 305653-6796 (Miami)") and 902 * removes unnecessary strings like "(Miami)". 903 * </p> 904 * <p> 905 * Do not call this method when trimming is inappropriate for its receivers. 906 * </p> 907 */ 908 private List<String> splitAndTrimPhoneNumbers(final String phoneNumber) { 909 final List<String> phoneList = new ArrayList<String>(); 910 911 StringBuilder builder = new StringBuilder(); 912 final int length = phoneNumber.length(); 913 for (int i = 0; i < length; i++) { 914 final char ch = phoneNumber.charAt(i); 915 if (Character.isDigit(ch) || ch == '+') { 916 builder.append(ch); 917 } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { 918 phoneList.add(builder.toString()); 919 builder = new StringBuilder(); 920 } 921 } 922 if (builder.length() > 0) { 923 phoneList.add(builder.toString()); 924 } 925 926 return phoneList; 927 } 928 929 public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) { 930 boolean emailAddressExists = false; 931 if (contentValuesList != null) { 932 final Set<String> addressSet = new HashSet<String>(); 933 for (ContentValues contentValues : contentValuesList) { 934 String emailAddress = contentValues.getAsString(Email.DATA); 935 if (emailAddress != null) { 936 emailAddress = emailAddress.trim(); 937 } 938 if (TextUtils.isEmpty(emailAddress)) { 939 continue; 940 } 941 Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); 942 final int type = (typeAsObject != null ? 943 typeAsObject : DEFAULT_EMAIL_TYPE); 944 final String label = contentValues.getAsString(Email.LABEL); 945 Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); 946 final boolean isPrimary = (isPrimaryAsInteger != null ? 947 (isPrimaryAsInteger > 0) : false); 948 emailAddressExists = true; 949 if (!addressSet.contains(emailAddress)) { 950 addressSet.add(emailAddress); 951 appendEmailLine(type, label, emailAddress, isPrimary); 952 } 953 } 954 } 955 956 if (!emailAddressExists && mIsDoCoMo) { 957 appendEmailLine(Email.TYPE_HOME, "", "", false); 958 } 959 960 return this; 961 } 962 963 public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) { 964 if (contentValuesList == null || contentValuesList.isEmpty()) { 965 if (mIsDoCoMo) { 966 mBuilder.append(VCardConstants.PROPERTY_ADR); 967 mBuilder.append(VCARD_PARAM_SEPARATOR); 968 mBuilder.append(VCardConstants.PARAM_TYPE_HOME); 969 mBuilder.append(VCARD_DATA_SEPARATOR); 970 mBuilder.append(VCARD_END_OF_LINE); 971 } 972 } else { 973 if (mIsDoCoMo) { 974 appendPostalsForDoCoMo(contentValuesList); 975 } else { 976 appendPostalsForGeneric(contentValuesList); 977 } 978 } 979 980 return this; 981 } 982 983 private static final Map<Integer, Integer> sPostalTypePriorityMap; 984 985 static { 986 sPostalTypePriorityMap = new HashMap<Integer, Integer>(); 987 sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); 988 sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); 989 sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); 990 sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); 991 } 992 993 /** 994 * Tries to append just one line. If there's no appropriate address 995 * information, append an empty line. 996 */ 997 private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) { 998 int currentPriority = Integer.MAX_VALUE; 999 int currentType = Integer.MAX_VALUE; 1000 ContentValues currentContentValues = null; 1001 for (final ContentValues contentValues : contentValuesList) { 1002 if (contentValues == null) { 1003 continue; 1004 } 1005 final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); 1006 final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); 1007 final int priority = 1008 (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); 1009 if (priority < currentPriority) { 1010 currentPriority = priority; 1011 currentType = typeAsInteger; 1012 currentContentValues = contentValues; 1013 if (priority == 0) { 1014 break; 1015 } 1016 } 1017 } 1018 1019 if (currentContentValues == null) { 1020 Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); 1021 return; 1022 } 1023 1024 final String label = currentContentValues.getAsString(StructuredPostal.LABEL); 1025 appendPostalLine(currentType, label, currentContentValues, false, true); 1026 } 1027 1028 private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) { 1029 for (final ContentValues contentValues : contentValuesList) { 1030 if (contentValues == null) { 1031 continue; 1032 } 1033 final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); 1034 final int type = (typeAsInteger != null ? 1035 typeAsInteger : DEFAULT_POSTAL_TYPE); 1036 final String label = contentValues.getAsString(StructuredPostal.LABEL); 1037 final Integer isPrimaryAsInteger = 1038 contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); 1039 final boolean isPrimary = (isPrimaryAsInteger != null ? 1040 (isPrimaryAsInteger > 0) : false); 1041 appendPostalLine(type, label, contentValues, isPrimary, false); 1042 } 1043 } 1044 1045 private static class PostalStruct { 1046 final boolean reallyUseQuotedPrintable; 1047 final boolean appendCharset; 1048 final String addressData; 1049 public PostalStruct(final boolean reallyUseQuotedPrintable, 1050 final boolean appendCharset, final String addressData) { 1051 this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; 1052 this.appendCharset = appendCharset; 1053 this.addressData = addressData; 1054 } 1055 } 1056 1057 /** 1058 * @return null when there's no information available to construct the data. 1059 */ 1060 private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { 1061 // adr-value = 0*6(text-value ";") text-value 1062 // ; PO Box, Extended Address, Street, Locality, Region, Postal 1063 // ; Code, Country Name 1064 final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX); 1065 final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); 1066 final String rawStreet = contentValues.getAsString(StructuredPostal.STREET); 1067 final String rawLocality = contentValues.getAsString(StructuredPostal.CITY); 1068 final String rawRegion = contentValues.getAsString(StructuredPostal.REGION); 1069 final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE); 1070 final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY); 1071 final String[] rawAddressArray = new String[]{ 1072 rawPoBox, rawNeighborhood, rawStreet, rawLocality, 1073 rawRegion, rawPostalCode, rawCountry}; 1074 if (!VCardUtils.areAllEmpty(rawAddressArray)) { 1075 final boolean reallyUseQuotedPrintable = 1076 (mShouldUseQuotedPrintable && 1077 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray)); 1078 final boolean appendCharset = 1079 !VCardUtils.containsOnlyPrintableAscii(rawAddressArray); 1080 final String encodedPoBox; 1081 final String encodedStreet; 1082 final String encodedLocality; 1083 final String encodedRegion; 1084 final String encodedPostalCode; 1085 final String encodedCountry; 1086 final String encodedNeighborhood; 1087 1088 final String rawLocality2; 1089 // This looks inefficient since we encode rawLocality and rawNeighborhood twice, 1090 // but this is intentional. 1091 // 1092 // QP encoding may add line feeds when needed and the result of 1093 // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood) 1094 // may be different from 1095 // - encodedLocality + " " + encodedNeighborhood. 1096 // 1097 // We use safer way. 1098 if (TextUtils.isEmpty(rawLocality)) { 1099 if (TextUtils.isEmpty(rawNeighborhood)) { 1100 rawLocality2 = ""; 1101 } else { 1102 rawLocality2 = rawNeighborhood; 1103 } 1104 } else { 1105 if (TextUtils.isEmpty(rawNeighborhood)) { 1106 rawLocality2 = rawLocality; 1107 } else { 1108 rawLocality2 = rawLocality + " " + rawNeighborhood; 1109 } 1110 } 1111 if (reallyUseQuotedPrintable) { 1112 encodedPoBox = encodeQuotedPrintable(rawPoBox); 1113 encodedStreet = encodeQuotedPrintable(rawStreet); 1114 encodedLocality = encodeQuotedPrintable(rawLocality2); 1115 encodedRegion = encodeQuotedPrintable(rawRegion); 1116 encodedPostalCode = encodeQuotedPrintable(rawPostalCode); 1117 encodedCountry = encodeQuotedPrintable(rawCountry); 1118 } else { 1119 encodedPoBox = escapeCharacters(rawPoBox); 1120 encodedStreet = escapeCharacters(rawStreet); 1121 encodedLocality = escapeCharacters(rawLocality2); 1122 encodedRegion = escapeCharacters(rawRegion); 1123 encodedPostalCode = escapeCharacters(rawPostalCode); 1124 encodedCountry = escapeCharacters(rawCountry); 1125 encodedNeighborhood = escapeCharacters(rawNeighborhood); 1126 } 1127 final StringBuilder addressBuilder = new StringBuilder(); 1128 addressBuilder.append(encodedPoBox); 1129 addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address 1130 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street 1131 addressBuilder.append(encodedStreet); 1132 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality 1133 addressBuilder.append(encodedLocality); 1134 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region 1135 addressBuilder.append(encodedRegion); 1136 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code 1137 addressBuilder.append(encodedPostalCode); 1138 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country 1139 addressBuilder.append(encodedCountry); 1140 return new PostalStruct( 1141 reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); 1142 } else { // VCardUtils.areAllEmpty(rawAddressArray) == true 1143 // Try to use FORMATTED_ADDRESS instead. 1144 final String rawFormattedAddress = 1145 contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1146 if (TextUtils.isEmpty(rawFormattedAddress)) { 1147 return null; 1148 } 1149 final boolean reallyUseQuotedPrintable = 1150 (mShouldUseQuotedPrintable && 1151 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress)); 1152 final boolean appendCharset = 1153 !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress); 1154 final String encodedFormattedAddress; 1155 if (reallyUseQuotedPrintable) { 1156 encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress); 1157 } else { 1158 encodedFormattedAddress = escapeCharacters(rawFormattedAddress); 1159 } 1160 1161 // We use the second value ("Extended Address") just because Japanese mobile phones 1162 // do so. If the other importer expects the value be in the other field, some flag may 1163 // be needed. 1164 final StringBuilder addressBuilder = new StringBuilder(); 1165 addressBuilder.append(VCARD_ITEM_SEPARATOR); // PO BOX ; Extended Address 1166 addressBuilder.append(encodedFormattedAddress); 1167 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Extended Address : Street 1168 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Street : Locality 1169 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Locality : Region 1170 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Region : Postal Code 1171 addressBuilder.append(VCARD_ITEM_SEPARATOR); // Postal Code : Country 1172 return new PostalStruct( 1173 reallyUseQuotedPrintable, appendCharset, addressBuilder.toString()); 1174 } 1175 } 1176 1177 public VCardBuilder appendIms(final List<ContentValues> contentValuesList) { 1178 if (contentValuesList != null) { 1179 for (ContentValues contentValues : contentValuesList) { 1180 final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); 1181 if (protocolAsObject == null) { 1182 continue; 1183 } 1184 final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); 1185 if (propertyName == null) { 1186 continue; 1187 } 1188 String data = contentValues.getAsString(Im.DATA); 1189 if (data != null) { 1190 data = data.trim(); 1191 } 1192 if (TextUtils.isEmpty(data)) { 1193 continue; 1194 } 1195 final String typeAsString; 1196 { 1197 final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); 1198 switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { 1199 case Im.TYPE_HOME: { 1200 typeAsString = VCardConstants.PARAM_TYPE_HOME; 1201 break; 1202 } 1203 case Im.TYPE_WORK: { 1204 typeAsString = VCardConstants.PARAM_TYPE_WORK; 1205 break; 1206 } 1207 case Im.TYPE_CUSTOM: { 1208 final String label = contentValues.getAsString(Im.LABEL); 1209 typeAsString = (label != null ? "X-" + label : null); 1210 break; 1211 } 1212 case Im.TYPE_OTHER: // Ignore 1213 default: { 1214 typeAsString = null; 1215 break; 1216 } 1217 } 1218 } 1219 1220 final List<String> parameterList = new ArrayList<String>(); 1221 if (!TextUtils.isEmpty(typeAsString)) { 1222 parameterList.add(typeAsString); 1223 } 1224 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); 1225 final boolean isPrimary = (isPrimaryAsInteger != null ? 1226 (isPrimaryAsInteger > 0) : false); 1227 if (isPrimary) { 1228 parameterList.add(VCardConstants.PARAM_TYPE_PREF); 1229 } 1230 1231 appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); 1232 } 1233 } 1234 return this; 1235 } 1236 1237 public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) { 1238 if (contentValuesList != null) { 1239 for (ContentValues contentValues : contentValuesList) { 1240 String website = contentValues.getAsString(Website.URL); 1241 if (website != null) { 1242 website = website.trim(); 1243 } 1244 1245 // Note: vCard 3.0 does not allow any parameter addition toward "URL" 1246 // property, while there's no document in vCard 2.1. 1247 if (!TextUtils.isEmpty(website)) { 1248 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website); 1249 } 1250 } 1251 } 1252 return this; 1253 } 1254 1255 public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) { 1256 if (contentValuesList != null) { 1257 for (ContentValues contentValues : contentValuesList) { 1258 String company = contentValues.getAsString(Organization.COMPANY); 1259 if (company != null) { 1260 company = company.trim(); 1261 } 1262 String department = contentValues.getAsString(Organization.DEPARTMENT); 1263 if (department != null) { 1264 department = department.trim(); 1265 } 1266 String title = contentValues.getAsString(Organization.TITLE); 1267 if (title != null) { 1268 title = title.trim(); 1269 } 1270 1271 StringBuilder orgBuilder = new StringBuilder(); 1272 if (!TextUtils.isEmpty(company)) { 1273 orgBuilder.append(company); 1274 } 1275 if (!TextUtils.isEmpty(department)) { 1276 if (orgBuilder.length() > 0) { 1277 orgBuilder.append(';'); 1278 } 1279 orgBuilder.append(department); 1280 } 1281 final String orgline = orgBuilder.toString(); 1282 appendLine(VCardConstants.PROPERTY_ORG, orgline, 1283 !VCardUtils.containsOnlyPrintableAscii(orgline), 1284 (mShouldUseQuotedPrintable && 1285 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); 1286 1287 if (!TextUtils.isEmpty(title)) { 1288 appendLine(VCardConstants.PROPERTY_TITLE, title, 1289 !VCardUtils.containsOnlyPrintableAscii(title), 1290 (mShouldUseQuotedPrintable && 1291 !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); 1292 } 1293 } 1294 } 1295 return this; 1296 } 1297 1298 public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) { 1299 if (contentValuesList != null) { 1300 for (ContentValues contentValues : contentValuesList) { 1301 if (contentValues == null) { 1302 continue; 1303 } 1304 byte[] data = contentValues.getAsByteArray(Photo.PHOTO); 1305 if (data == null) { 1306 continue; 1307 } 1308 final String photoType = VCardUtils.guessImageType(data); 1309 if (photoType == null) { 1310 Log.d(LOG_TAG, "Unknown photo type. Ignored."); 1311 continue; 1312 } 1313 // TODO: check this works fine. 1314 final String photoString = new String(Base64.encode(data, Base64.NO_WRAP)); 1315 if (!TextUtils.isEmpty(photoString)) { 1316 appendPhotoLine(photoString, photoType); 1317 } 1318 } 1319 } 1320 return this; 1321 } 1322 1323 public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) { 1324 if (contentValuesList != null) { 1325 if (mOnlyOneNoteFieldIsAvailable) { 1326 final StringBuilder noteBuilder = new StringBuilder(); 1327 boolean first = true; 1328 for (final ContentValues contentValues : contentValuesList) { 1329 String note = contentValues.getAsString(Note.NOTE); 1330 if (note == null) { 1331 note = ""; 1332 } 1333 if (note.length() > 0) { 1334 if (first) { 1335 first = false; 1336 } else { 1337 noteBuilder.append('\n'); 1338 } 1339 noteBuilder.append(note); 1340 } 1341 } 1342 final String noteStr = noteBuilder.toString(); 1343 // This means we scan noteStr completely twice, which is redundant. 1344 // But for now, we assume this is not so time-consuming.. 1345 final boolean shouldAppendCharsetInfo = 1346 !VCardUtils.containsOnlyPrintableAscii(noteStr); 1347 final boolean reallyUseQuotedPrintable = 1348 (mShouldUseQuotedPrintable && 1349 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); 1350 appendLine(VCardConstants.PROPERTY_NOTE, noteStr, 1351 shouldAppendCharsetInfo, reallyUseQuotedPrintable); 1352 } else { 1353 for (ContentValues contentValues : contentValuesList) { 1354 final String noteStr = contentValues.getAsString(Note.NOTE); 1355 if (!TextUtils.isEmpty(noteStr)) { 1356 final boolean shouldAppendCharsetInfo = 1357 !VCardUtils.containsOnlyPrintableAscii(noteStr); 1358 final boolean reallyUseQuotedPrintable = 1359 (mShouldUseQuotedPrintable && 1360 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); 1361 appendLine(VCardConstants.PROPERTY_NOTE, noteStr, 1362 shouldAppendCharsetInfo, reallyUseQuotedPrintable); 1363 } 1364 } 1365 } 1366 } 1367 return this; 1368 } 1369 1370 public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { 1371 // There's possibility where a given object may have more than one birthday, which 1372 // is inappropriate. We just build one birthday. 1373 if (contentValuesList != null) { 1374 String primaryBirthday = null; 1375 String secondaryBirthday = null; 1376 for (final ContentValues contentValues : contentValuesList) { 1377 if (contentValues == null) { 1378 continue; 1379 } 1380 final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); 1381 final int eventType; 1382 if (eventTypeAsInteger != null) { 1383 eventType = eventTypeAsInteger; 1384 } else { 1385 eventType = Event.TYPE_OTHER; 1386 } 1387 if (eventType == Event.TYPE_BIRTHDAY) { 1388 final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); 1389 if (birthdayCandidate == null) { 1390 continue; 1391 } 1392 final Integer isSuperPrimaryAsInteger = 1393 contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); 1394 final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? 1395 (isSuperPrimaryAsInteger > 0) : false); 1396 if (isSuperPrimary) { 1397 // "super primary" birthday should the prefered one. 1398 primaryBirthday = birthdayCandidate; 1399 break; 1400 } 1401 final Integer isPrimaryAsInteger = 1402 contentValues.getAsInteger(Event.IS_PRIMARY); 1403 final boolean isPrimary = (isPrimaryAsInteger != null ? 1404 (isPrimaryAsInteger > 0) : false); 1405 if (isPrimary) { 1406 // We don't break here since "super primary" birthday may exist later. 1407 primaryBirthday = birthdayCandidate; 1408 } else if (secondaryBirthday == null) { 1409 // First entry is set to the "secondary" candidate. 1410 secondaryBirthday = birthdayCandidate; 1411 } 1412 } else if (mUsesAndroidProperty) { 1413 // Event types other than Birthday is not supported by vCard. 1414 appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); 1415 } 1416 } 1417 if (primaryBirthday != null) { 1418 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, 1419 primaryBirthday.trim()); 1420 } else if (secondaryBirthday != null){ 1421 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, 1422 secondaryBirthday.trim()); 1423 } 1424 } 1425 return this; 1426 } 1427 1428 public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) { 1429 if (mUsesAndroidProperty && contentValuesList != null) { 1430 for (final ContentValues contentValues : contentValuesList) { 1431 if (contentValues == null) { 1432 continue; 1433 } 1434 appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); 1435 } 1436 } 1437 return this; 1438 } 1439 1440 /** 1441 * @param emitEveryTime If true, builder builds the line even when there's no entry. 1442 */ 1443 public void appendPostalLine(final int type, final String label, 1444 final ContentValues contentValues, 1445 final boolean isPrimary, final boolean emitEveryTime) { 1446 final boolean reallyUseQuotedPrintable; 1447 final boolean appendCharset; 1448 final String addressValue; 1449 { 1450 PostalStruct postalStruct = tryConstructPostalStruct(contentValues); 1451 if (postalStruct == null) { 1452 if (emitEveryTime) { 1453 reallyUseQuotedPrintable = false; 1454 appendCharset = false; 1455 addressValue = ""; 1456 } else { 1457 return; 1458 } 1459 } else { 1460 reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; 1461 appendCharset = postalStruct.appendCharset; 1462 addressValue = postalStruct.addressData; 1463 } 1464 } 1465 1466 List<String> parameterList = new ArrayList<String>(); 1467 if (isPrimary) { 1468 parameterList.add(VCardConstants.PARAM_TYPE_PREF); 1469 } 1470 switch (type) { 1471 case StructuredPostal.TYPE_HOME: { 1472 parameterList.add(VCardConstants.PARAM_TYPE_HOME); 1473 break; 1474 } 1475 case StructuredPostal.TYPE_WORK: { 1476 parameterList.add(VCardConstants.PARAM_TYPE_WORK); 1477 break; 1478 } 1479 case StructuredPostal.TYPE_CUSTOM: { 1480 if (!TextUtils.isEmpty(label) 1481 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 1482 // We're not sure whether the label is valid in the spec 1483 // ("IANA-token" in the vCard 3.0 is unclear...) 1484 // Just for safety, we add "X-" at the beggining of each label. 1485 // Also checks the label obeys with vCard 3.0 spec. 1486 parameterList.add("X-" + label); 1487 } 1488 break; 1489 } 1490 case StructuredPostal.TYPE_OTHER: { 1491 break; 1492 } 1493 default: { 1494 Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); 1495 break; 1496 } 1497 } 1498 1499 mBuilder.append(VCardConstants.PROPERTY_ADR); 1500 if (!parameterList.isEmpty()) { 1501 mBuilder.append(VCARD_PARAM_SEPARATOR); 1502 appendTypeParameters(parameterList); 1503 } 1504 if (appendCharset) { 1505 // Strictly, vCard 3.0 does not allow exporters to emit charset information, 1506 // but we will add it since the information should be useful for importers, 1507 // 1508 // Assume no parser does not emit error with this parameter in vCard 3.0. 1509 mBuilder.append(VCARD_PARAM_SEPARATOR); 1510 mBuilder.append(mVCardCharsetParameter); 1511 } 1512 if (reallyUseQuotedPrintable) { 1513 mBuilder.append(VCARD_PARAM_SEPARATOR); 1514 mBuilder.append(VCARD_PARAM_ENCODING_QP); 1515 } 1516 mBuilder.append(VCARD_DATA_SEPARATOR); 1517 mBuilder.append(addressValue); 1518 mBuilder.append(VCARD_END_OF_LINE); 1519 } 1520 1521 public void appendEmailLine(final int type, final String label, 1522 final String rawValue, final boolean isPrimary) { 1523 final String typeAsString; 1524 switch (type) { 1525 case Email.TYPE_CUSTOM: { 1526 if (VCardUtils.isMobilePhoneLabel(label)) { 1527 typeAsString = VCardConstants.PARAM_TYPE_CELL; 1528 } else if (!TextUtils.isEmpty(label) 1529 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 1530 typeAsString = "X-" + label; 1531 } else { 1532 typeAsString = null; 1533 } 1534 break; 1535 } 1536 case Email.TYPE_HOME: { 1537 typeAsString = VCardConstants.PARAM_TYPE_HOME; 1538 break; 1539 } 1540 case Email.TYPE_WORK: { 1541 typeAsString = VCardConstants.PARAM_TYPE_WORK; 1542 break; 1543 } 1544 case Email.TYPE_OTHER: { 1545 typeAsString = null; 1546 break; 1547 } 1548 case Email.TYPE_MOBILE: { 1549 typeAsString = VCardConstants.PARAM_TYPE_CELL; 1550 break; 1551 } 1552 default: { 1553 Log.e(LOG_TAG, "Unknown Email type: " + type); 1554 typeAsString = null; 1555 break; 1556 } 1557 } 1558 1559 final List<String> parameterList = new ArrayList<String>(); 1560 if (isPrimary) { 1561 parameterList.add(VCardConstants.PARAM_TYPE_PREF); 1562 } 1563 if (!TextUtils.isEmpty(typeAsString)) { 1564 parameterList.add(typeAsString); 1565 } 1566 1567 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, 1568 rawValue); 1569 } 1570 1571 public void appendTelLine(final Integer typeAsInteger, final String label, 1572 final String encodedValue, boolean isPrimary) { 1573 mBuilder.append(VCardConstants.PROPERTY_TEL); 1574 mBuilder.append(VCARD_PARAM_SEPARATOR); 1575 1576 final int type; 1577 if (typeAsInteger == null) { 1578 type = Phone.TYPE_OTHER; 1579 } else { 1580 type = typeAsInteger; 1581 } 1582 1583 ArrayList<String> parameterList = new ArrayList<String>(); 1584 switch (type) { 1585 case Phone.TYPE_HOME: { 1586 parameterList.addAll( 1587 Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); 1588 break; 1589 } 1590 case Phone.TYPE_WORK: { 1591 parameterList.addAll( 1592 Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); 1593 break; 1594 } 1595 case Phone.TYPE_FAX_HOME: { 1596 parameterList.addAll( 1597 Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); 1598 break; 1599 } 1600 case Phone.TYPE_FAX_WORK: { 1601 parameterList.addAll( 1602 Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); 1603 break; 1604 } 1605 case Phone.TYPE_MOBILE: { 1606 parameterList.add(VCardConstants.PARAM_TYPE_CELL); 1607 break; 1608 } 1609 case Phone.TYPE_PAGER: { 1610 if (mIsDoCoMo) { 1611 // Not sure about the reason, but previous implementation had 1612 // used "VOICE" instead of "PAGER" 1613 parameterList.add(VCardConstants.PARAM_TYPE_VOICE); 1614 } else { 1615 parameterList.add(VCardConstants.PARAM_TYPE_PAGER); 1616 } 1617 break; 1618 } 1619 case Phone.TYPE_OTHER: { 1620 parameterList.add(VCardConstants.PARAM_TYPE_VOICE); 1621 break; 1622 } 1623 case Phone.TYPE_CAR: { 1624 parameterList.add(VCardConstants.PARAM_TYPE_CAR); 1625 break; 1626 } 1627 case Phone.TYPE_COMPANY_MAIN: { 1628 // There's no relevant field in vCard (at least 2.1). 1629 parameterList.add(VCardConstants.PARAM_TYPE_WORK); 1630 isPrimary = true; 1631 break; 1632 } 1633 case Phone.TYPE_ISDN: { 1634 parameterList.add(VCardConstants.PARAM_TYPE_ISDN); 1635 break; 1636 } 1637 case Phone.TYPE_MAIN: { 1638 isPrimary = true; 1639 break; 1640 } 1641 case Phone.TYPE_OTHER_FAX: { 1642 parameterList.add(VCardConstants.PARAM_TYPE_FAX); 1643 break; 1644 } 1645 case Phone.TYPE_TELEX: { 1646 parameterList.add(VCardConstants.PARAM_TYPE_TLX); 1647 break; 1648 } 1649 case Phone.TYPE_WORK_MOBILE: { 1650 parameterList.addAll( 1651 Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); 1652 break; 1653 } 1654 case Phone.TYPE_WORK_PAGER: { 1655 parameterList.add(VCardConstants.PARAM_TYPE_WORK); 1656 // See above. 1657 if (mIsDoCoMo) { 1658 parameterList.add(VCardConstants.PARAM_TYPE_VOICE); 1659 } else { 1660 parameterList.add(VCardConstants.PARAM_TYPE_PAGER); 1661 } 1662 break; 1663 } 1664 case Phone.TYPE_MMS: { 1665 parameterList.add(VCardConstants.PARAM_TYPE_MSG); 1666 break; 1667 } 1668 case Phone.TYPE_CUSTOM: { 1669 if (TextUtils.isEmpty(label)) { 1670 // Just ignore the custom type. 1671 parameterList.add(VCardConstants.PARAM_TYPE_VOICE); 1672 } else if (VCardUtils.isMobilePhoneLabel(label)) { 1673 parameterList.add(VCardConstants.PARAM_TYPE_CELL); 1674 } else if (mIsV30OrV40) { 1675 // This label is appropriately encoded in appendTypeParameters. 1676 parameterList.add(label); 1677 } else { 1678 final String upperLabel = label.toUpperCase(); 1679 if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { 1680 parameterList.add(upperLabel); 1681 } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 1682 // Note: Strictly, vCard 2.1 does not allow "X-" parameter without 1683 // "TYPE=" string. 1684 parameterList.add("X-" + label); 1685 } 1686 } 1687 break; 1688 } 1689 case Phone.TYPE_RADIO: 1690 case Phone.TYPE_TTY_TDD: 1691 default: { 1692 break; 1693 } 1694 } 1695 1696 if (isPrimary) { 1697 parameterList.add(VCardConstants.PARAM_TYPE_PREF); 1698 } 1699 1700 if (parameterList.isEmpty()) { 1701 appendUncommonPhoneType(mBuilder, type); 1702 } else { 1703 appendTypeParameters(parameterList); 1704 } 1705 1706 mBuilder.append(VCARD_DATA_SEPARATOR); 1707 mBuilder.append(encodedValue); 1708 mBuilder.append(VCARD_END_OF_LINE); 1709 } 1710 1711 /** 1712 * Appends phone type string which may not be available in some devices. 1713 */ 1714 private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { 1715 if (mIsDoCoMo) { 1716 // The previous implementation for DoCoMo had been conservative 1717 // about miscellaneous types. 1718 builder.append(VCardConstants.PARAM_TYPE_VOICE); 1719 } else { 1720 String phoneType = VCardUtils.getPhoneTypeString(type); 1721 if (phoneType != null) { 1722 appendTypeParameter(phoneType); 1723 } else { 1724 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); 1725 } 1726 } 1727 } 1728 1729 /** 1730 * @param encodedValue Must be encoded by BASE64 1731 * @param photoType 1732 */ 1733 public void appendPhotoLine(final String encodedValue, final String photoType) { 1734 StringBuilder tmpBuilder = new StringBuilder(); 1735 tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); 1736 tmpBuilder.append(VCARD_PARAM_SEPARATOR); 1737 if (mIsV30OrV40) { 1738 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_AS_B); 1739 } else { 1740 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); 1741 } 1742 tmpBuilder.append(VCARD_PARAM_SEPARATOR); 1743 appendTypeParameter(tmpBuilder, photoType); 1744 tmpBuilder.append(VCARD_DATA_SEPARATOR); 1745 tmpBuilder.append(encodedValue); 1746 1747 final String tmpStr = tmpBuilder.toString(); 1748 tmpBuilder = new StringBuilder(); 1749 int lineCount = 0; 1750 final int length = tmpStr.length(); 1751 final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 1752 - VCARD_END_OF_LINE.length(); 1753 final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); 1754 int maxNum = maxNumForFirstLine; 1755 for (int i = 0; i < length; i++) { 1756 tmpBuilder.append(tmpStr.charAt(i)); 1757 lineCount++; 1758 if (lineCount > maxNum) { 1759 tmpBuilder.append(VCARD_END_OF_LINE); 1760 tmpBuilder.append(VCARD_WS); 1761 maxNum = maxNumInGeneral; 1762 lineCount = 0; 1763 } 1764 } 1765 mBuilder.append(tmpBuilder.toString()); 1766 mBuilder.append(VCARD_END_OF_LINE); 1767 mBuilder.append(VCARD_END_OF_LINE); 1768 } 1769 1770 /** 1771 * SIP (Session Initiation Protocol) is first supported in RFC 4770 as part of IMPP 1772 * support. vCard 2.1 and old vCard 3.0 may not able to parse it, or expect X-SIP 1773 * instead of "IMPP;sip:...". 1774 * 1775 * We honor RFC 4770 and don't allow vCard 3.0 to emit X-SIP at all. 1776 * 1777 * vCard 4.0 is aware of RFC 4770, so just using IMPP would be fine. 1778 */ 1779 public VCardBuilder appendSipAddresses(final List<ContentValues> contentValuesList) { 1780 final boolean useXProperty; 1781 if (mIsV30OrV40) { 1782 useXProperty = false; 1783 } else if (mUsesDefactProperty){ 1784 useXProperty = true; 1785 } else { 1786 return this; 1787 } 1788 1789 if (contentValuesList != null) { 1790 for (ContentValues contentValues : contentValuesList) { 1791 String sipAddress = contentValues.getAsString(SipAddress.SIP_ADDRESS); 1792 if (TextUtils.isEmpty(sipAddress)) { 1793 continue; 1794 } 1795 if (useXProperty) { 1796 // X-SIP does not contain "sip:" prefix. 1797 if (sipAddress.startsWith("sip:")) { 1798 if (sipAddress.length() == 4) { 1799 continue; 1800 } 1801 sipAddress = sipAddress.substring(4); 1802 } 1803 // No type is available yet. 1804 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_X_SIP, sipAddress); 1805 } else { 1806 // IMPP is not just for SIP but the other protcols like XMPP. 1807 if (!sipAddress.startsWith("sip:")) { 1808 sipAddress = "sip:" + sipAddress; 1809 } 1810 appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_IMPP, sipAddress); 1811 } 1812 } 1813 } 1814 return this; 1815 } 1816 1817 public void appendAndroidSpecificProperty( 1818 final String mimeType, ContentValues contentValues) { 1819 if (!sAllowedAndroidPropertySet.contains(mimeType)) { 1820 return; 1821 } 1822 final List<String> rawValueList = new ArrayList<String>(); 1823 for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { 1824 String value = contentValues.getAsString("data" + i); 1825 if (value == null) { 1826 value = ""; 1827 } 1828 rawValueList.add(value); 1829 } 1830 1831 boolean needCharset = 1832 (mShouldAppendCharsetParam && 1833 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); 1834 boolean reallyUseQuotedPrintable = 1835 (mShouldUseQuotedPrintable && 1836 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); 1837 mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM); 1838 if (needCharset) { 1839 mBuilder.append(VCARD_PARAM_SEPARATOR); 1840 mBuilder.append(mVCardCharsetParameter); 1841 } 1842 if (reallyUseQuotedPrintable) { 1843 mBuilder.append(VCARD_PARAM_SEPARATOR); 1844 mBuilder.append(VCARD_PARAM_ENCODING_QP); 1845 } 1846 mBuilder.append(VCARD_DATA_SEPARATOR); 1847 mBuilder.append(mimeType); // Should not be encoded. 1848 for (String rawValue : rawValueList) { 1849 final String encodedValue; 1850 if (reallyUseQuotedPrintable) { 1851 encodedValue = encodeQuotedPrintable(rawValue); 1852 } else { 1853 // TODO: one line may be too huge, which may be invalid in vCard 3.0 1854 // (which says "When generating a content line, lines longer than 1855 // 75 characters SHOULD be folded"), though several 1856 // (even well-known) applications do not care this. 1857 encodedValue = escapeCharacters(rawValue); 1858 } 1859 mBuilder.append(VCARD_ITEM_SEPARATOR); 1860 mBuilder.append(encodedValue); 1861 } 1862 mBuilder.append(VCARD_END_OF_LINE); 1863 } 1864 1865 public void appendLineWithCharsetAndQPDetection(final String propertyName, 1866 final String rawValue) { 1867 appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); 1868 } 1869 1870 public void appendLineWithCharsetAndQPDetection( 1871 final String propertyName, final List<String> rawValueList) { 1872 appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); 1873 } 1874 1875 public void appendLineWithCharsetAndQPDetection(final String propertyName, 1876 final List<String> parameterList, final String rawValue) { 1877 final boolean needCharset = 1878 !VCardUtils.containsOnlyPrintableAscii(rawValue); 1879 final boolean reallyUseQuotedPrintable = 1880 (mShouldUseQuotedPrintable && 1881 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)); 1882 appendLine(propertyName, parameterList, 1883 rawValue, needCharset, reallyUseQuotedPrintable); 1884 } 1885 1886 public void appendLineWithCharsetAndQPDetection(final String propertyName, 1887 final List<String> parameterList, final List<String> rawValueList) { 1888 boolean needCharset = 1889 (mShouldAppendCharsetParam && 1890 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); 1891 boolean reallyUseQuotedPrintable = 1892 (mShouldUseQuotedPrintable && 1893 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); 1894 appendLine(propertyName, parameterList, rawValueList, 1895 needCharset, reallyUseQuotedPrintable); 1896 } 1897 1898 /** 1899 * Appends one line with a given property name and value. 1900 */ 1901 public void appendLine(final String propertyName, final String rawValue) { 1902 appendLine(propertyName, rawValue, false, false); 1903 } 1904 1905 public void appendLine(final String propertyName, final List<String> rawValueList) { 1906 appendLine(propertyName, rawValueList, false, false); 1907 } 1908 1909 public void appendLine(final String propertyName, 1910 final String rawValue, final boolean needCharset, 1911 boolean reallyUseQuotedPrintable) { 1912 appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable); 1913 } 1914 1915 public void appendLine(final String propertyName, final List<String> parameterList, 1916 final String rawValue) { 1917 appendLine(propertyName, parameterList, rawValue, false, false); 1918 } 1919 1920 public void appendLine(final String propertyName, final List<String> parameterList, 1921 final String rawValue, final boolean needCharset, 1922 boolean reallyUseQuotedPrintable) { 1923 mBuilder.append(propertyName); 1924 if (parameterList != null && parameterList.size() > 0) { 1925 mBuilder.append(VCARD_PARAM_SEPARATOR); 1926 appendTypeParameters(parameterList); 1927 } 1928 if (needCharset) { 1929 mBuilder.append(VCARD_PARAM_SEPARATOR); 1930 mBuilder.append(mVCardCharsetParameter); 1931 } 1932 1933 final String encodedValue; 1934 if (reallyUseQuotedPrintable) { 1935 mBuilder.append(VCARD_PARAM_SEPARATOR); 1936 mBuilder.append(VCARD_PARAM_ENCODING_QP); 1937 encodedValue = encodeQuotedPrintable(rawValue); 1938 } else { 1939 // TODO: one line may be too huge, which may be invalid in vCard spec, though 1940 // several (even well-known) applications do not care that violation. 1941 encodedValue = escapeCharacters(rawValue); 1942 } 1943 1944 mBuilder.append(VCARD_DATA_SEPARATOR); 1945 mBuilder.append(encodedValue); 1946 mBuilder.append(VCARD_END_OF_LINE); 1947 } 1948 1949 public void appendLine(final String propertyName, final List<String> rawValueList, 1950 final boolean needCharset, boolean needQuotedPrintable) { 1951 appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); 1952 } 1953 1954 public void appendLine(final String propertyName, final List<String> parameterList, 1955 final List<String> rawValueList, final boolean needCharset, 1956 final boolean needQuotedPrintable) { 1957 mBuilder.append(propertyName); 1958 if (parameterList != null && parameterList.size() > 0) { 1959 mBuilder.append(VCARD_PARAM_SEPARATOR); 1960 appendTypeParameters(parameterList); 1961 } 1962 if (needCharset) { 1963 mBuilder.append(VCARD_PARAM_SEPARATOR); 1964 mBuilder.append(mVCardCharsetParameter); 1965 } 1966 if (needQuotedPrintable) { 1967 mBuilder.append(VCARD_PARAM_SEPARATOR); 1968 mBuilder.append(VCARD_PARAM_ENCODING_QP); 1969 } 1970 1971 mBuilder.append(VCARD_DATA_SEPARATOR); 1972 boolean first = true; 1973 for (String rawValue : rawValueList) { 1974 final String encodedValue; 1975 if (needQuotedPrintable) { 1976 encodedValue = encodeQuotedPrintable(rawValue); 1977 } else { 1978 // TODO: one line may be too huge, which may be invalid in vCard 3.0 1979 // (which says "When generating a content line, lines longer than 1980 // 75 characters SHOULD be folded"), though several 1981 // (even well-known) applications do not care this. 1982 encodedValue = escapeCharacters(rawValue); 1983 } 1984 1985 if (first) { 1986 first = false; 1987 } else { 1988 mBuilder.append(VCARD_ITEM_SEPARATOR); 1989 } 1990 mBuilder.append(encodedValue); 1991 } 1992 mBuilder.append(VCARD_END_OF_LINE); 1993 } 1994 1995 /** 1996 * VCARD_PARAM_SEPARATOR must be appended before this method being called. 1997 */ 1998 private void appendTypeParameters(final List<String> types) { 1999 // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, 2000 // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. 2001 boolean first = true; 2002 for (final String typeValue : types) { 2003 if (VCardConfig.isVersion30(mVCardType) || VCardConfig.isVersion40(mVCardType)) { 2004 final String encoded = (VCardConfig.isVersion40(mVCardType) ? 2005 VCardUtils.toStringAsV40ParamValue(typeValue) : 2006 VCardUtils.toStringAsV30ParamValue(typeValue)); 2007 if (TextUtils.isEmpty(encoded)) { 2008 continue; 2009 } 2010 2011 if (first) { 2012 first = false; 2013 } else { 2014 mBuilder.append(VCARD_PARAM_SEPARATOR); 2015 } 2016 appendTypeParameter(encoded); 2017 } else { // vCard 2.1 2018 if (!VCardUtils.isV21Word(typeValue)) { 2019 continue; 2020 } 2021 if (first) { 2022 first = false; 2023 } else { 2024 mBuilder.append(VCARD_PARAM_SEPARATOR); 2025 } 2026 appendTypeParameter(typeValue); 2027 } 2028 } 2029 } 2030 2031 /** 2032 * VCARD_PARAM_SEPARATOR must be appended before this method being called. 2033 */ 2034 private void appendTypeParameter(final String type) { 2035 appendTypeParameter(mBuilder, type); 2036 } 2037 2038 private void appendTypeParameter(final StringBuilder builder, final String type) { 2039 // Refrain from using appendType() so that "TYPE=" is not be appended when the 2040 // device is DoCoMo's (just for safety). 2041 // 2042 // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" 2043 if (VCardConfig.isVersion40(mVCardType) || 2044 ((VCardConfig.isVersion30(mVCardType) || mAppendTypeParamName) && !mIsDoCoMo)) { 2045 builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); 2046 } 2047 builder.append(type); 2048 } 2049 2050 /** 2051 * Returns true when the property line should contain charset parameter 2052 * information. This method may return true even when vCard version is 3.0. 2053 * 2054 * Strictly, adding charset information is invalid in VCard 3.0. 2055 * However we'll add the info only when charset we use is not UTF-8 2056 * in vCard 3.0 format, since parser side may be able to use the charset 2057 * via this field, though we may encounter another problem by adding it. 2058 * 2059 * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 2060 * recommends UTF-8. By adding this field, parsers may be able 2061 * to know this text is NOT UTF-8 but Shift_Jis. 2062 */ 2063 private boolean shouldAppendCharsetParam(String...propertyValueList) { 2064 if (!mShouldAppendCharsetParam) { 2065 return false; 2066 } 2067 for (String propertyValue : propertyValueList) { 2068 if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { 2069 return true; 2070 } 2071 } 2072 return false; 2073 } 2074 2075 private String encodeQuotedPrintable(final String str) { 2076 if (TextUtils.isEmpty(str)) { 2077 return ""; 2078 } 2079 2080 final StringBuilder builder = new StringBuilder(); 2081 int index = 0; 2082 int lineCount = 0; 2083 byte[] strArray = null; 2084 2085 try { 2086 strArray = str.getBytes(mCharset); 2087 } catch (UnsupportedEncodingException e) { 2088 Log.e(LOG_TAG, "Charset " + mCharset + " cannot be used. " 2089 + "Try default charset"); 2090 strArray = str.getBytes(); 2091 } 2092 while (index < strArray.length) { 2093 builder.append(String.format("=%02X", strArray[index])); 2094 index += 1; 2095 lineCount += 3; 2096 2097 if (lineCount >= 67) { 2098 // Specification requires CRLF must be inserted before the 2099 // length of the line 2100 // becomes more than 76. 2101 // Assuming that the next character is a multi-byte character, 2102 // it will become 2103 // 6 bytes. 2104 // 76 - 6 - 3 = 67 2105 builder.append("=\r\n"); 2106 lineCount = 0; 2107 } 2108 } 2109 2110 return builder.toString(); 2111 } 2112 2113 /** 2114 * Append '\' to the characters which should be escaped. The character set is different 2115 * not only between vCard 2.1 and vCard 3.0 but also among each device. 2116 * 2117 * Note that Quoted-Printable string must not be input here. 2118 */ 2119 @SuppressWarnings("fallthrough") 2120 private String escapeCharacters(final String unescaped) { 2121 if (TextUtils.isEmpty(unescaped)) { 2122 return ""; 2123 } 2124 2125 final StringBuilder tmpBuilder = new StringBuilder(); 2126 final int length = unescaped.length(); 2127 for (int i = 0; i < length; i++) { 2128 final char ch = unescaped.charAt(i); 2129 switch (ch) { 2130 case ';': { 2131 tmpBuilder.append('\\'); 2132 tmpBuilder.append(';'); 2133 break; 2134 } 2135 case '\r': { 2136 if (i + 1 < length) { 2137 char nextChar = unescaped.charAt(i); 2138 if (nextChar == '\n') { 2139 break; 2140 } else { 2141 // fall through 2142 } 2143 } else { 2144 // fall through 2145 } 2146 } 2147 case '\n': { 2148 // In vCard 2.1, there's no specification about this, while 2149 // vCard 3.0 explicitly requires this should be encoded to "\n". 2150 tmpBuilder.append("\\n"); 2151 break; 2152 } 2153 case '\\': { 2154 if (mIsV30OrV40) { 2155 tmpBuilder.append("\\\\"); 2156 break; 2157 } else { 2158 // fall through 2159 } 2160 } 2161 case '<': 2162 case '>': { 2163 if (mIsDoCoMo) { 2164 tmpBuilder.append('\\'); 2165 tmpBuilder.append(ch); 2166 } else { 2167 tmpBuilder.append(ch); 2168 } 2169 break; 2170 } 2171 case ',': { 2172 if (mIsV30OrV40) { 2173 tmpBuilder.append("\\,"); 2174 } else { 2175 tmpBuilder.append(ch); 2176 } 2177 break; 2178 } 2179 default: { 2180 tmpBuilder.append(ch); 2181 break; 2182 } 2183 } 2184 } 2185 return tmpBuilder.toString(); 2186 } 2187 2188 @Override 2189 public String toString() { 2190 if (!mEndAppended) { 2191 if (mIsDoCoMo) { 2192 appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); 2193 appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); 2194 appendLine(VCardConstants.PROPERTY_X_NO, ""); 2195 appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); 2196 } 2197 appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); 2198 mEndAppended = true; 2199 } 2200 return mBuilder.toString(); 2201 } 2202} 2203