VCardComposer.java revision 592988d307e8d305ca163c4e58da0fb350054194
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 android.pim.vcard; 17 18import android.content.ContentResolver; 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.Entity; 22import android.content.EntityIterator; 23import android.content.Entity.NamedContentValues; 24import android.database.Cursor; 25import android.database.sqlite.SQLiteException; 26import android.net.Uri; 27import android.os.RemoteException; 28import android.provider.CallLog; 29import android.provider.CallLog.Calls; 30import android.provider.ContactsContract.Contacts; 31import android.provider.ContactsContract.Data; 32import android.provider.ContactsContract.RawContacts; 33import android.provider.ContactsContract.CommonDataKinds.Email; 34import android.provider.ContactsContract.CommonDataKinds.Event; 35import android.provider.ContactsContract.CommonDataKinds.Im; 36import android.provider.ContactsContract.CommonDataKinds.Nickname; 37import android.provider.ContactsContract.CommonDataKinds.Note; 38import android.provider.ContactsContract.CommonDataKinds.Organization; 39import android.provider.ContactsContract.CommonDataKinds.Phone; 40import android.provider.ContactsContract.CommonDataKinds.Photo; 41import android.provider.ContactsContract.CommonDataKinds.StructuredName; 42import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 43import android.provider.ContactsContract.CommonDataKinds.Website; 44import android.telephony.PhoneNumberUtils; 45import android.text.TextUtils; 46import android.text.format.Time; 47import android.util.CharsetUtils; 48import android.util.Log; 49 50import java.io.BufferedWriter; 51import java.io.FileOutputStream; 52import java.io.IOException; 53import java.io.OutputStream; 54import java.io.OutputStreamWriter; 55import java.io.UnsupportedEncodingException; 56import java.io.Writer; 57import java.nio.charset.UnsupportedCharsetException; 58import java.util.ArrayList; 59import java.util.Arrays; 60import java.util.HashMap; 61import java.util.HashSet; 62import java.util.List; 63import java.util.Map; 64import java.util.Set; 65 66/** 67 * <p> 68 * The class for composing VCard from Contacts information. Note that this is 69 * completely differnt implementation from 70 * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. 71 * </p> 72 * 73 * <p> 74 * Usually, this class should be used like this. 75 * </p> 76 * 77 * <pre class="prettyprint">VCardComposer composer = null; 78 * try { 79 * composer = new VCardComposer(context); 80 * composer.addHandler( 81 * composer.new HandlerForOutputStream(outputStream)); 82 * if (!composer.init()) { 83 * // Do something handling the situation. 84 * return; 85 * } 86 * while (!composer.isAfterLast()) { 87 * if (mCanceled) { 88 * // Assume a user may cancel this operation during the export. 89 * return; 90 * } 91 * if (!composer.createOneEntry()) { 92 * // Do something handling the error situation. 93 * return; 94 * } 95 * } 96 * } finally { 97 * if (composer != null) { 98 * composer.terminate(); 99 * } 100 * } </pre> 101 */ 102public class VCardComposer { 103 private static final String LOG_TAG = "vcard.VCardComposer"; 104 105 // TODO: Should be configurable? 106 public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; 107 public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; 108 public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; 109 110 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 111 "Failed to get database information"; 112 113 public static final String FAILURE_REASON_NO_ENTRY = 114 "There's no exportable in the database"; 115 116 public static final String FAILURE_REASON_NOT_INITIALIZED = 117 "The vCard composer object is not correctly initialized"; 118 119 /** Should be visible only from developers... (no need to translate, hopefully) */ 120 public static final String FAILURE_REASON_UNSUPPORTED_URI = 121 "The Uri vCard composer received is not supported by the composer."; 122 123 public static final String NO_ERROR = "No error"; 124 125 public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; 126 127 // Property for call log entry 128 private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; 129 private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; 130 private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; 131 private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; 132 133 private static final String VCARD_DATA_VCARD = "VCARD"; 134 private static final String VCARD_DATA_PUBLIC = "PUBLIC"; 135 136 private static final String VCARD_PARAM_SEPARATOR = ";"; 137 private static final String VCARD_END_OF_LINE = "\r\n"; 138 private static final String VCARD_DATA_SEPARATOR = ":"; 139 private static final String VCARD_ITEM_SEPARATOR = ";"; 140 private static final String VCARD_WS = " "; 141 private static final String VCARD_PARAM_EQUAL = "="; 142 143 private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; 144 145 private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; 146 private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; 147 148 private static final String SHIFT_JIS = "SHIFT_JIS"; 149 private static final String UTF_8 = "UTF-8"; 150 151 /** 152 * Special URI for testing. 153 */ 154 public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; 155 public static final Uri VCARD_TEST_AUTHORITY_URI = 156 Uri.parse("content://" + VCARD_TEST_AUTHORITY); 157 public static final Uri CONTACTS_TEST_CONTENT_URI = 158 Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); 159 160 private static final Uri sDataRequestUri; 161 private static final Map<Integer, String> sImMap; 162 163 /** 164 * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}. 165 */ 166 private static final Set<String> sPrimaryPropertyNameSet; 167 168 static { 169 Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); 170 builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); 171 sDataRequestUri = builder.build(); 172 sImMap = new HashMap<Integer, String>(); 173 sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); 174 sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); 175 sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); 176 sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); 177 sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); 178 sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); 179 // Google talk is a special case. 180 181 // TODO: incomplete. Implement properly 182 sPrimaryPropertyNameSet = new HashSet<String>(); 183 sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); 184 sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); 185 sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); 186 } 187 188 public static interface OneEntryHandler { 189 public boolean onInit(Context context); 190 public boolean onEntryCreated(String vcard); 191 public void onTerminate(); 192 } 193 194 /** 195 * <p> 196 * An useful example handler, which emits VCard String to outputstream one by one. 197 * </p> 198 * <p> 199 * The input OutputStream object is closed() on {{@link #onTerminate()}. 200 * Must not close the stream outside. 201 * </p> 202 */ 203 public class HandlerForOutputStream implements OneEntryHandler { 204 @SuppressWarnings("hiding") 205 private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; 206 207 final private OutputStream mOutputStream; // mWriter will close this. 208 private Writer mWriter; 209 210 private boolean mOnTerminateIsCalled = false; 211 212 /** 213 * Input stream will be closed on the detruction of this object. 214 */ 215 public HandlerForOutputStream(OutputStream outputStream) { 216 mOutputStream = outputStream; 217 } 218 219 public boolean onInit(Context context) { 220 try { 221 mWriter = new BufferedWriter(new OutputStreamWriter( 222 mOutputStream, mCharsetString)); 223 } catch (UnsupportedEncodingException e1) { 224 Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); 225 mErrorReason = "Encoding is not supported (usually this does not happen!): " 226 + mCharsetString; 227 return false; 228 } 229 230 if (mIsDoCoMo) { 231 try { 232 // Create one empty entry. 233 mWriter.write(createOneEntryInternal("-1")); 234 } catch (IOException e) { 235 Log.e(LOG_TAG, 236 "IOException occurred during exportOneContactData: " 237 + e.getMessage()); 238 mErrorReason = "IOException occurred: " + e.getMessage(); 239 return false; 240 } 241 } 242 return true; 243 } 244 245 public boolean onEntryCreated(String vcard) { 246 try { 247 mWriter.write(vcard); 248 } catch (IOException e) { 249 Log.e(LOG_TAG, 250 "IOException occurred during exportOneContactData: " 251 + e.getMessage()); 252 mErrorReason = "IOException occurred: " + e.getMessage(); 253 return false; 254 } 255 return true; 256 } 257 258 public void onTerminate() { 259 mOnTerminateIsCalled = true; 260 if (mWriter != null) { 261 try { 262 // Flush and sync the data so that a user is able to pull 263 // the SDCard just after 264 // the export. 265 mWriter.flush(); 266 if (mOutputStream != null 267 && mOutputStream instanceof FileOutputStream) { 268 ((FileOutputStream) mOutputStream).getFD().sync(); 269 } 270 } catch (IOException e) { 271 Log.d(LOG_TAG, 272 "IOException during closing the output stream: " 273 + e.getMessage()); 274 } finally { 275 try { 276 mWriter.close(); 277 } catch (IOException e) { 278 } 279 } 280 } 281 } 282 283 @Override 284 public void finalize() { 285 if (!mOnTerminateIsCalled) { 286 onTerminate(); 287 } 288 } 289 } 290 291 private final Context mContext; 292 private final int mVCardType; 293 private final boolean mCareHandlerErrors; 294 private final ContentResolver mContentResolver; 295 296 // Convenient member variables about the restriction of the vCard format. 297 // Used for not calling the same methods returning same results. 298 private final boolean mIsV30; 299 private final boolean mIsJapaneseMobilePhone; 300 private final boolean mOnlyOneNoteFieldIsAvailable; 301 private final boolean mIsDoCoMo; 302 private final boolean mUsesQuotedPrintable; 303 private final boolean mUsesAndroidProperty; 304 private final boolean mUsesDefactProperty; 305 private final boolean mUsesUtf8; 306 private final boolean mUsesShiftJis; 307 private final boolean mAppendTypeParamName; 308 private final boolean mRefrainsQPToPrimaryProperties; 309 private final boolean mNeedsToConvertPhoneticString; 310 311 private Cursor mCursor; 312 private int mIdColumn; 313 314 private final String mCharsetString; 315 private final String mVCardCharsetParameter; 316 private boolean mTerminateIsCalled; 317 final private List<OneEntryHandler> mHandlerList; 318 319 private String mErrorReason = NO_ERROR; 320 321 private boolean mIsCallLogComposer; 322 323 private static final String[] sContactsProjection = new String[] { 324 Contacts._ID, 325 }; 326 327 /** The projection to use when querying the call log table */ 328 private static final String[] sCallLogProjection = new String[] { 329 Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE, 330 Calls.CACHED_NUMBER_LABEL 331 }; 332 private static final int NUMBER_COLUMN_INDEX = 0; 333 private static final int DATE_COLUMN_INDEX = 1; 334 private static final int CALL_TYPE_COLUMN_INDEX = 2; 335 private static final int CALLER_NAME_COLUMN_INDEX = 3; 336 private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; 337 private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; 338 339 private static final String FLAG_TIMEZONE_UTC = "Z"; 340 341 public VCardComposer(Context context) { 342 this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); 343 } 344 345 public VCardComposer(Context context, int vcardType) { 346 this(context, vcardType, true); 347 } 348 349 public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { 350 this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); 351 } 352 353 /** 354 * Construct for supporting call log entry vCard composing. 355 */ 356 public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { 357 mContext = context; 358 mVCardType = vcardType; 359 mCareHandlerErrors = careHandlerErrors; 360 mContentResolver = context.getContentResolver(); 361 362 mIsV30 = VCardConfig.isV30(vcardType); 363 mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); 364 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 365 mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); 366 mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); 367 mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); 368 mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); 369 mUsesUtf8 = VCardConfig.usesUtf8(vcardType); 370 mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); 371 mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType); 372 mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); 373 mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); 374 mHandlerList = new ArrayList<OneEntryHandler>(); 375 376 if (mIsDoCoMo) { 377 String charset; 378 try { 379 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); 380 } catch (UnsupportedCharsetException e) { 381 Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 382 charset = SHIFT_JIS; 383 } 384 mCharsetString = charset; 385 // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but 386 // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in 387 // Android, not shown to the public). 388 mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; 389 } else if (mUsesShiftJis) { 390 String charset; 391 try { 392 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); 393 } catch (UnsupportedCharsetException e) { 394 Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 395 charset = SHIFT_JIS; 396 } 397 mCharsetString = charset; 398 mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; 399 } else { 400 mCharsetString = UTF_8; 401 mVCardCharsetParameter = "CHARSET=" + UTF_8; 402 } 403 } 404 405 /** 406 * Must call before {{@link #init()}. 407 */ 408 public void addHandler(OneEntryHandler handler) { 409 if (handler != null) { 410 mHandlerList.add(handler); 411 } 412 } 413 414 /** 415 * @return Returns true when initialization is successful and all the other 416 * methods are available. Returns false otherwise. 417 */ 418 public boolean init() { 419 return init(null, null); 420 } 421 422 public boolean init(final String selection, final String[] selectionArgs) { 423 return init(Contacts.CONTENT_URI, selection, selectionArgs, null); 424 } 425 426 /** 427 * Note that this is unstable interface, may be deleted in the future. 428 */ 429 public boolean init(final Uri contentUri, final String selection, 430 final String[] selectionArgs, final String sortOrder) { 431 if (contentUri == null) { 432 return false; 433 } 434 if (mCareHandlerErrors) { 435 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 436 mHandlerList.size()); 437 for (OneEntryHandler handler : mHandlerList) { 438 if (!handler.onInit(mContext)) { 439 for (OneEntryHandler finished : finishedList) { 440 finished.onTerminate(); 441 } 442 return false; 443 } 444 } 445 } else { 446 // Just ignore the false returned from onInit(). 447 for (OneEntryHandler handler : mHandlerList) { 448 handler.onInit(mContext); 449 } 450 } 451 452 final String[] projection; 453 if (CallLog.Calls.CONTENT_URI.equals(contentUri)) { 454 projection = sCallLogProjection; 455 mIsCallLogComposer = true; 456 } else if (Contacts.CONTENT_URI.equals(contentUri) || 457 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { 458 projection = sContactsProjection; 459 } else { 460 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 461 return false; 462 } 463 mCursor = mContentResolver.query( 464 contentUri, projection, selection, selectionArgs, sortOrder); 465 466 if (mCursor == null) { 467 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 468 return false; 469 } 470 471 if (getCount() == 0 || !mCursor.moveToFirst()) { 472 try { 473 mCursor.close(); 474 } catch (SQLiteException e) { 475 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 476 } finally { 477 mCursor = null; 478 mErrorReason = FAILURE_REASON_NO_ENTRY; 479 } 480 return false; 481 } 482 483 if (mIsCallLogComposer) { 484 mIdColumn = -1; 485 } else { 486 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 487 } 488 489 return true; 490 } 491 492 public boolean createOneEntry() { 493 if (mCursor == null || mCursor.isAfterLast()) { 494 mErrorReason = FAILURE_REASON_NOT_INITIALIZED; 495 return false; 496 } 497 String name = null; 498 String vcard; 499 try { 500 if (mIsCallLogComposer) { 501 vcard = createOneCallLogEntryInternal(); 502 } else { 503 if (mIdColumn >= 0) { 504 vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); 505 } else { 506 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); 507 return true; 508 } 509 } 510 } catch (OutOfMemoryError error) { 511 // Maybe some data (e.g. photo) is too big to have in memory. But it 512 // should be rare. 513 Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " 514 + name); 515 System.gc(); 516 // TODO: should tell users what happened? 517 return true; 518 } finally { 519 mCursor.moveToNext(); 520 } 521 522 // This function does not care the OutOfMemoryError on the handler side 523 // :-P 524 if (mCareHandlerErrors) { 525 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 526 mHandlerList.size()); 527 for (OneEntryHandler handler : mHandlerList) { 528 if (!handler.onEntryCreated(vcard)) { 529 return false; 530 } 531 } 532 } else { 533 for (OneEntryHandler handler : mHandlerList) { 534 handler.onEntryCreated(vcard); 535 } 536 } 537 538 return true; 539 } 540 541 private String createOneEntryInternal(final String contactId) { 542 final Map<String, List<ContentValues>> contentValuesListMap = 543 new HashMap<String, List<ContentValues>>(); 544 final String selection = Data.CONTACT_ID + "=?"; 545 final String[] selectionArgs = new String[] {contactId}; 546 // The resolver may return the entity iterator with no data. It is possiible. 547 // e.g. If all the data in the contact of the given contact id are not exportable ones, 548 // they are hidden from the view of this method, though contact id itself exists. 549 boolean dataExists = false; 550 EntityIterator entityIterator = null; 551 try { 552 entityIterator = mContentResolver.queryEntities( 553 sDataRequestUri, selection, selectionArgs, null); 554 dataExists = entityIterator.hasNext(); 555 while (entityIterator.hasNext()) { 556 Entity entity = entityIterator.next(); 557 for (NamedContentValues namedContentValues : entity.getSubValues()) { 558 ContentValues contentValues = namedContentValues.values; 559 String key = contentValues.getAsString(Data.MIMETYPE); 560 if (key != null) { 561 List<ContentValues> contentValuesList = 562 contentValuesListMap.get(key); 563 if (contentValuesList == null) { 564 contentValuesList = new ArrayList<ContentValues>(); 565 contentValuesListMap.put(key, contentValuesList); 566 } 567 contentValuesList.add(contentValues); 568 } 569 } 570 } 571 } catch (RemoteException e) { 572 Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)", 573 contactId, e.getMessage())); 574 return ""; 575 } finally { 576 if (entityIterator != null) { 577 entityIterator.close(); 578 } 579 } 580 581 if (!dataExists) { 582 return ""; 583 } 584 585 final StringBuilder builder = new StringBuilder(); 586 appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); 587 if (mIsV30) { 588 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); 589 } else { 590 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); 591 } 592 593 appendStructuredNames(builder, contentValuesListMap); 594 appendNickNames(builder, contentValuesListMap); 595 appendPhones(builder, contentValuesListMap); 596 appendEmails(builder, contentValuesListMap); 597 appendPostals(builder, contentValuesListMap); 598 appendIms(builder, contentValuesListMap); 599 appendWebsites(builder, contentValuesListMap); 600 appendBirthday(builder, contentValuesListMap); 601 appendOrganizations(builder, contentValuesListMap); 602 appendPhotos(builder, contentValuesListMap); 603 appendNotes(builder, contentValuesListMap); 604 // TODO: GroupMembership, Relation, Event other than birthday. 605 606 if (mIsDoCoMo) { 607 appendVCardLine(builder, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); 608 appendVCardLine(builder, Constants.PROPERTY_X_REDUCTION, ""); 609 appendVCardLine(builder, Constants.PROPERTY_X_NO, ""); 610 appendVCardLine(builder, Constants.PROPERTY_X_DCM_HMN_MODE, ""); 611 } 612 613 appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); 614 615 return builder.toString(); 616 } 617 618 public void terminate() { 619 for (OneEntryHandler handler : mHandlerList) { 620 handler.onTerminate(); 621 } 622 623 if (mCursor != null) { 624 try { 625 mCursor.close(); 626 } catch (SQLiteException e) { 627 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " 628 + e.getMessage()); 629 } 630 mCursor = null; 631 } 632 633 mTerminateIsCalled = true; 634 } 635 636 @Override 637 public void finalize() { 638 if (!mTerminateIsCalled) { 639 terminate(); 640 } 641 } 642 643 public int getCount() { 644 if (mCursor == null) { 645 return 0; 646 } 647 return mCursor.getCount(); 648 } 649 650 public boolean isAfterLast() { 651 if (mCursor == null) { 652 return false; 653 } 654 return mCursor.isAfterLast(); 655 } 656 657 /** 658 * @return Return the error reason if possible. 659 */ 660 public String getErrorReason() { 661 return mErrorReason; 662 } 663 664 private void appendStructuredNames(final StringBuilder builder, 665 final Map<String, List<ContentValues>> contentValuesListMap) { 666 final List<ContentValues> contentValuesList = contentValuesListMap 667 .get(StructuredName.CONTENT_ITEM_TYPE); 668 if (contentValuesList != null && contentValuesList.size() > 0) { 669 appendStructuredNamesInternal(builder, contentValuesList); 670 } else if (mIsDoCoMo) { 671 appendVCardLine(builder, Constants.PROPERTY_N, ""); 672 } else if (mIsV30) { 673 // vCard 3.0 requires "N" and "FN" properties. 674 appendVCardLine(builder, Constants.PROPERTY_N, ""); 675 appendVCardLine(builder, Constants.PROPERTY_FN, ""); 676 } 677 } 678 679 private boolean containsNonEmptyName(ContentValues contentValues) { 680 final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); 681 final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); 682 final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); 683 final String prefix = contentValues.getAsString(StructuredName.PREFIX); 684 final String suffix = contentValues.getAsString(StructuredName.SUFFIX); 685 final String phoneticFamilyName = 686 contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); 687 final String phoneticMiddleName = 688 contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); 689 final String phoneticGivenName = 690 contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); 691 final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); 692 return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && 693 TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && 694 TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) && 695 TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && 696 TextUtils.isEmpty(displayName)); 697 } 698 699 private void appendStructuredNamesInternal(final StringBuilder builder, 700 final List<ContentValues> contentValuesList) { 701 // For safety, we'll emit just one value around StructuredName, as external importers 702 // may get confused with multiple "N", "FN", etc. properties, though it is valid in 703 // vCard spec. 704 ContentValues primaryContentValues = null; 705 ContentValues subprimaryContentValues = null; 706 for (ContentValues contentValues : contentValuesList) { 707 if (contentValues == null){ 708 continue; 709 } 710 Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); 711 if (isSuperPrimary != null && isSuperPrimary > 0) { 712 // We choose "super primary" ContentValues. 713 primaryContentValues = contentValues; 714 break; 715 } else if (primaryContentValues == null) { 716 // We choose the first "primary" ContentValues 717 // if "super primary" ContentValues does not exist. 718 Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); 719 if (isPrimary != null && isPrimary > 0 && 720 containsNonEmptyName(contentValues)) { 721 primaryContentValues = contentValues; 722 // Do not break, since there may be ContentValues with "super primary" 723 // afterword. 724 } else if (subprimaryContentValues == null && 725 containsNonEmptyName(contentValues)) { 726 subprimaryContentValues = contentValues; 727 } 728 } 729 } 730 731 if (primaryContentValues == null) { 732 if (subprimaryContentValues != null) { 733 // We choose the first ContentValues if any "primary" ContentValues does not exist. 734 primaryContentValues = subprimaryContentValues; 735 } else { 736 Log.e(LOG_TAG, "All ContentValues given from database is empty."); 737 primaryContentValues = new ContentValues(); 738 } 739 } 740 741 final String familyName = primaryContentValues.getAsString(StructuredName.FAMILY_NAME); 742 final String middleName = primaryContentValues.getAsString(StructuredName.MIDDLE_NAME); 743 final String givenName = primaryContentValues.getAsString(StructuredName.GIVEN_NAME); 744 final String prefix = primaryContentValues.getAsString(StructuredName.PREFIX); 745 final String suffix = primaryContentValues.getAsString(StructuredName.SUFFIX); 746 final String displayName = primaryContentValues.getAsString(StructuredName.DISPLAY_NAME); 747 748 if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { 749 final boolean shouldAppendCharsetParameterToName = 750 !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && 751 shouldAppendCharsetParameters(Arrays.asList( 752 familyName, givenName, middleName, prefix, suffix)); 753 final boolean reallyUseQuotedPrintableToName = 754 (!mRefrainsQPToPrimaryProperties && 755 !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && 756 VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && 757 VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && 758 VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && 759 VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); 760 761 final String formattedName; 762 if (!TextUtils.isEmpty(displayName)) { 763 formattedName = displayName; 764 } else { 765 formattedName = VCardUtils.constructNameFromElements( 766 VCardConfig.getNameOrderType(mVCardType), 767 familyName, middleName, givenName, prefix, suffix); 768 } 769 final boolean shouldAppendCharsetParameterToFN = 770 !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && 771 shouldAppendCharsetParameter(formattedName); 772 final boolean reallyUseQuotedPrintableToFN = 773 !mRefrainsQPToPrimaryProperties && 774 !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); 775 776 final String encodedFamily; 777 final String encodedGiven; 778 final String encodedMiddle; 779 final String encodedPrefix; 780 final String encodedSuffix; 781 if (reallyUseQuotedPrintableToName) { 782 encodedFamily = encodeQuotedPrintable(familyName); 783 encodedGiven = encodeQuotedPrintable(givenName); 784 encodedMiddle = encodeQuotedPrintable(middleName); 785 encodedPrefix = encodeQuotedPrintable(prefix); 786 encodedSuffix = encodeQuotedPrintable(suffix); 787 } else { 788 encodedFamily = escapeCharacters(familyName); 789 encodedGiven = escapeCharacters(givenName); 790 encodedMiddle = escapeCharacters(middleName); 791 encodedPrefix = escapeCharacters(prefix); 792 encodedSuffix = escapeCharacters(suffix); 793 } 794 795 final String encodedFormattedname = 796 (reallyUseQuotedPrintableToFN ? 797 encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); 798 799 builder.append(Constants.PROPERTY_N); 800 if (mIsDoCoMo) { 801 if (shouldAppendCharsetParameterToName) { 802 builder.append(VCARD_PARAM_SEPARATOR); 803 builder.append(mVCardCharsetParameter); 804 } 805 if (reallyUseQuotedPrintableToName) { 806 builder.append(VCARD_PARAM_SEPARATOR); 807 builder.append(VCARD_PARAM_ENCODING_QP); 808 } 809 builder.append(VCARD_DATA_SEPARATOR); 810 // DoCoMo phones require that all the elements in the "family name" field. 811 builder.append(formattedName); 812 builder.append(VCARD_ITEM_SEPARATOR); 813 builder.append(VCARD_ITEM_SEPARATOR); 814 builder.append(VCARD_ITEM_SEPARATOR); 815 builder.append(VCARD_ITEM_SEPARATOR); 816 } else { 817 if (shouldAppendCharsetParameterToName) { 818 builder.append(VCARD_PARAM_SEPARATOR); 819 builder.append(mVCardCharsetParameter); 820 } 821 if (reallyUseQuotedPrintableToName) { 822 builder.append(VCARD_PARAM_SEPARATOR); 823 builder.append(VCARD_PARAM_ENCODING_QP); 824 } 825 builder.append(VCARD_DATA_SEPARATOR); 826 builder.append(encodedFamily); 827 builder.append(VCARD_ITEM_SEPARATOR); 828 builder.append(encodedGiven); 829 builder.append(VCARD_ITEM_SEPARATOR); 830 builder.append(encodedMiddle); 831 builder.append(VCARD_ITEM_SEPARATOR); 832 builder.append(encodedPrefix); 833 builder.append(VCARD_ITEM_SEPARATOR); 834 builder.append(encodedSuffix); 835 } 836 builder.append(VCARD_END_OF_LINE); 837 838 // FN property 839 builder.append(Constants.PROPERTY_FN); 840 if (shouldAppendCharsetParameterToFN) { 841 builder.append(VCARD_PARAM_SEPARATOR); 842 builder.append(mVCardCharsetParameter); 843 } 844 if (reallyUseQuotedPrintableToFN) { 845 builder.append(VCARD_PARAM_SEPARATOR); 846 builder.append(VCARD_PARAM_ENCODING_QP); 847 } 848 builder.append(VCARD_DATA_SEPARATOR); 849 builder.append(encodedFormattedname); 850 builder.append(VCARD_END_OF_LINE); 851 } else if (!TextUtils.isEmpty(displayName)) { 852 final boolean reallyUseQuotedPrintableToDisplayName = 853 (!mRefrainsQPToPrimaryProperties && 854 !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); 855 final String encodedDisplayName = 856 reallyUseQuotedPrintableToDisplayName ? 857 encodeQuotedPrintable(displayName) : 858 escapeCharacters(displayName); 859 860 builder.append(Constants.PROPERTY_N); 861 if (shouldAppendCharsetParameter(displayName)) { 862 builder.append(VCARD_PARAM_SEPARATOR); 863 builder.append(mVCardCharsetParameter); 864 } 865 if (reallyUseQuotedPrintableToDisplayName) { 866 builder.append(VCARD_PARAM_SEPARATOR); 867 builder.append(VCARD_PARAM_ENCODING_QP); 868 } 869 builder.append(VCARD_DATA_SEPARATOR); 870 builder.append(encodedDisplayName); 871 builder.append(VCARD_ITEM_SEPARATOR); 872 builder.append(VCARD_ITEM_SEPARATOR); 873 builder.append(VCARD_ITEM_SEPARATOR); 874 builder.append(VCARD_ITEM_SEPARATOR); 875 builder.append(VCARD_END_OF_LINE); 876 builder.append(Constants.PROPERTY_FN); 877 878 // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it 879 // when it would be useful for external importers, assuming no external 880 // importer allows this vioration. 881 if (shouldAppendCharsetParameter(displayName)) { 882 builder.append(VCARD_PARAM_SEPARATOR); 883 builder.append(mVCardCharsetParameter); 884 } 885 builder.append(VCARD_DATA_SEPARATOR); 886 builder.append(encodedDisplayName); 887 builder.append(VCARD_END_OF_LINE); 888 } else if (mIsV30) { 889 // vCard 3.0 specification requires these fields. 890 appendVCardLine(builder, Constants.PROPERTY_N, ""); 891 appendVCardLine(builder, Constants.PROPERTY_FN, ""); 892 } else if (mIsDoCoMo) { 893 appendVCardLine(builder, Constants.PROPERTY_N, ""); 894 } 895 896 final String phoneticFamilyName; 897 final String phoneticMiddleName; 898 final String phoneticGivenName; 899 { 900 String tmpPhoneticFamilyName = 901 primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); 902 String tmpPhoneticMiddleName = 903 primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); 904 String tmpPhoneticGivenName = 905 primaryContentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); 906 if (mNeedsToConvertPhoneticString) { 907 phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); 908 phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); 909 phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); 910 } else { 911 phoneticFamilyName = tmpPhoneticFamilyName; 912 phoneticMiddleName = tmpPhoneticMiddleName; 913 phoneticGivenName = tmpPhoneticGivenName; 914 } 915 } 916 if (!(TextUtils.isEmpty(phoneticFamilyName) 917 && TextUtils.isEmpty(phoneticMiddleName) 918 && TextUtils.isEmpty(phoneticGivenName))) { 919 920 if (mIsV30) { 921 final String sortString = VCardUtils 922 .constructNameFromElements(mVCardType, 923 phoneticFamilyName, phoneticMiddleName, phoneticGivenName); 924 builder.append(Constants.PROPERTY_SORT_STRING); 925 if (shouldAppendCharsetParameter(sortString)) { 926 builder.append(VCARD_PARAM_SEPARATOR); 927 builder.append(mVCardCharsetParameter); 928 } 929 builder.append(VCARD_DATA_SEPARATOR); 930 builder.append(escapeCharacters(sortString)); 931 builder.append(VCARD_END_OF_LINE); 932 } else if (mIsJapaneseMobilePhone) { 933 // Note: There is no appropriate property for expressing 934 // phonetic name in vCard 2.1, while there is in 935 // vCard 3.0 (SORT-STRING). 936 // We chose to use DoCoMo's way when the device is Japanese one 937 // since it is supported by 938 // a lot of Japanese mobile phones. This is "X-" property, so 939 // any parser hopefully would not get confused with this. 940 builder.append(Constants.PROPERTY_SOUND); 941 builder.append(VCARD_PARAM_SEPARATOR); 942 builder.append(Constants.PARAM_TYPE_X_IRMC_N); 943 944 boolean reallyUseQuotedPrintable = 945 (!mRefrainsQPToPrimaryProperties 946 && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( 947 phoneticFamilyName) 948 && VCardUtils.containsOnlyNonCrLfPrintableAscii( 949 phoneticMiddleName) 950 && VCardUtils.containsOnlyNonCrLfPrintableAscii( 951 phoneticGivenName))); 952 953 final String encodedPhoneticFamilyName; 954 final String encodedPhoneticMiddleName; 955 final String encodedPhoneticGivenName; 956 if (reallyUseQuotedPrintable) { 957 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); 958 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); 959 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); 960 } else { 961 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); 962 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); 963 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); 964 } 965 966 if (shouldAppendCharsetParameters(Arrays.asList( 967 encodedPhoneticFamilyName, encodedPhoneticMiddleName, 968 encodedPhoneticGivenName))) { 969 builder.append(VCARD_PARAM_SEPARATOR); 970 builder.append(mVCardCharsetParameter); 971 } 972 builder.append(VCARD_DATA_SEPARATOR); 973 // DoCoMo's specification requires vCard composer to use just the first 974 // column. 975 { 976 boolean first = true; 977 if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { 978 builder.append(encodedPhoneticFamilyName); 979 first = false; 980 } 981 if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { 982 if (first) { 983 first = false; 984 } else { 985 builder.append(' '); 986 } 987 builder.append(encodedPhoneticMiddleName); 988 } 989 if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { 990 if (!first) { 991 builder.append(' '); 992 } 993 builder.append(encodedPhoneticGivenName); 994 } 995 } 996 builder.append(VCARD_ITEM_SEPARATOR); 997 builder.append(VCARD_ITEM_SEPARATOR); 998 builder.append(VCARD_ITEM_SEPARATOR); 999 builder.append(VCARD_ITEM_SEPARATOR); 1000 builder.append(VCARD_END_OF_LINE); 1001 } 1002 } else if (mIsDoCoMo) { 1003 builder.append(Constants.PROPERTY_SOUND); 1004 builder.append(VCARD_PARAM_SEPARATOR); 1005 builder.append(Constants.PARAM_TYPE_X_IRMC_N); 1006 builder.append(VCARD_DATA_SEPARATOR); 1007 builder.append(VCARD_ITEM_SEPARATOR); 1008 builder.append(VCARD_ITEM_SEPARATOR); 1009 builder.append(VCARD_ITEM_SEPARATOR); 1010 builder.append(VCARD_ITEM_SEPARATOR); 1011 builder.append(VCARD_END_OF_LINE); 1012 } 1013 1014 if (mUsesDefactProperty) { 1015 if (!TextUtils.isEmpty(phoneticGivenName)) { 1016 final boolean reallyUseQuotedPrintable = 1017 (mUsesQuotedPrintable && 1018 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); 1019 final String encodedPhoneticGivenName; 1020 if (reallyUseQuotedPrintable) { 1021 encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); 1022 } else { 1023 encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); 1024 } 1025 builder.append(Constants.PROPERTY_X_PHONETIC_FIRST_NAME); 1026 if (shouldAppendCharsetParameter(phoneticGivenName)) { 1027 builder.append(VCARD_PARAM_SEPARATOR); 1028 builder.append(mVCardCharsetParameter); 1029 } 1030 if (reallyUseQuotedPrintable) { 1031 builder.append(VCARD_PARAM_SEPARATOR); 1032 builder.append(VCARD_PARAM_ENCODING_QP); 1033 } 1034 builder.append(VCARD_DATA_SEPARATOR); 1035 builder.append(encodedPhoneticGivenName); 1036 builder.append(VCARD_END_OF_LINE); 1037 } 1038 if (!TextUtils.isEmpty(phoneticMiddleName)) { 1039 final boolean reallyUseQuotedPrintable = 1040 (mUsesQuotedPrintable && 1041 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); 1042 final String encodedPhoneticMiddleName; 1043 if (reallyUseQuotedPrintable) { 1044 encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); 1045 } else { 1046 encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); 1047 } 1048 builder.append(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME); 1049 if (shouldAppendCharsetParameter(phoneticMiddleName)) { 1050 builder.append(VCARD_PARAM_SEPARATOR); 1051 builder.append(mVCardCharsetParameter); 1052 } 1053 if (reallyUseQuotedPrintable) { 1054 builder.append(VCARD_PARAM_SEPARATOR); 1055 builder.append(VCARD_PARAM_ENCODING_QP); 1056 } 1057 builder.append(VCARD_DATA_SEPARATOR); 1058 builder.append(encodedPhoneticMiddleName); 1059 builder.append(VCARD_END_OF_LINE); 1060 } 1061 if (!TextUtils.isEmpty(phoneticFamilyName)) { 1062 final boolean reallyUseQuotedPrintable = 1063 (mUsesQuotedPrintable && 1064 !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); 1065 final String encodedPhoneticFamilyName; 1066 if (reallyUseQuotedPrintable) { 1067 encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); 1068 } else { 1069 encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); 1070 } 1071 builder.append(Constants.PROPERTY_X_PHONETIC_LAST_NAME); 1072 if (shouldAppendCharsetParameter(phoneticFamilyName)) { 1073 builder.append(VCARD_PARAM_SEPARATOR); 1074 builder.append(mVCardCharsetParameter); 1075 } 1076 if (reallyUseQuotedPrintable) { 1077 builder.append(VCARD_PARAM_SEPARATOR); 1078 builder.append(VCARD_PARAM_ENCODING_QP); 1079 } 1080 builder.append(VCARD_DATA_SEPARATOR); 1081 builder.append(encodedPhoneticFamilyName); 1082 builder.append(VCARD_END_OF_LINE); 1083 } 1084 } 1085 } 1086 1087 private void appendNickNames(final StringBuilder builder, 1088 final Map<String, List<ContentValues>> contentValuesListMap) { 1089 final List<ContentValues> contentValuesList = contentValuesListMap 1090 .get(Nickname.CONTENT_ITEM_TYPE); 1091 if (contentValuesList == null) { 1092 return; 1093 } 1094 1095 final boolean useAndroidProperty; 1096 if (mIsV30) { 1097 useAndroidProperty = false; 1098 } else if (mUsesAndroidProperty) { 1099 useAndroidProperty = true; 1100 } else { 1101 // There's no way to add this field. 1102 return; 1103 } 1104 1105 for (ContentValues contentValues : contentValuesList) { 1106 final String nickname = contentValues.getAsString(Nickname.NAME); 1107 if (TextUtils.isEmpty(nickname)) { 1108 continue; 1109 } 1110 if (useAndroidProperty) { 1111 appendAndroidSpecificProperty(builder, Nickname.CONTENT_ITEM_TYPE, 1112 contentValues); 1113 } else { 1114 appendVCardLineWithCharsetAndQPDetection(builder, 1115 Constants.PROPERTY_NICKNAME, nickname); 1116 } 1117 } 1118 } 1119 1120 private void appendPhones(final StringBuilder builder, 1121 final Map<String, List<ContentValues>> contentValuesListMap) { 1122 final List<ContentValues> contentValuesList = contentValuesListMap 1123 .get(Phone.CONTENT_ITEM_TYPE); 1124 boolean phoneLineExists = false; 1125 if (contentValuesList != null) { 1126 Set<String> phoneSet = new HashSet<String>(); 1127 for (ContentValues contentValues : contentValuesList) { 1128 final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); 1129 final String label = contentValues.getAsString(Phone.LABEL); 1130 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); 1131 final boolean isPrimary = (isPrimaryAsInteger != null ? 1132 (isPrimaryAsInteger > 0) : false); 1133 String phoneNumber = contentValues.getAsString(Phone.NUMBER); 1134 if (phoneNumber != null) { 1135 phoneNumber = phoneNumber.trim(); 1136 } 1137 if (TextUtils.isEmpty(phoneNumber)) { 1138 continue; 1139 } 1140 int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); 1141 if (type == Phone.TYPE_PAGER) { 1142 phoneLineExists = true; 1143 if (!phoneSet.contains(phoneNumber)) { 1144 phoneSet.add(phoneNumber); 1145 appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary); 1146 } 1147 } else { 1148 // The entry "may" have several phone numbers when the contact entry is 1149 // corrupted because of its original source. 1150 // 1151 // e.g. I encountered the entry like the following. 1152 // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." 1153 // This kind of entry is not able to be inserted via Android devices, but 1154 // possible if the source of the data is already corrupted. 1155 List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); 1156 if (phoneNumberList.isEmpty()) { 1157 continue; 1158 } 1159 phoneLineExists = true; 1160 for (String actualPhoneNumber : phoneNumberList) { 1161 if (!phoneSet.contains(actualPhoneNumber)) { 1162 final int format = VCardUtils.getPhoneNumberFormat(mVCardType); 1163 final String formattedPhoneNumber = 1164 PhoneNumberUtils.formatNumber(actualPhoneNumber, format); 1165 phoneSet.add(actualPhoneNumber); 1166 appendVCardTelephoneLine(builder, type, label, 1167 formattedPhoneNumber, isPrimary); 1168 } 1169 } 1170 } 1171 } 1172 } 1173 1174 if (!phoneLineExists && mIsDoCoMo) { 1175 appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false); 1176 } 1177 } 1178 1179 private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { 1180 List<String> phoneList = new ArrayList<String>(); 1181 1182 StringBuilder builder = new StringBuilder(); 1183 final int length = phoneNumber.length(); 1184 for (int i = 0; i < length; i++) { 1185 final char ch = phoneNumber.charAt(i); 1186 if (Character.isDigit(ch)) { 1187 builder.append(ch); 1188 } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { 1189 phoneList.add(builder.toString()); 1190 builder = new StringBuilder(); 1191 } 1192 } 1193 if (builder.length() > 0) { 1194 phoneList.add(builder.toString()); 1195 } 1196 1197 return phoneList; 1198 } 1199 1200 private void appendEmails(final StringBuilder builder, 1201 final Map<String, List<ContentValues>> contentValuesListMap) { 1202 final List<ContentValues> contentValuesList = contentValuesListMap 1203 .get(Email.CONTENT_ITEM_TYPE); 1204 1205 boolean emailAddressExists = false; 1206 if (contentValuesList != null) { 1207 final Set<String> addressSet = new HashSet<String>(); 1208 for (ContentValues contentValues : contentValuesList) { 1209 String emailAddress = contentValues.getAsString(Email.DATA); 1210 if (emailAddress != null) { 1211 emailAddress = emailAddress.trim(); 1212 } 1213 if (TextUtils.isEmpty(emailAddress)) { 1214 continue; 1215 } 1216 Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); 1217 final int type = (typeAsObject != null ? 1218 typeAsObject : DEFAULT_EMAIL_TYPE); 1219 final String label = contentValues.getAsString(Email.LABEL); 1220 Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); 1221 final boolean isPrimary = (isPrimaryAsInteger != null ? 1222 (isPrimaryAsInteger > 0) : false); 1223 emailAddressExists = true; 1224 if (!addressSet.contains(emailAddress)) { 1225 addressSet.add(emailAddress); 1226 appendVCardEmailLine(builder, type, label, emailAddress, isPrimary); 1227 } 1228 } 1229 } 1230 1231 if (!emailAddressExists && mIsDoCoMo) { 1232 appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false); 1233 } 1234 } 1235 1236 private void appendPostals(final StringBuilder builder, 1237 final Map<String, List<ContentValues>> contentValuesListMap) { 1238 final List<ContentValues> contentValuesList = contentValuesListMap 1239 .get(StructuredPostal.CONTENT_ITEM_TYPE); 1240 if (contentValuesList != null) { 1241 if (mIsDoCoMo) { 1242 appendPostalsForDoCoMo(builder, contentValuesList); 1243 } else { 1244 appendPostalsForGeneric(builder, contentValuesList); 1245 } 1246 } else if (mIsDoCoMo) { 1247 builder.append(Constants.PROPERTY_ADR); 1248 builder.append(VCARD_PARAM_SEPARATOR); 1249 builder.append(Constants.PARAM_TYPE_HOME); 1250 builder.append(VCARD_DATA_SEPARATOR); 1251 builder.append(VCARD_END_OF_LINE); 1252 } 1253 } 1254 1255 /** 1256 * Tries to append just one line. If there's no appropriate address 1257 * information, append an empty line. 1258 */ 1259 private void appendPostalsForDoCoMo(final StringBuilder builder, 1260 final List<ContentValues> contentValuesList) { 1261 // TODO: from old, inefficient code. fix this. 1262 if (appendPostalsForDoCoMoInternal(builder, contentValuesList, 1263 StructuredPostal.TYPE_HOME)) { 1264 return; 1265 } 1266 if (appendPostalsForDoCoMoInternal(builder, contentValuesList, 1267 StructuredPostal.TYPE_WORK)) { 1268 return; 1269 } 1270 if (appendPostalsForDoCoMoInternal(builder, contentValuesList, 1271 StructuredPostal.TYPE_OTHER)) { 1272 return; 1273 } 1274 if (appendPostalsForDoCoMoInternal(builder, contentValuesList, 1275 StructuredPostal.TYPE_CUSTOM)) { 1276 return; 1277 } 1278 1279 Log.w(LOG_TAG, 1280 "Should not come here. Must have at least one postal data."); 1281 } 1282 1283 private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, 1284 final List<ContentValues> contentValuesList, Integer preferedType) { 1285 for (ContentValues contentValues : contentValuesList) { 1286 final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); 1287 final String label = contentValues.getAsString(StructuredPostal.LABEL); 1288 if (type == preferedType) { 1289 // Note: Not sure why we need to emit "empty" line even when actual 1290 // data does not exist. There may be some reason or may not. 1291 // We keep safer side since the previous implementation did so. 1292 appendVCardPostalLine(builder, type, label, contentValues, true, true); 1293 return true; 1294 } 1295 } 1296 return false; 1297 } 1298 1299 private void appendPostalsForGeneric(final StringBuilder builder, 1300 final List<ContentValues> contentValuesList) { 1301 for (ContentValues contentValues : contentValuesList) { 1302 if (contentValues == null) { 1303 continue; 1304 } 1305 final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE); 1306 final int type = (typeAsObject != null ? 1307 typeAsObject : DEFAULT_POSTAL_TYPE); 1308 final String label = contentValues.getAsString(StructuredPostal.LABEL); 1309 final Integer isPrimaryAsInteger = 1310 contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); 1311 final boolean isPrimary = (isPrimaryAsInteger != null ? 1312 (isPrimaryAsInteger > 0) : false); 1313 appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false); 1314 } 1315 } 1316 1317 private void appendIms(final StringBuilder builder, 1318 final Map<String, List<ContentValues>> contentValuesListMap) { 1319 final List<ContentValues> contentValuesList = contentValuesListMap 1320 .get(Im.CONTENT_ITEM_TYPE); 1321 if (contentValuesList == null) { 1322 return; 1323 } 1324 for (ContentValues contentValues : contentValuesList) { 1325 final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); 1326 if (protocolAsObject == null) { 1327 continue; 1328 } 1329 final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); 1330 if (propertyName == null) { 1331 continue; 1332 } 1333 String data = contentValues.getAsString(Im.DATA); 1334 if (data != null) { 1335 data = data.trim(); 1336 } 1337 if (TextUtils.isEmpty(data)) { 1338 continue; 1339 } 1340 final String typeAsString; 1341 { 1342 final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); 1343 switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { 1344 case Im.TYPE_HOME: { 1345 typeAsString = Constants.PARAM_TYPE_HOME; 1346 break; 1347 } 1348 case Im.TYPE_WORK: { 1349 typeAsString = Constants.PARAM_TYPE_WORK; 1350 break; 1351 } 1352 case Im.TYPE_CUSTOM: { 1353 final String label = contentValues.getAsString(Im.LABEL); 1354 typeAsString = (label != null ? "X-" + label : null); 1355 break; 1356 } 1357 case Im.TYPE_OTHER: // Ignore 1358 default: { 1359 typeAsString = null; 1360 break; 1361 } 1362 } 1363 } 1364 1365 List<String> parameterList = new ArrayList<String>(); 1366 if (!TextUtils.isEmpty(typeAsString)) { 1367 parameterList.add(typeAsString); 1368 } 1369 final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); 1370 final boolean isPrimary = (isPrimaryAsInteger != null ? 1371 (isPrimaryAsInteger > 0) : false); 1372 if (isPrimary) { 1373 parameterList.add(Constants.PARAM_TYPE_PREF); 1374 } 1375 1376 appendVCardLineWithCharsetAndQPDetection( 1377 builder, propertyName, parameterList, data); 1378 } 1379 } 1380 1381 private void appendWebsites(final StringBuilder builder, 1382 final Map<String, List<ContentValues>> contentValuesListMap) { 1383 final List<ContentValues> contentValuesList = contentValuesListMap 1384 .get(Website.CONTENT_ITEM_TYPE); 1385 if (contentValuesList == null) { 1386 return; 1387 } 1388 for (ContentValues contentValues : contentValuesList) { 1389 String website = contentValues.getAsString(Website.URL); 1390 if (website != null) { 1391 website = website.trim(); 1392 } 1393 1394 // Note: vCard 3.0 does not allow any parameter addition toward "URL" 1395 // property, while there's no document in vCard 2.1. 1396 // 1397 // TODO: Should we allow adding it when appropriate? 1398 // (Actually, we drop some data. Using "group.X-URL-TYPE" or something 1399 // may help) 1400 if (!TextUtils.isEmpty(website)) { 1401 appendVCardLine(builder, Constants.PROPERTY_URL, website); 1402 } 1403 } 1404 } 1405 1406 /** 1407 * Theoretically, there must be only one birthday for each vCard entry. 1408 * Also, we are afraid of some importer's parse error during its import. 1409 * We emit only one birthday entry even when there are more than one. 1410 */ 1411 private void appendBirthday(final StringBuilder builder, 1412 final Map<String, List<ContentValues>> contentValuesListMap) { 1413 final List<ContentValues> contentValuesList = 1414 contentValuesListMap.get(Event.CONTENT_ITEM_TYPE); 1415 if (contentValuesList == null) { 1416 return; 1417 } 1418 String primaryBirthday = null; 1419 String secondaryBirthday = null; 1420 for (ContentValues contentValues : contentValuesList) { 1421 if (contentValues == null) { 1422 continue; 1423 } 1424 final Integer eventType = contentValues.getAsInteger(Event.TYPE); 1425 if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { 1426 continue; 1427 } 1428 final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); 1429 if (birthdayCandidate == null) { 1430 continue; 1431 } 1432 final Integer isSuperPrimaryAsInteger = 1433 contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); 1434 final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? 1435 (isSuperPrimaryAsInteger > 0) : false); 1436 if (isSuperPrimary) { 1437 // "super primary" birthday should the prefered one. 1438 primaryBirthday = birthdayCandidate; 1439 break; 1440 } 1441 final Integer isPrimaryAsInteger = 1442 contentValues.getAsInteger(Event.IS_PRIMARY); 1443 final boolean isPrimary = (isPrimaryAsInteger != null ? 1444 (isPrimaryAsInteger > 0) : false); 1445 if (isPrimary) { 1446 // We don't break here since "super primary" birthday may exist later. 1447 primaryBirthday = birthdayCandidate; 1448 } else if (secondaryBirthday == null) { 1449 // First entry is set to the "secondary" candidate. 1450 secondaryBirthday = birthdayCandidate; 1451 } 1452 } 1453 1454 final String birthday; 1455 if (primaryBirthday != null) { 1456 birthday = primaryBirthday.trim(); 1457 } else if (secondaryBirthday != null){ 1458 birthday = secondaryBirthday.trim(); 1459 } else { 1460 return; 1461 } 1462 appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_BDAY, birthday); 1463 } 1464 1465 private void appendOrganizations(final StringBuilder builder, 1466 final Map<String, List<ContentValues>> contentValuesListMap) { 1467 final List<ContentValues> contentValuesList = contentValuesListMap 1468 .get(Organization.CONTENT_ITEM_TYPE); 1469 if (contentValuesList != null) { 1470 for (ContentValues contentValues : contentValuesList) { 1471 String company = contentValues.getAsString(Organization.COMPANY); 1472 if (company != null) { 1473 company = company.trim(); 1474 } 1475 String department = contentValues.getAsString(Organization.DEPARTMENT); 1476 if (department != null) { 1477 department = department.trim(); 1478 } 1479 String title = contentValues.getAsString(Organization.TITLE); 1480 if (title != null) { 1481 title = title.trim(); 1482 } 1483 1484 StringBuilder orgBuilder = new StringBuilder(); 1485 if (!TextUtils.isEmpty(company)) { 1486 orgBuilder.append(company); 1487 } 1488 if (!TextUtils.isEmpty(department)) { 1489 if (orgBuilder.length() > 0) { 1490 orgBuilder.append(';'); 1491 } 1492 orgBuilder.append(department); 1493 } 1494 final String orgline = orgBuilder.toString(); 1495 appendVCardLine(builder, Constants.PROPERTY_ORG, orgline, 1496 !VCardUtils.containsOnlyPrintableAscii(orgline), 1497 (mUsesQuotedPrintable && 1498 !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); 1499 1500 if (!TextUtils.isEmpty(title)) { 1501 appendVCardLine(builder, Constants.PROPERTY_TITLE, title, 1502 !VCardUtils.containsOnlyPrintableAscii(title), 1503 (mUsesQuotedPrintable && 1504 !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); 1505 } 1506 } 1507 } 1508 } 1509 1510 private void appendPhotos(final StringBuilder builder, 1511 final Map<String, List<ContentValues>> contentValuesListMap) { 1512 final List<ContentValues> contentValuesList = contentValuesListMap 1513 .get(Photo.CONTENT_ITEM_TYPE); 1514 if (contentValuesList != null) { 1515 for (ContentValues contentValues : contentValuesList) { 1516 byte[] data = contentValues.getAsByteArray(Photo.PHOTO); 1517 if (data == null) { 1518 continue; 1519 } 1520 final String photoType; 1521 // Use some heuristics for guessing the format of the image. 1522 // TODO: there should be some general API for detecting the file format. 1523 if (data.length >= 3 && data[0] == 'G' && data[1] == 'I' 1524 && data[2] == 'F') { 1525 photoType = "GIF"; 1526 } else if (data.length >= 4 && data[0] == (byte) 0x89 1527 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { 1528 // Note: vCard 2.1 officially does not support PNG, but we may 1529 // have it and using X- word like "X-PNG" may not let importers 1530 // know it is PNG. So we use the String "PNG" as is... 1531 photoType = "PNG"; 1532 } else if (data.length >= 2 && data[0] == (byte) 0xff 1533 && data[1] == (byte) 0xd8) { 1534 photoType = "JPEG"; 1535 } else { 1536 Log.d(LOG_TAG, "Unknown photo type. Ignore."); 1537 continue; 1538 } 1539 final String photoString = VCardUtils.encodeBase64(data); 1540 if (photoString.length() > 0) { 1541 appendVCardPhotoLine(builder, photoString, photoType); 1542 } 1543 } 1544 } 1545 } 1546 1547 private void appendNotes(final StringBuilder builder, 1548 final Map<String, List<ContentValues>> contentValuesListMap) { 1549 final List<ContentValues> contentValuesList = 1550 contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); 1551 if (contentValuesList != null) { 1552 if (mOnlyOneNoteFieldIsAvailable) { 1553 StringBuilder noteBuilder = new StringBuilder(); 1554 boolean first = true; 1555 for (ContentValues contentValues : contentValuesList) { 1556 String note = contentValues.getAsString(Note.NOTE); 1557 if (note == null) { 1558 note = ""; 1559 } 1560 if (note.length() > 0) { 1561 if (first) { 1562 first = false; 1563 } else { 1564 noteBuilder.append('\n'); 1565 } 1566 noteBuilder.append(note); 1567 } 1568 } 1569 final String noteStr = noteBuilder.toString(); 1570 // This means we scan noteStr completely twice, which is redundant. 1571 // But for now, we assume this is not so time-consuming.. 1572 final boolean shouldAppendCharsetInfo = 1573 !VCardUtils.containsOnlyPrintableAscii(noteStr); 1574 final boolean reallyUseQuotedPrintable = 1575 (mUsesQuotedPrintable && 1576 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); 1577 appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, 1578 shouldAppendCharsetInfo, reallyUseQuotedPrintable); 1579 } else { 1580 for (ContentValues contentValues : contentValuesList) { 1581 final String noteStr = contentValues.getAsString(Note.NOTE); 1582 if (!TextUtils.isEmpty(noteStr)) { 1583 final boolean shouldAppendCharsetInfo = 1584 !VCardUtils.containsOnlyPrintableAscii(noteStr); 1585 final boolean reallyUseQuotedPrintable = 1586 (mUsesQuotedPrintable && 1587 !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); 1588 appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, 1589 shouldAppendCharsetInfo, reallyUseQuotedPrintable); 1590 } 1591 } 1592 } 1593 } 1594 } 1595 1596 private void appendAndroidSpecificProperty(final StringBuilder builder, 1597 final String mimeType, ContentValues contentValues) { 1598 List<String> rawDataList = new ArrayList<String>(); 1599 rawDataList.add(mimeType); 1600 final List<String> columnNameList; 1601 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { 1602 1603 } else { 1604 // If you add the other field, please check all the columns are able to be 1605 // converted to String. 1606 // 1607 // e.g. BLOB is not what we can handle here now. 1608 return; 1609 } 1610 1611 for (int i = 1; i <= Constants.MAX_DATA_COLUMN; i++) { 1612 String value = contentValues.getAsString("data" + i); 1613 if (value == null) { 1614 value = ""; 1615 } 1616 rawDataList.add(value); 1617 } 1618 1619 appendVCardLineWithCharsetAndQPDetection(builder, 1620 Constants.PROPERTY_X_ANDROID_CUSTOM, rawDataList); 1621 } 1622 1623 /** 1624 * Append '\' to the characters which should be escaped. The character set is different 1625 * not only between vCard 2.1 and vCard 3.0 but also among each device. 1626 * 1627 * Note that Quoted-Printable string must not be input here. 1628 */ 1629 @SuppressWarnings("fallthrough") 1630 private String escapeCharacters(final String unescaped) { 1631 if (TextUtils.isEmpty(unescaped)) { 1632 return ""; 1633 } 1634 1635 final StringBuilder tmpBuilder = new StringBuilder(); 1636 final int length = unescaped.length(); 1637 for (int i = 0; i < length; i++) { 1638 final char ch = unescaped.charAt(i); 1639 switch (ch) { 1640 case ';': { 1641 tmpBuilder.append('\\'); 1642 tmpBuilder.append(';'); 1643 break; 1644 } 1645 case '\r': { 1646 if (i + 1 < length) { 1647 char nextChar = unescaped.charAt(i); 1648 if (nextChar == '\n') { 1649 break; 1650 } else { 1651 // fall through 1652 } 1653 } else { 1654 // fall through 1655 } 1656 } 1657 case '\n': { 1658 // In vCard 2.1, there's no specification about this, while 1659 // vCard 3.0 explicitly requires this should be encoded to "\n". 1660 tmpBuilder.append("\\n"); 1661 break; 1662 } 1663 case '\\': { 1664 if (mIsV30) { 1665 tmpBuilder.append("\\\\"); 1666 break; 1667 } else { 1668 // fall through 1669 } 1670 } 1671 case '<': 1672 case '>': { 1673 if (mIsDoCoMo) { 1674 tmpBuilder.append('\\'); 1675 tmpBuilder.append(ch); 1676 } else { 1677 tmpBuilder.append(ch); 1678 } 1679 break; 1680 } 1681 case ',': { 1682 if (mIsV30) { 1683 tmpBuilder.append("\\,"); 1684 } else { 1685 tmpBuilder.append(ch); 1686 } 1687 break; 1688 } 1689 default: { 1690 tmpBuilder.append(ch); 1691 break; 1692 } 1693 } 1694 } 1695 return tmpBuilder.toString(); 1696 } 1697 1698 private void appendVCardPhotoLine(final StringBuilder builder, 1699 final String encodedData, final String photoType) { 1700 StringBuilder tmpBuilder = new StringBuilder(); 1701 tmpBuilder.append(Constants.PROPERTY_PHOTO); 1702 tmpBuilder.append(VCARD_PARAM_SEPARATOR); 1703 if (mIsV30) { 1704 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); 1705 } else { 1706 tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); 1707 } 1708 tmpBuilder.append(VCARD_PARAM_SEPARATOR); 1709 appendTypeParameter(tmpBuilder, photoType); 1710 tmpBuilder.append(VCARD_DATA_SEPARATOR); 1711 tmpBuilder.append(encodedData); 1712 1713 final String tmpStr = tmpBuilder.toString(); 1714 tmpBuilder = new StringBuilder(); 1715 int lineCount = 0; 1716 int length = tmpStr.length(); 1717 for (int i = 0; i < length; i++) { 1718 tmpBuilder.append(tmpStr.charAt(i)); 1719 lineCount++; 1720 if (lineCount > 72) { 1721 tmpBuilder.append(VCARD_END_OF_LINE); 1722 tmpBuilder.append(VCARD_WS); 1723 lineCount = 0; 1724 } 1725 } 1726 builder.append(tmpBuilder.toString()); 1727 builder.append(VCARD_END_OF_LINE); 1728 builder.append(VCARD_END_OF_LINE); 1729 } 1730 1731 private class PostalStruct { 1732 final boolean reallyUseQuotedPrintable; 1733 final boolean appendCharset; 1734 final String addressData; 1735 public PostalStruct(final boolean reallyUseQuotedPrintable, 1736 final boolean appendCharset, final String addressData) { 1737 this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; 1738 this.appendCharset = appendCharset; 1739 this.addressData = addressData; 1740 } 1741 } 1742 1743 /** 1744 * @return null when there's no information available to construct the data. 1745 */ 1746 private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { 1747 boolean reallyUseQuotedPrintable = false; 1748 boolean appendCharset = false; 1749 1750 boolean dataArrayExists = false; 1751 String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); 1752 for (String data : dataArray) { 1753 if (!TextUtils.isEmpty(data)) { 1754 dataArrayExists = true; 1755 if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { 1756 appendCharset = true; 1757 } 1758 if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { 1759 reallyUseQuotedPrintable = true; 1760 break; 1761 } 1762 } 1763 } 1764 1765 if (dataArrayExists) { 1766 StringBuffer addressBuffer = new StringBuffer(); 1767 boolean first = true; 1768 for (String data : dataArray) { 1769 if (first) { 1770 first = false; 1771 } else { 1772 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1773 } 1774 if (!TextUtils.isEmpty(data)) { 1775 if (reallyUseQuotedPrintable) { 1776 addressBuffer.append(encodeQuotedPrintable(data)); 1777 } else { 1778 addressBuffer.append(escapeCharacters(data)); 1779 } 1780 } 1781 } 1782 return new PostalStruct(reallyUseQuotedPrintable, appendCharset, 1783 addressBuffer.toString()); 1784 } 1785 1786 String formattedAddress = 1787 contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1788 if (!TextUtils.isEmpty(formattedAddress)) { 1789 reallyUseQuotedPrintable = 1790 !VCardUtils.containsOnlyPrintableAscii(formattedAddress); 1791 appendCharset = 1792 !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedAddress); 1793 if (reallyUseQuotedPrintable) { 1794 formattedAddress = encodeQuotedPrintable(formattedAddress); 1795 } else { 1796 formattedAddress = escapeCharacters(formattedAddress); 1797 } 1798 // We use the second value ("Extended Address"). 1799 // 1800 // adr-value = 0*6(text-value ";") text-value 1801 // ; PO Box, Extended Address, Street, Locality, Region, Postal 1802 // ; Code, Country Name 1803 StringBuffer addressBuffer = new StringBuffer(); 1804 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1805 addressBuffer.append(formattedAddress); 1806 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1807 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1808 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1809 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1810 addressBuffer.append(VCARD_ITEM_SEPARATOR); 1811 return new PostalStruct( 1812 reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); 1813 } 1814 return null; // There's no data available. 1815 } 1816 1817 private void appendVCardPostalLine(final StringBuilder builder, 1818 final int type, final String label, final ContentValues contentValues, 1819 final boolean isPrimary, final boolean emitLineEveryTime) { 1820 final boolean reallyUseQuotedPrintable; 1821 final boolean appendCharset; 1822 final String addressData; 1823 { 1824 PostalStruct postalStruct = tryConstructPostalStruct(contentValues); 1825 if (postalStruct == null) { 1826 if (emitLineEveryTime) { 1827 reallyUseQuotedPrintable = false; 1828 appendCharset = false; 1829 addressData = ""; 1830 } else { 1831 return; 1832 } 1833 } else { 1834 reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; 1835 appendCharset = postalStruct.appendCharset; 1836 addressData = postalStruct.addressData; 1837 } 1838 } 1839 1840 List<String> parameterList = new ArrayList<String>(); 1841 if (isPrimary) { 1842 parameterList.add(Constants.PARAM_TYPE_PREF); 1843 } 1844 switch (type) { 1845 case StructuredPostal.TYPE_HOME: { 1846 parameterList.add(Constants.PARAM_TYPE_HOME); 1847 break; 1848 } 1849 case StructuredPostal.TYPE_WORK: { 1850 parameterList.add(Constants.PARAM_TYPE_WORK); 1851 break; 1852 } 1853 case StructuredPostal.TYPE_CUSTOM: { 1854 if (mUsesAndroidProperty && !TextUtils.isEmpty(label) 1855 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 1856 // We're not sure whether the label is valid in the spec 1857 // ("IANA-token" in the vCard 3.0 is unclear...) 1858 // Just for safety, we add "X-" at the beggining of each label. 1859 // Also checks the label obeys with vCard 3.0 spec. 1860 parameterList.add("X-" + label); 1861 } 1862 break; 1863 } 1864 case StructuredPostal.TYPE_OTHER: { 1865 break; 1866 } 1867 default: { 1868 Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); 1869 break; 1870 } 1871 } 1872 1873 // Actual data construction starts from here. 1874 // TODO: add a new version of appendVCardLine() for this purpose. 1875 1876 builder.append(Constants.PROPERTY_ADR); 1877 builder.append(VCARD_PARAM_SEPARATOR); 1878 1879 // Parameters 1880 { 1881 boolean shouldAppendParamSeparator = false; 1882 if (!parameterList.isEmpty()) { 1883 appendTypeParameters(builder, parameterList); 1884 shouldAppendParamSeparator = true; 1885 } 1886 1887 if (appendCharset) { 1888 // Strictly, vCard 3.0 does not allow exporters to emit charset information, 1889 // but we will add it since the information should be useful for importers, 1890 // 1891 // Assume no parser does not emit error with this parameter in vCard 3.0. 1892 if (shouldAppendParamSeparator) { 1893 builder.append(VCARD_PARAM_SEPARATOR); 1894 } 1895 builder.append(mVCardCharsetParameter); 1896 shouldAppendParamSeparator = true; 1897 } 1898 1899 if (reallyUseQuotedPrintable) { 1900 if (shouldAppendParamSeparator) { 1901 builder.append(VCARD_PARAM_SEPARATOR); 1902 } 1903 builder.append(VCARD_PARAM_ENCODING_QP); 1904 shouldAppendParamSeparator = true; 1905 } 1906 } 1907 1908 builder.append(VCARD_DATA_SEPARATOR); 1909 builder.append(addressData); 1910 builder.append(VCARD_END_OF_LINE); 1911 } 1912 1913 private void appendVCardEmailLine(final StringBuilder builder, 1914 final int type, final String label, 1915 final String rawData, final boolean isPrimary) { 1916 final String typeAsString; 1917 switch (type) { 1918 case Email.TYPE_CUSTOM: { 1919 // For backward compatibility. 1920 // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. 1921 // To support mobile type at that time, this custom label had been used. 1922 if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME 1923 .equals(label)) { 1924 typeAsString = Constants.PARAM_TYPE_CELL; 1925 } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) 1926 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 1927 typeAsString = "X-" + label; 1928 } else { 1929 typeAsString = null; 1930 } 1931 break; 1932 } 1933 case Email.TYPE_HOME: { 1934 typeAsString = Constants.PARAM_TYPE_HOME; 1935 break; 1936 } 1937 case Email.TYPE_WORK: { 1938 typeAsString = Constants.PARAM_TYPE_WORK; 1939 break; 1940 } 1941 case Email.TYPE_OTHER: { 1942 typeAsString = null; 1943 break; 1944 } 1945 case Email.TYPE_MOBILE: { 1946 typeAsString = Constants.PARAM_TYPE_CELL; 1947 break; 1948 } 1949 default: { 1950 Log.e(LOG_TAG, "Unknown Email type: " + type); 1951 typeAsString = null; 1952 break; 1953 } 1954 } 1955 1956 final List<String> parameterList = new ArrayList<String>(); 1957 if (isPrimary) { 1958 parameterList.add(Constants.PARAM_TYPE_PREF); 1959 } 1960 if (!TextUtils.isEmpty(typeAsString)) { 1961 parameterList.add(typeAsString); 1962 } 1963 1964 appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL, 1965 parameterList, rawData); 1966 } 1967 1968 private void appendVCardTelephoneLine(final StringBuilder builder, 1969 final Integer typeAsObject, final String label, 1970 final String encodedData, boolean isPrimary) { 1971 builder.append(Constants.PROPERTY_TEL); 1972 builder.append(VCARD_PARAM_SEPARATOR); 1973 1974 final int typeAsPrimitive; 1975 if (typeAsObject == null) { 1976 typeAsPrimitive = Phone.TYPE_OTHER; 1977 } else { 1978 typeAsPrimitive = typeAsObject; 1979 } 1980 1981 ArrayList<String> parameterList = new ArrayList<String>(); 1982 switch (typeAsPrimitive) { 1983 case Phone.TYPE_HOME: 1984 parameterList.addAll( 1985 Arrays.asList(Constants.PARAM_TYPE_HOME)); 1986 break; 1987 case Phone.TYPE_WORK: 1988 parameterList.addAll( 1989 Arrays.asList(Constants.PARAM_TYPE_WORK)); 1990 break; 1991 case Phone.TYPE_FAX_HOME: 1992 parameterList.addAll( 1993 Arrays.asList(Constants.PARAM_TYPE_HOME, Constants.PARAM_TYPE_FAX)); 1994 break; 1995 case Phone.TYPE_FAX_WORK: 1996 parameterList.addAll( 1997 Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_FAX)); 1998 break; 1999 case Phone.TYPE_MOBILE: 2000 parameterList.add(Constants.PARAM_TYPE_CELL); 2001 break; 2002 case Phone.TYPE_PAGER: 2003 if (mIsDoCoMo) { 2004 // Not sure about the reason, but previous implementation had 2005 // used "VOICE" instead of "PAGER" 2006 parameterList.add(Constants.PARAM_TYPE_VOICE); 2007 } else { 2008 parameterList.add(Constants.PARAM_TYPE_PAGER); 2009 } 2010 break; 2011 case Phone.TYPE_OTHER: 2012 parameterList.add(Constants.PARAM_TYPE_VOICE); 2013 break; 2014 case Phone.TYPE_CAR: 2015 parameterList.add(Constants.PARAM_TYPE_CAR); 2016 break; 2017 case Phone.TYPE_COMPANY_MAIN: 2018 // There's no relevant field in vCard (at least 2.1). 2019 parameterList.add(Constants.PARAM_TYPE_WORK); 2020 isPrimary = true; 2021 break; 2022 case Phone.TYPE_ISDN: 2023 parameterList.add(Constants.PARAM_TYPE_ISDN); 2024 break; 2025 case Phone.TYPE_MAIN: 2026 isPrimary = true; 2027 break; 2028 case Phone.TYPE_OTHER_FAX: 2029 parameterList.add(Constants.PARAM_TYPE_FAX); 2030 break; 2031 case Phone.TYPE_TELEX: 2032 parameterList.add(Constants.PARAM_TYPE_TLX); 2033 break; 2034 case Phone.TYPE_WORK_MOBILE: 2035 parameterList.addAll( 2036 Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_CELL)); 2037 break; 2038 case Phone.TYPE_WORK_PAGER: 2039 parameterList.add(Constants.PARAM_TYPE_WORK); 2040 // See above. 2041 if (mIsDoCoMo) { 2042 parameterList.add(Constants.PARAM_TYPE_VOICE); 2043 } else { 2044 parameterList.add(Constants.PARAM_TYPE_PAGER); 2045 } 2046 break; 2047 case Phone.TYPE_MMS: 2048 parameterList.add(Constants.PARAM_TYPE_MSG); 2049 break; 2050 case Phone.TYPE_CUSTOM: 2051 if (mUsesAndroidProperty && !TextUtils.isEmpty(label) 2052 && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { 2053 // Note: Strictly, vCard 2.1 does not allow "X-" parameter without 2054 // "TYPE=" string. 2055 parameterList.add("X-" + label); 2056 } else { 2057 // Just ignore the custom type. 2058 parameterList.add(Constants.PARAM_TYPE_VOICE); 2059 } 2060 break; 2061 case Phone.TYPE_RADIO: 2062 case Phone.TYPE_TTY_TDD: 2063 default: 2064 break; 2065 } 2066 2067 if (isPrimary) { 2068 parameterList.add(Constants.PARAM_TYPE_PREF); 2069 } 2070 2071 if (parameterList.isEmpty()) { 2072 appendUncommonPhoneType(builder, typeAsPrimitive); 2073 } else { 2074 appendTypeParameters(builder, parameterList); 2075 } 2076 2077 builder.append(VCARD_DATA_SEPARATOR); 2078 builder.append(encodedData); 2079 builder.append(VCARD_END_OF_LINE); 2080 } 2081 2082 /** 2083 * Appends phone type string which may not be available in some devices. 2084 */ 2085 private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { 2086 if (mIsDoCoMo) { 2087 // The previous implementation for DoCoMo had been conservative 2088 // about miscellaneous types. 2089 builder.append(Constants.PARAM_TYPE_VOICE); 2090 } else { 2091 String phoneType = VCardUtils.getPhoneTypeString(type); 2092 if (phoneType != null) { 2093 appendTypeParameter(builder, phoneType); 2094 } else { 2095 Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); 2096 } 2097 } 2098 } 2099 2100 // appendVCardLine() variants accepting one String. 2101 2102 private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, 2103 final String propertyName, final String rawData) { 2104 appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData); 2105 } 2106 2107 private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, 2108 final String propertyName, 2109 final List<String> parameterList, final String rawData) { 2110 final boolean needCharset = 2111 (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData)); 2112 final boolean reallyUseQuotedPrintable = 2113 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData); 2114 appendVCardLine(builder, propertyName, parameterList, 2115 rawData, needCharset, reallyUseQuotedPrintable); 2116 } 2117 2118 private void appendVCardLine(final StringBuilder builder, 2119 final String propertyName, final String rawData) { 2120 appendVCardLine(builder, propertyName, rawData, false, false); 2121 } 2122 2123 private void appendVCardLine(final StringBuilder builder, 2124 final String propertyName, final String rawData, final boolean needCharset, 2125 boolean needQuotedPrintable) { 2126 appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable); 2127 } 2128 2129 private void appendVCardLine(final StringBuilder builder, 2130 final String propertyName, 2131 final List<String> parameterList, 2132 final String rawData, final boolean needCharset, 2133 boolean needQuotedPrintable) { 2134 builder.append(propertyName); 2135 if (parameterList != null && parameterList.size() > 0) { 2136 builder.append(VCARD_PARAM_SEPARATOR); 2137 appendTypeParameters(builder, parameterList); 2138 } 2139 if (needCharset) { 2140 builder.append(VCARD_PARAM_SEPARATOR); 2141 builder.append(mVCardCharsetParameter); 2142 } 2143 2144 final String encodedData; 2145 if (needQuotedPrintable) { 2146 builder.append(VCARD_PARAM_SEPARATOR); 2147 builder.append(VCARD_PARAM_ENCODING_QP); 2148 encodedData = encodeQuotedPrintable(rawData); 2149 } else { 2150 // TODO: one line may be too huge, which may be invalid in vCard spec, though 2151 // several (even well-known) applications do not care this. 2152 encodedData = escapeCharacters(rawData); 2153 } 2154 2155 builder.append(VCARD_DATA_SEPARATOR); 2156 builder.append(encodedData); 2157 builder.append(VCARD_END_OF_LINE); 2158 } 2159 2160 // appendVCardLine() variants accepting List<String>. 2161 2162 private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, 2163 final String propertyName, final List<String> rawDataList) { 2164 appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList); 2165 } 2166 2167 private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, 2168 final String propertyName, 2169 final List<String> parameterList, final List<String> rawDataList) { 2170 boolean needCharset = false; 2171 boolean reallyUseQuotedPrintable = false; 2172 for (String rawData : rawDataList) { 2173 if (!needCharset && mUsesQuotedPrintable && 2174 !VCardUtils.containsOnlyPrintableAscii(rawData)) { 2175 needCharset = true; 2176 } 2177 if (!reallyUseQuotedPrintable && 2178 !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData)) { 2179 reallyUseQuotedPrintable = true; 2180 } 2181 if (needCharset && reallyUseQuotedPrintable) { 2182 break; 2183 } 2184 } 2185 2186 appendVCardLine(builder, propertyName, parameterList, 2187 rawDataList, needCharset, reallyUseQuotedPrintable); 2188 } 2189 2190 /* 2191 private void appendVCardLine(final StringBuilder builder, 2192 final String propertyName, final List<String> rawDataList) { 2193 appendVCardLine(builder, propertyName, rawDataList, false, false); 2194 } 2195 2196 private void appendVCardLine(final StringBuilder builder, 2197 final String propertyName, final List<String> rawDataList, 2198 final boolean needCharset, boolean needQuotedPrintable) { 2199 appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable); 2200 }*/ 2201 2202 private void appendVCardLine(final StringBuilder builder, 2203 final String propertyName, 2204 final List<String> parameterList, 2205 final List<String> rawDataList, final boolean needCharset, 2206 boolean needQuotedPrintable) { 2207 builder.append(propertyName); 2208 if (parameterList != null && parameterList.size() > 0) { 2209 builder.append(VCARD_PARAM_SEPARATOR); 2210 appendTypeParameters(builder, parameterList); 2211 } 2212 if (needCharset) { 2213 builder.append(VCARD_PARAM_SEPARATOR); 2214 builder.append(mVCardCharsetParameter); 2215 } 2216 2217 builder.append(VCARD_DATA_SEPARATOR); 2218 boolean first = true; 2219 for (String rawData : rawDataList) { 2220 final String encodedData; 2221 if (needQuotedPrintable) { 2222 builder.append(VCARD_PARAM_SEPARATOR); 2223 builder.append(VCARD_PARAM_ENCODING_QP); 2224 encodedData = encodeQuotedPrintable(rawData); 2225 } else { 2226 // TODO: one line may be too huge, which may be invalid in vCard spec, though 2227 // several (even well-known) applications do not care this. 2228 encodedData = escapeCharacters(rawData); 2229 } 2230 2231 if (first) { 2232 first = false; 2233 } else { 2234 builder.append(VCARD_ITEM_SEPARATOR); 2235 } 2236 builder.append(encodedData); 2237 } 2238 builder.append(VCARD_END_OF_LINE); 2239 } 2240 2241 /** 2242 * VCARD_PARAM_SEPARATOR must be appended before this method being called. 2243 */ 2244 private void appendTypeParameters(final StringBuilder builder, 2245 final List<String> types) { 2246 // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, 2247 // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. 2248 boolean first = true; 2249 for (String type : types) { 2250 if (first) { 2251 first = false; 2252 } else { 2253 builder.append(VCARD_PARAM_SEPARATOR); 2254 } 2255 appendTypeParameter(builder, type); 2256 } 2257 } 2258 2259 /** 2260 * VCARD_PARAM_SEPARATOR must be appended before this method being called. 2261 */ 2262 private void appendTypeParameter(final StringBuilder builder, final String type) { 2263 // Refrain from using appendType() so that "TYPE=" is not be appended when the 2264 // device is DoCoMo's (just for safety). 2265 // 2266 // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" 2267 if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { 2268 builder.append(Constants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); 2269 } 2270 builder.append(type); 2271 } 2272 2273 /** 2274 * Returns true when the property line should contain charset parameter 2275 * information. This method may return true even when vCard version is 3.0. 2276 * 2277 * Strictly, adding charset information is invalid in VCard 3.0. 2278 * However we'll add the info only when charset we use is not UTF-8 2279 * in vCard 3.0 format, since parser side may be able to use the charset 2280 * via this field, though we may encounter another problem by adding it. 2281 * 2282 * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 2283 * recommends UTF-8. By adding this field, parsers may be able 2284 * to know this text is NOT UTF-8 but Shift_Jis. 2285 */ 2286 private boolean shouldAppendCharsetParameter(final String propertyValue) { 2287 return (!(mIsV30 && mUsesUtf8) && !VCardUtils.containsOnlyPrintableAscii(propertyValue)); 2288 } 2289 2290 private boolean shouldAppendCharsetParameters(final List<String> propertyValueList) { 2291 if (mIsV30 && mUsesUtf8) { 2292 return false; 2293 } 2294 for (String propertyValue : propertyValueList) { 2295 if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { 2296 return true; 2297 } 2298 } 2299 return false; 2300 } 2301 2302 private String encodeQuotedPrintable(String str) { 2303 if (TextUtils.isEmpty(str)) { 2304 return ""; 2305 } 2306 { 2307 // Replace "\n" and "\r" with "\r\n". 2308 StringBuilder tmpBuilder = new StringBuilder(); 2309 int length = str.length(); 2310 for (int i = 0; i < length; i++) { 2311 char ch = str.charAt(i); 2312 if (ch == '\r') { 2313 if (i + 1 < length && str.charAt(i + 1) == '\n') { 2314 i++; 2315 } 2316 tmpBuilder.append("\r\n"); 2317 } else if (ch == '\n') { 2318 tmpBuilder.append("\r\n"); 2319 } else { 2320 tmpBuilder.append(ch); 2321 } 2322 } 2323 str = tmpBuilder.toString(); 2324 } 2325 2326 final StringBuilder tmpBuilder = new StringBuilder(); 2327 int index = 0; 2328 int lineCount = 0; 2329 byte[] strArray = null; 2330 2331 try { 2332 strArray = str.getBytes(mCharsetString); 2333 } catch (UnsupportedEncodingException e) { 2334 Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " 2335 + "Try default charset"); 2336 strArray = str.getBytes(); 2337 } 2338 while (index < strArray.length) { 2339 tmpBuilder.append(String.format("=%02X", strArray[index])); 2340 index += 1; 2341 lineCount += 3; 2342 2343 if (lineCount >= 67) { 2344 // Specification requires CRLF must be inserted before the 2345 // length of the line 2346 // becomes more than 76. 2347 // Assuming that the next character is a multi-byte character, 2348 // it will become 2349 // 6 bytes. 2350 // 76 - 6 - 3 = 67 2351 tmpBuilder.append("=\r\n"); 2352 lineCount = 0; 2353 } 2354 } 2355 2356 return tmpBuilder.toString(); 2357 } 2358 2359 //// The methods bellow are for call log history //// 2360 2361 /** 2362 * This static function is to compose vCard for phone own number 2363 */ 2364 public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, 2365 String phoneNumber, boolean vcardVer21) { 2366 final StringBuilder builder = new StringBuilder(); 2367 appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); 2368 if (!vcardVer21) { 2369 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); 2370 } else { 2371 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); 2372 } 2373 2374 boolean needCharset = false; 2375 if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { 2376 needCharset = true; 2377 } 2378 appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false); 2379 appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false); 2380 2381 String label = Integer.toString(phonetype); 2382 appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); 2383 2384 appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); 2385 2386 return builder.toString(); 2387 } 2388 2389 /** 2390 * Format according to RFC 2445 DATETIME type. 2391 * The format is: ("%Y%m%dT%H%M%SZ"). 2392 */ 2393 private final String toRfc2455Format(final long millSecs) { 2394 Time startDate = new Time(); 2395 startDate.set(millSecs); 2396 String date = startDate.format2445(); 2397 return date + FLAG_TIMEZONE_UTC; 2398 } 2399 2400 /** 2401 * Try to append the property line for a call history time stamp field if possible. 2402 * Do nothing if the call log type gotton from the database is invalid. 2403 */ 2404 private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { 2405 // Extension for call history as defined in 2406 // in the Specification for Ic Mobile Communcation - ver 1.1, 2407 // Oct 2000. This is used to send the details of the call 2408 // history - missed, incoming, outgoing along with date and time 2409 // to the requesting device (For example, transferring phone book 2410 // when connected over bluetooth) 2411 // 2412 // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" 2413 final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); 2414 final String callLogTypeStr; 2415 switch (callLogType) { 2416 case Calls.INCOMING_TYPE: { 2417 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; 2418 break; 2419 } 2420 case Calls.OUTGOING_TYPE: { 2421 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; 2422 break; 2423 } 2424 case Calls.MISSED_TYPE: { 2425 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; 2426 break; 2427 } 2428 default: { 2429 Log.w(LOG_TAG, "Call log type not correct."); 2430 return; 2431 } 2432 } 2433 2434 final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); 2435 builder.append(VCARD_PROPERTY_X_TIMESTAMP); 2436 builder.append(VCARD_PARAM_SEPARATOR); 2437 appendTypeParameter(builder, callLogTypeStr); 2438 builder.append(VCARD_DATA_SEPARATOR); 2439 builder.append(toRfc2455Format(dateAsLong)); 2440 builder.append(VCARD_END_OF_LINE); 2441 } 2442 2443 private String createOneCallLogEntryInternal() { 2444 final StringBuilder builder = new StringBuilder(); 2445 appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); 2446 if (mIsV30) { 2447 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); 2448 } else { 2449 appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); 2450 } 2451 String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); 2452 if (TextUtils.isEmpty(name)) { 2453 name = mCursor.getString(NUMBER_COLUMN_INDEX); 2454 } 2455 final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); 2456 appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false); 2457 appendVCardLine(builder, Constants.PROPERTY_N, name, needCharset, false); 2458 2459 String number = mCursor.getString(NUMBER_COLUMN_INDEX); 2460 int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); 2461 String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); 2462 if (TextUtils.isEmpty(label)) { 2463 label = Integer.toString(type); 2464 } 2465 appendVCardTelephoneLine(builder, type, label, number, false); 2466 tryAppendCallHistoryTimeStampField(builder); 2467 appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); 2468 return builder.toString(); 2469 } 2470} 2471