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