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