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