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