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