VCardComposer.java revision 677ef21613a9d35053ec098444832ce4125a847e
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.ContentResolver; 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.Entity; 22import android.content.Entity.NamedContentValues; 23import android.content.EntityIterator; 24import android.database.Cursor; 25import android.database.sqlite.SQLiteException; 26import android.net.Uri; 27import android.provider.ContactsContract.CommonDataKinds.Email; 28import android.provider.ContactsContract.CommonDataKinds.Event; 29import android.provider.ContactsContract.CommonDataKinds.Im; 30import android.provider.ContactsContract.CommonDataKinds.Nickname; 31import android.provider.ContactsContract.CommonDataKinds.Note; 32import android.provider.ContactsContract.CommonDataKinds.Organization; 33import android.provider.ContactsContract.CommonDataKinds.Phone; 34import android.provider.ContactsContract.CommonDataKinds.Photo; 35import android.provider.ContactsContract.CommonDataKinds.Relation; 36import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37import android.provider.ContactsContract.CommonDataKinds.StructuredName; 38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 39import android.provider.ContactsContract.CommonDataKinds.Website; 40import android.provider.ContactsContract.Contacts; 41import android.provider.ContactsContract.Data; 42import android.provider.ContactsContract.RawContacts; 43import android.provider.ContactsContract.RawContactsEntity; 44import android.text.TextUtils; 45import android.util.Log; 46 47import java.io.BufferedWriter; 48import java.io.IOException; 49import java.io.OutputStream; 50import java.io.OutputStreamWriter; 51import java.io.UnsupportedEncodingException; 52import java.io.Writer; 53import java.lang.reflect.InvocationTargetException; 54import java.lang.reflect.Method; 55import java.util.ArrayList; 56import java.util.HashMap; 57import java.util.List; 58import java.util.Map; 59 60/** 61 * <p> 62 * The class for composing vCard from Contacts information. 63 * </p> 64 * <p> 65 * Usually, this class should be used like this. 66 * </p> 67 * <pre class="prettyprint">VCardComposer composer = null; 68 * try { 69 * composer = new VCardComposer(context); 70 * composer.addHandler( 71 * composer.new HandlerForOutputStream(outputStream)); 72 * if (!composer.init()) { 73 * // Do something handling the situation. 74 * return; 75 * } 76 * while (!composer.isAfterLast()) { 77 * if (mCanceled) { 78 * // Assume a user may cancel this operation during the export. 79 * return; 80 * } 81 * if (!composer.createOneEntry()) { 82 * // Do something handling the error situation. 83 * return; 84 * } 85 * } 86 * } finally { 87 * if (composer != null) { 88 * composer.terminate(); 89 * } 90 * }</pre> 91 * <p> 92 * Users have to manually take care of memory efficiency. Even one vCard may contain 93 * image of non-trivial size for mobile devices. 94 * </p> 95 * <p> 96 * {@link VCardBuilder} is used to build each vCard. 97 * </p> 98 */ 99public class VCardComposer { 100 private static final String LOG_TAG = "VCardComposer"; 101 private static final boolean DEBUG = false; 102 103 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 104 "Failed to get database information"; 105 106 public static final String FAILURE_REASON_NO_ENTRY = 107 "There's no exportable in the database"; 108 109 public static final String FAILURE_REASON_NOT_INITIALIZED = 110 "The vCard composer object is not correctly initialized"; 111 112 /** Should be visible only from developers... (no need to translate, hopefully) */ 113 public static final String FAILURE_REASON_UNSUPPORTED_URI = 114 "The Uri vCard composer received is not supported by the composer."; 115 116 public static final String NO_ERROR = "No error"; 117 118 // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, 119 // since usual vCard devices for Japanese devices already use it. 120 private static final String SHIFT_JIS = "SHIFT_JIS"; 121 private static final String UTF_8 = "UTF-8"; 122 123 private static final Map<Integer, String> sImMap; 124 125 static { 126 sImMap = new HashMap<Integer, String>(); 127 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); 128 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); 129 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); 130 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); 131 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); 132 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 133 // We don't add Google talk here since it has to be handled separately. 134 } 135 136 public static interface OneEntryHandler { 137 public boolean onInit(Context context); 138 public boolean onEntryCreated(String vcard); 139 public void onTerminate(); 140 } 141 142 /** 143 * <p> 144 * An useful handler for emitting vCard String to an OutputStream object one by one. 145 * </p> 146 * <p> 147 * The input OutputStream object is closed() on {@link #onTerminate()}. 148 * Must not close the stream outside this class. 149 * </p> 150 */ 151 public final class HandlerForOutputStream implements OneEntryHandler { 152 private final OutputStream mOutputStream; // mWriter will close this. 153 private Writer mWriter; 154 155 /** 156 * Input stream will be closed on the detruction of this object. 157 */ 158 public HandlerForOutputStream(final OutputStream outputStream) { 159 mOutputStream = outputStream; 160 } 161 162 @Override 163 public boolean onInit(final Context context) { 164 try { 165 mWriter = new BufferedWriter(new OutputStreamWriter( 166 mOutputStream, mCharset)); 167 } catch (UnsupportedEncodingException e1) { 168 Log.e(LOG_TAG, "Unsupported charset: " + mCharset); 169 mErrorReason = "Encoding is not supported (usually this does not happen!): " 170 + mCharset; 171 return false; 172 } 173 174 if (mIsDoCoMo) { 175 try { 176 // Create one empty entry. 177 mWriter.write(createOneEntryInternal("-1", null)); 178 } catch (IOException e) { 179 Log.e(LOG_TAG, 180 "IOException occurred during exportOneContactData: " 181 + e.getMessage()); 182 mErrorReason = "IOException occurred: " + e.getMessage(); 183 return false; 184 } 185 } 186 return true; 187 } 188 189 @Override 190 public boolean onEntryCreated(String vcard) { 191 try { 192 mWriter.write(vcard); 193 } catch (IOException e) { 194 Log.e(LOG_TAG, 195 "IOException occurred during exportOneContactData: " 196 + e.getMessage()); 197 mErrorReason = "IOException occurred: " + e.getMessage(); 198 return false; 199 } 200 return true; 201 } 202 203 @Override 204 public void onTerminate() { 205 if (mWriter != null) { 206 try { 207 mWriter.close(); 208 } catch (IOException e) { 209 Log.w(LOG_TAG, "IOException is thrown during close(). Ignored.", e); 210 } 211 } 212 } 213 } 214 215 private final Context mContext; 216 private final int mVCardType; 217 private final boolean mCareHandlerErrors; 218 private final ContentResolver mContentResolver; 219 220 private final boolean mIsDoCoMo; 221 private Cursor mCursor; 222 private boolean mCursorSuppliedFromOutside; 223 private int mIdColumn; 224 private Uri mContentUriForRawContactsEntity; 225 226 private final String mCharset; 227 private final List<OneEntryHandler> mHandlerList; 228 229 private boolean mInitDone; 230 private String mErrorReason = NO_ERROR; 231 232 /** 233 * Set to false when one of {@link #init()} variants is called, and set to true when 234 * {@link #terminate()} is called. Initially set to true. 235 */ 236 private boolean mTerminateCalled = true; 237 238 private static final String[] sContactsProjection = new String[] { 239 Contacts._ID, 240 }; 241 242 public VCardComposer(Context context) { 243 this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); 244 } 245 246 /** 247 * The variant which sets charset to null and sets careHandlerErrors to true. 248 */ 249 public VCardComposer(Context context, int vcardType) { 250 this(context, vcardType, null, true); 251 } 252 253 public VCardComposer(Context context, int vcardType, String charset) { 254 this(context, vcardType, charset, true); 255 } 256 257 /** 258 * The variant which sets charset to null. 259 */ 260 public VCardComposer(final Context context, final int vcardType, 261 final boolean careHandlerErrors) { 262 this(context, vcardType, null, careHandlerErrors); 263 } 264 265 /** 266 * Constructs for supporting call log entry vCard composing. 267 * 268 * @param context Context to be used during the composition. 269 * @param vcardType The type of vCard, typically available via {@link VCardConfig}. 270 * @param charset The charset to be used. Use null when you don't need the charset. 271 * @param careHandlerErrors If true, This object returns false everytime 272 * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false. 273 * If false, this ignores those errors. 274 */ 275 public VCardComposer(final Context context, final int vcardType, String charset, 276 final boolean careHandlerErrors) { 277 this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors); 278 } 279 280 /** 281 * Just for testing for now. 282 * @param resolver {@link ContentResolver} which used by this object. 283 * @hide 284 */ 285 public VCardComposer(final Context context, ContentResolver resolver, 286 final int vcardType, String charset, final boolean careHandlerErrors) { 287 mContext = context; 288 mVCardType = vcardType; 289 mCareHandlerErrors = careHandlerErrors; 290 mContentResolver = resolver; 291 292 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 293 mHandlerList = new ArrayList<OneEntryHandler>(); 294 295 charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); 296 final boolean shouldAppendCharsetParam = !( 297 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset)); 298 299 if (mIsDoCoMo || shouldAppendCharsetParam) { 300 // TODO: clean up once we're sure CharsetUtils are really unnecessary any more. 301 if (SHIFT_JIS.equalsIgnoreCase(charset)) { 302 /*if (mIsDoCoMo) { 303 try { 304 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); 305 } catch (UnsupportedCharsetException e) { 306 Log.e(LOG_TAG, 307 "DoCoMo-specific SHIFT_JIS was not found. " 308 + "Use SHIFT_JIS as is."); 309 charset = SHIFT_JIS; 310 } 311 } else { 312 try { 313 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); 314 } catch (UnsupportedCharsetException e) { 315 // Log.e(LOG_TAG, 316 // "Career-specific SHIFT_JIS was not found. " 317 // + "Use SHIFT_JIS as is."); 318 charset = SHIFT_JIS; 319 } 320 }*/ 321 mCharset = charset; 322 } else { 323 /* Log.w(LOG_TAG, 324 "The charset \"" + charset + "\" is used while " 325 + SHIFT_JIS + " is needed to be used."); */ 326 if (TextUtils.isEmpty(charset)) { 327 mCharset = SHIFT_JIS; 328 } else { 329 /* 330 try { 331 charset = CharsetUtils.charsetForVendor(charset).name(); 332 } catch (UnsupportedCharsetException e) { 333 Log.i(LOG_TAG, 334 "Career-specific \"" + charset + "\" was not found (as usual). " 335 + "Use it as is."); 336 }*/ 337 mCharset = charset; 338 } 339 } 340 } else { 341 if (TextUtils.isEmpty(charset)) { 342 mCharset = UTF_8; 343 } else { 344 /*try { 345 charset = CharsetUtils.charsetForVendor(charset).name(); 346 } catch (UnsupportedCharsetException e) { 347 Log.i(LOG_TAG, 348 "Career-specific \"" + charset + "\" was not found (as usual). " 349 + "Use it as is."); 350 }*/ 351 mCharset = charset; 352 } 353 } 354 355 Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); 356 } 357 358 /** 359 * Must be called before {@link #init()}. 360 */ 361 public void addHandler(OneEntryHandler handler) { 362 if (handler != null) { 363 mHandlerList.add(handler); 364 } 365 } 366 367 /** 368 * Initializes this object using default {@link Contacts#CONTENT_URI}. 369 * 370 * You can call this method or a variant of this method just once. In other words, you cannot 371 * reuse this object. 372 * 373 * @return Returns true when initialization is successful and all the other 374 * methods are available. Returns false otherwise. 375 */ 376 public boolean init() { 377 return init(null, null); 378 } 379 380 /** 381 * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from 382 * {@link ContentResolver} with {@link Contacts#_ID}. 383 * <code> 384 * String selection = Data.CONTACT_ID + "=?"; 385 * String[] selectionArgs = new String[] {contactId}; 386 * Cursor cursor = mContentResolver.query( 387 * contentUriForRawContactsEntity, null, selection, selectionArgs, null) 388 * </code> 389 * 390 * You can call this method or a variant of this method just once. In other words, you cannot 391 * reuse this object. 392 * 393 * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really 394 * need to change the default Uri. 395 */ 396 @Deprecated 397 public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) { 398 return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null, 399 contentUriForRawContactsEntity); 400 } 401 402 /** 403 * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection 404 * arguments. 405 */ 406 public boolean init(final String selection, final String[] selectionArgs) { 407 return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs, 408 null, null); 409 } 410 411 /** 412 * Note that this is unstable interface, may be deleted in the future. 413 */ 414 public boolean init(final Uri contentUri, final String selection, 415 final String[] selectionArgs, final String sortOrder) { 416 return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null); 417 } 418 419 /** 420 * A variant of init(). Currently just for testing. Use other variants for init(). 421 * 422 * First we'll create {@link Cursor} for the list of contactId. 423 * 424 * <code> 425 * Cursor cursorForId = mContentResolver.query( 426 * contentUri, projection, selection, selectionArgs, sortOrder); 427 * </code> 428 * 429 * After that, we'll obtain data for each contactId in the list. 430 * 431 * <code> 432 * Cursor cursorForContent = mContentResolver.query( 433 * contentUriForRawContactsEntity, null, 434 * Data.CONTACT_ID + "=?", new String[] {contactId}, null) 435 * </code> 436 * 437 * {@link #createOneEntry()} or its variants let the caller obtain each entry from 438 * <code>cursorForContent</code> above. 439 * 440 * @param contentUri Uri for obtaining the list of contactId. Used with 441 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 442 * @param projection projection used with 443 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 444 * @param selection selection used with 445 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 446 * @param selectionArgs selectionArgs used with 447 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 448 * @param sortOrder sortOrder used with 449 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 450 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each 451 * contactId. 452 * @return true when successful 453 * 454 * @hide 455 */ 456 public boolean init(final Uri contentUri, final String[] projection, 457 final String selection, final String[] selectionArgs, 458 final String sortOrder, Uri contentUriForRawContactsEntity) { 459 if (!Contacts.CONTENT_URI.equals(contentUri)) { 460 if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri); 461 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 462 return false; 463 } 464 if (!initInterFirstPart(contentUriForRawContactsEntity)) { 465 return false; 466 } 467 if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs, 468 sortOrder)) { 469 return false; 470 } 471 if (!initInterMainPart()) { 472 return false; 473 } 474 return initInterLastPart(); 475 } 476 477 /** 478 * Just for testing for now. Do not use. 479 * @hide 480 */ 481 public boolean init(Cursor cursor) { 482 if (!initInterFirstPart(null)) { 483 return false; 484 } 485 mCursorSuppliedFromOutside = true; 486 mCursor = cursor; 487 if (!initInterMainPart()) { 488 return false; 489 } 490 return initInterLastPart(); 491 } 492 493 private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) { 494 mContentUriForRawContactsEntity = 495 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity : 496 RawContactsEntity.CONTENT_URI); 497 if (mInitDone) { 498 Log.e(LOG_TAG, "init() is already called"); 499 return false; 500 } 501 502 if (mCareHandlerErrors) { 503 final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 504 mHandlerList.size()); 505 for (OneEntryHandler handler : mHandlerList) { 506 if (!handler.onInit(mContext)) { 507 if (DEBUG) { 508 Log.d(LOG_TAG, 509 String.format("One of OneEntryHandler (%s) return false on init.", 510 handler.toString())); 511 } 512 for (OneEntryHandler finished : finishedList) { 513 finished.onTerminate(); 514 } 515 return false; 516 } 517 } 518 } else { 519 // Just ignore the false returned from onInit(). 520 for (OneEntryHandler handler : mHandlerList) { 521 handler.onInit(mContext); 522 } 523 } 524 525 return true; 526 } 527 528 private boolean initInterCursorCreationPart( 529 final Uri contentUri, final String[] projection, 530 final String selection, final String[] selectionArgs, final String sortOrder) { 531 mCursorSuppliedFromOutside = false; 532 mCursor = mContentResolver.query( 533 contentUri, projection, selection, selectionArgs, sortOrder); 534 535 if (mCursor == null) { 536 Log.e(LOG_TAG, String.format("Cursor became null unexpectedly")); 537 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 538 return false; 539 } 540 return true; 541 } 542 543 private boolean initInterMainPart() { 544 if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) { 545 if (DEBUG) { 546 Log.d(LOG_TAG, 547 String.format("mCursor has an error (getCount: %d): ", mCursor.getCount())); 548 } 549 closeCursorIfAppropriate(); 550 return false; 551 } 552 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 553 return mIdColumn >= 0; 554 } 555 556 private boolean initInterLastPart() { 557 mInitDone = true; 558 mTerminateCalled = false; 559 return true; 560 } 561 562 // TODO: replace this with createOneEntryNew(). Also remove OneEntryHandler. init/terminate 563 // capability can be prepared if caller really wants. 564 public boolean createOneEntry() { 565 return createOneEntry(null); 566 } 567 568 /** 569 * @return a vCard string. 570 */ 571 public String createOneEntryNew() { 572 return createOneEntryNew(null); 573 } 574 575 /** 576 * @hide 577 */ 578 public String createOneEntryNew(Method getEntityIteratorMethod) { 579 final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn), 580 getEntityIteratorMethod); 581 if (!mCursor.moveToNext()) { 582 Log.e(LOG_TAG, "Cursor#moveToNext() returned false"); 583 } 584 return vcard; 585 } 586 587 /** 588 * @param getEntityIteratorMethod For Dependency Injection. 589 * @hide just for testing. 590 */ 591 public boolean createOneEntry(Method getEntityIteratorMethod) { 592 if (!mInitDone) { 593 mErrorReason = FAILURE_REASON_NOT_INITIALIZED; 594 return false; 595 } 596 final String vcard; 597 try { 598 if (mIdColumn >= 0) { 599 vcard = createOneEntryInternal(mCursor.getString(mIdColumn), 600 getEntityIteratorMethod); 601 } else { 602 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); 603 return true; 604 } 605 } catch (OutOfMemoryError error) { 606 // Maybe some data (e.g. photo) is too big to have in memory. But it 607 // should be rare. 608 Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry."); 609 System.gc(); 610 // TODO: should tell users what happened? 611 return true; 612 } finally { 613 if (!mCursor.moveToNext()) { 614 Log.e(LOG_TAG, "Cursor#moveToNext() returned false"); 615 } 616 } 617 618 // This function does not care the OutOfMemoryError on the handler side :-P 619 if (mCareHandlerErrors) { 620 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 621 mHandlerList.size()); 622 for (OneEntryHandler handler : mHandlerList) { 623 if (!handler.onEntryCreated(vcard)) { 624 return false; 625 } 626 } 627 } else { 628 for (OneEntryHandler handler : mHandlerList) { 629 handler.onEntryCreated(vcard); 630 } 631 } 632 633 return true; 634 } 635 636 private String createOneEntryInternal(final String contactId, 637 final Method getEntityIteratorMethod) { 638 final Map<String, List<ContentValues>> contentValuesListMap = 639 new HashMap<String, List<ContentValues>>(); 640 // The resolver may return the entity iterator with no data. It is possible. 641 // e.g. If all the data in the contact of the given contact id are not exportable ones, 642 // they are hidden from the view of this method, though contact id itself exists. 643 EntityIterator entityIterator = null; 644 try { 645 final Uri uri = mContentUriForRawContactsEntity; 646 final String selection = Data.CONTACT_ID + "=?"; 647 final String[] selectionArgs = new String[] {contactId}; 648 if (getEntityIteratorMethod != null) { 649 // Please note that this branch is executed by unit tests only 650 try { 651 entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, 652 mContentResolver, uri, selection, selectionArgs, null); 653 } catch (IllegalArgumentException e) { 654 Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + 655 e.getMessage()); 656 } catch (IllegalAccessException e) { 657 Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + 658 e.getMessage()); 659 } catch (InvocationTargetException e) { 660 Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); 661 StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); 662 for (StackTraceElement element : stackTraceElements) { 663 Log.e(LOG_TAG, " at " + element.toString()); 664 } 665 throw new RuntimeException("InvocationTargetException has been thrown: " + 666 e.getCause().getMessage()); 667 } 668 } else { 669 entityIterator = RawContacts.newEntityIterator(mContentResolver.query( 670 uri, null, selection, selectionArgs, null)); 671 } 672 673 if (entityIterator == null) { 674 Log.e(LOG_TAG, "EntityIterator is null"); 675 return ""; 676 } 677 678 if (!entityIterator.hasNext()) { 679 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); 680 return ""; 681 } 682 683 while (entityIterator.hasNext()) { 684 Entity entity = entityIterator.next(); 685 for (NamedContentValues namedContentValues : entity.getSubValues()) { 686 ContentValues contentValues = namedContentValues.values; 687 String key = contentValues.getAsString(Data.MIMETYPE); 688 if (key != null) { 689 List<ContentValues> contentValuesList = 690 contentValuesListMap.get(key); 691 if (contentValuesList == null) { 692 contentValuesList = new ArrayList<ContentValues>(); 693 contentValuesListMap.put(key, contentValuesList); 694 } 695 contentValuesList.add(contentValues); 696 } 697 } 698 } 699 } finally { 700 if (entityIterator != null) { 701 entityIterator.close(); 702 } 703 } 704 705 return buildVCard(contentValuesListMap); 706 } 707 708 /** 709 * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in 710 * {ContactsContract}. Developers can override this method to customize the output. 711 */ 712 public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { 713 if (contentValuesListMap == null) { 714 Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); 715 return ""; 716 } else { 717 final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); 718 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) 719 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) 720 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) 721 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) 722 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) 723 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) 724 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); 725 if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { 726 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); 727 } 728 builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) 729 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) 730 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) 731 .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE)) 732 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); 733 return builder.toString(); 734 } 735 } 736 737 public void terminate() { 738 for (OneEntryHandler handler : mHandlerList) { 739 handler.onTerminate(); 740 } 741 742 closeCursorIfAppropriate(); 743 mTerminateCalled = true; 744 } 745 746 private void closeCursorIfAppropriate() { 747 if (!mCursorSuppliedFromOutside && mCursor != null) { 748 try { 749 mCursor.close(); 750 } catch (SQLiteException e) { 751 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 752 } 753 mCursor = null; 754 } 755 } 756 757 @Override 758 protected void finalize() throws Throwable { 759 try { 760 if (!mTerminateCalled) { 761 Log.e(LOG_TAG, "finalized() is called before terminate() being called"); 762 } 763 } finally { 764 super.finalize(); 765 } 766 } 767 768 /** 769 * @return returns the number of available entities. The return value is undefined 770 * when this object is not ready yet (typically when {{@link #init()} is not called 771 * or when {@link #terminate()} is already called). 772 */ 773 public int getCount() { 774 if (mCursor == null) { 775 Log.w(LOG_TAG, "This object is not ready yet."); 776 return 0; 777 } 778 return mCursor.getCount(); 779 } 780 781 /** 782 * @return true when there's no entity to be built. The return value is undefined 783 * when this object is not ready yet. 784 */ 785 public boolean isAfterLast() { 786 if (mCursor == null) { 787 Log.w(LOG_TAG, "This object is not ready yet."); 788 return false; 789 } 790 return mCursor.isAfterLast(); 791 } 792 793 /** 794 * @return Returns the error reason. 795 */ 796 public String getErrorReason() { 797 return mErrorReason; 798 } 799} 800