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