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