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.provider.ContactsContract; 45import android.text.TextUtils; 46import android.util.Log; 47 48import java.lang.reflect.InvocationTargetException; 49import java.lang.reflect.Method; 50import java.util.ArrayList; 51import java.util.HashMap; 52import java.util.List; 53import java.util.Map; 54 55/** 56 * <p> 57 * The class for composing vCard from Contacts information. 58 * </p> 59 * <p> 60 * Usually, this class should be used like this. 61 * </p> 62 * <pre class="prettyprint">VCardComposer composer = null; 63 * try { 64 * composer = new VCardComposer(context); 65 * composer.addHandler( 66 * composer.new HandlerForOutputStream(outputStream)); 67 * if (!composer.init()) { 68 * // Do something handling the situation. 69 * return; 70 * } 71 * while (!composer.isAfterLast()) { 72 * if (mCanceled) { 73 * // Assume a user may cancel this operation during the export. 74 * return; 75 * } 76 * if (!composer.createOneEntry()) { 77 * // Do something handling the error situation. 78 * return; 79 * } 80 * } 81 * } finally { 82 * if (composer != null) { 83 * composer.terminate(); 84 * } 85 * }</pre> 86 * <p> 87 * Users have to manually take care of memory efficiency. Even one vCard may contain 88 * image of non-trivial size for mobile devices. 89 * </p> 90 * <p> 91 * {@link VCardBuilder} is used to build each vCard. 92 * </p> 93 */ 94public class VCardComposer { 95 private static final String LOG_TAG = "VCardComposer"; 96 private static final boolean DEBUG = false; 97 98 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 99 "Failed to get database information"; 100 101 public static final String FAILURE_REASON_NO_ENTRY = 102 "There's no exportable in the database"; 103 104 public static final String FAILURE_REASON_NOT_INITIALIZED = 105 "The vCard composer object is not correctly initialized"; 106 107 /** Should be visible only from developers... (no need to translate, hopefully) */ 108 public static final String FAILURE_REASON_UNSUPPORTED_URI = 109 "The Uri vCard composer received is not supported by the composer."; 110 111 public static final String NO_ERROR = "No error"; 112 113 // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, 114 // since usual vCard devices for Japanese devices already use it. 115 private static final String SHIFT_JIS = "SHIFT_JIS"; 116 private static final String UTF_8 = "UTF-8"; 117 118 private static final Map<Integer, String> sImMap; 119 120 static { 121 sImMap = new HashMap<Integer, String>(); 122 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); 123 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); 124 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); 125 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); 126 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); 127 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 128 // We don't add Google talk here since it has to be handled separately. 129 } 130 131 private final int mVCardType; 132 private final ContentResolver mContentResolver; 133 134 private final boolean mIsDoCoMo; 135 /** 136 * Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo 137 * vCard is emitted. 138 */ 139 private boolean mFirstVCardEmittedInDoCoMoCase; 140 141 private Cursor mCursor; 142 private boolean mCursorSuppliedFromOutside; 143 private int mIdColumn; 144 private Uri mContentUriForRawContactsEntity; 145 146 private final String mCharset; 147 148 private boolean mInitDone; 149 private String mErrorReason = NO_ERROR; 150 151 /** 152 * Set to false when one of {@link #init()} variants is called, and set to true when 153 * {@link #terminate()} is called. Initially set to true. 154 */ 155 private boolean mTerminateCalled = true; 156 157 private static final String[] sContactsProjection = new String[] { 158 Contacts._ID, 159 }; 160 161 public VCardComposer(Context context) { 162 this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); 163 } 164 165 /** 166 * The variant which sets charset to null and sets careHandlerErrors to true. 167 */ 168 public VCardComposer(Context context, int vcardType) { 169 this(context, vcardType, null, true); 170 } 171 172 public VCardComposer(Context context, int vcardType, String charset) { 173 this(context, vcardType, charset, true); 174 } 175 176 /** 177 * The variant which sets charset to null. 178 */ 179 public VCardComposer(final Context context, final int vcardType, 180 final boolean careHandlerErrors) { 181 this(context, vcardType, null, careHandlerErrors); 182 } 183 184 /** 185 * Constructs for supporting call log entry vCard composing. 186 * 187 * @param context Context to be used during the composition. 188 * @param vcardType The type of vCard, typically available via {@link VCardConfig}. 189 * @param charset The charset to be used. Use null when you don't need the charset. 190 * @param careHandlerErrors If true, This object returns false everytime 191 */ 192 public VCardComposer(final Context context, final int vcardType, String charset, 193 final boolean careHandlerErrors) { 194 this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors); 195 } 196 197 /** 198 * Just for testing for now. 199 * @param resolver {@link ContentResolver} which used by this object. 200 * @hide 201 */ 202 public VCardComposer(final Context context, ContentResolver resolver, 203 final int vcardType, String charset, final boolean careHandlerErrors) { 204 // Not used right now 205 // mContext = context; 206 mVCardType = vcardType; 207 mContentResolver = resolver; 208 209 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 210 211 charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); 212 final boolean shouldAppendCharsetParam = !( 213 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset)); 214 215 if (mIsDoCoMo || shouldAppendCharsetParam) { 216 // TODO: clean up once we're sure CharsetUtils are really unnecessary any more. 217 if (SHIFT_JIS.equalsIgnoreCase(charset)) { 218 /*if (mIsDoCoMo) { 219 try { 220 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); 221 } catch (UnsupportedCharsetException e) { 222 Log.e(LOG_TAG, 223 "DoCoMo-specific SHIFT_JIS was not found. " 224 + "Use SHIFT_JIS as is."); 225 charset = SHIFT_JIS; 226 } 227 } else { 228 try { 229 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); 230 } catch (UnsupportedCharsetException e) { 231 // Log.e(LOG_TAG, 232 // "Career-specific SHIFT_JIS was not found. " 233 // + "Use SHIFT_JIS as is."); 234 charset = SHIFT_JIS; 235 } 236 }*/ 237 mCharset = charset; 238 } else { 239 /* Log.w(LOG_TAG, 240 "The charset \"" + charset + "\" is used while " 241 + SHIFT_JIS + " is needed to be used."); */ 242 if (TextUtils.isEmpty(charset)) { 243 mCharset = SHIFT_JIS; 244 } else { 245 /* 246 try { 247 charset = CharsetUtils.charsetForVendor(charset).name(); 248 } catch (UnsupportedCharsetException e) { 249 Log.i(LOG_TAG, 250 "Career-specific \"" + charset + "\" was not found (as usual). " 251 + "Use it as is."); 252 }*/ 253 mCharset = charset; 254 } 255 } 256 } else { 257 if (TextUtils.isEmpty(charset)) { 258 mCharset = UTF_8; 259 } else { 260 /*try { 261 charset = CharsetUtils.charsetForVendor(charset).name(); 262 } catch (UnsupportedCharsetException e) { 263 Log.i(LOG_TAG, 264 "Career-specific \"" + charset + "\" was not found (as usual). " 265 + "Use it as is."); 266 }*/ 267 mCharset = charset; 268 } 269 } 270 271 Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); 272 } 273 274 /** 275 * Initializes this object using default {@link Contacts#CONTENT_URI}. 276 * 277 * You can call this method or a variant of this method just once. In other words, you cannot 278 * reuse this object. 279 * 280 * @return Returns true when initialization is successful and all the other 281 * methods are available. Returns false otherwise. 282 */ 283 public boolean init() { 284 return init(null, null); 285 } 286 287 /** 288 * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from 289 * {@link ContentResolver} with {@link Contacts#_ID}. 290 * <code> 291 * String selection = Data.CONTACT_ID + "=?"; 292 * String[] selectionArgs = new String[] {contactId}; 293 * Cursor cursor = mContentResolver.query( 294 * contentUriForRawContactsEntity, null, selection, selectionArgs, null) 295 * </code> 296 * 297 * You can call this method or a variant of this method just once. In other words, you cannot 298 * reuse this object. 299 * 300 * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really 301 * need to change the default Uri. 302 */ 303 @Deprecated 304 public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) { 305 return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null, 306 contentUriForRawContactsEntity); 307 } 308 309 /** 310 * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection 311 * arguments. 312 */ 313 public boolean init(final String selection, final String[] selectionArgs) { 314 return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs, 315 null, null); 316 } 317 318 /** 319 * Note that this is unstable interface, may be deleted in the future. 320 */ 321 public boolean init(final Uri contentUri, final String selection, 322 final String[] selectionArgs, final String sortOrder) { 323 return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null); 324 } 325 326 /** 327 * @param contentUri Uri for obtaining the list of contactId. Used with 328 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 329 * @param selection selection used with 330 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 331 * @param selectionArgs selectionArgs used with 332 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 333 * @param sortOrder sortOrder used with 334 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 335 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each 336 * contactId. 337 * Note that this is an unstable interface, may be deleted in the future. 338 */ 339 public boolean init(final Uri contentUri, final String selection, 340 final String[] selectionArgs, final String sortOrder, 341 final Uri contentUriForRawContactsEntity) { 342 return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, 343 contentUriForRawContactsEntity); 344 } 345 346 /** 347 * A variant of init(). Currently just for testing. Use other variants for init(). 348 * 349 * First we'll create {@link Cursor} for the list of contactId. 350 * 351 * <code> 352 * Cursor cursorForId = mContentResolver.query( 353 * contentUri, projection, selection, selectionArgs, sortOrder); 354 * </code> 355 * 356 * After that, we'll obtain data for each contactId in the list. 357 * 358 * <code> 359 * Cursor cursorForContent = mContentResolver.query( 360 * contentUriForRawContactsEntity, null, 361 * Data.CONTACT_ID + "=?", new String[] {contactId}, null) 362 * </code> 363 * 364 * {@link #createOneEntry()} or its variants let the caller obtain each entry from 365 * <code>cursorForContent</code> above. 366 * 367 * @param contentUri Uri for obtaining the list of contactId. Used with 368 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 369 * @param projection projection used with 370 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 371 * @param selection selection used with 372 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 373 * @param selectionArgs selectionArgs used with 374 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 375 * @param sortOrder sortOrder used with 376 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 377 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each 378 * contactId. 379 * @return true when successful 380 * 381 * @hide 382 */ 383 public boolean init(final Uri contentUri, final String[] projection, 384 final String selection, final String[] selectionArgs, 385 final String sortOrder, Uri contentUriForRawContactsEntity) { 386 if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) { 387 if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri); 388 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 389 return false; 390 } 391 392 if (!initInterFirstPart(contentUriForRawContactsEntity)) { 393 return false; 394 } 395 if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs, 396 sortOrder)) { 397 return false; 398 } 399 if (!initInterMainPart()) { 400 return false; 401 } 402 return initInterLastPart(); 403 } 404 405 /** 406 * Just for testing for now. Do not use. 407 * @hide 408 */ 409 public boolean init(Cursor cursor) { 410 if (!initInterFirstPart(null)) { 411 return false; 412 } 413 mCursorSuppliedFromOutside = true; 414 mCursor = cursor; 415 if (!initInterMainPart()) { 416 return false; 417 } 418 return initInterLastPart(); 419 } 420 421 private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) { 422 mContentUriForRawContactsEntity = 423 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity : 424 RawContactsEntity.CONTENT_URI); 425 if (mInitDone) { 426 Log.e(LOG_TAG, "init() is already called"); 427 return false; 428 } 429 return true; 430 } 431 432 private boolean initInterCursorCreationPart( 433 final Uri contentUri, final String[] projection, 434 final String selection, final String[] selectionArgs, final String sortOrder) { 435 mCursorSuppliedFromOutside = false; 436 mCursor = mContentResolver.query( 437 contentUri, projection, selection, selectionArgs, sortOrder); 438 if (mCursor == null) { 439 Log.e(LOG_TAG, String.format("Cursor became null unexpectedly")); 440 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 441 return false; 442 } 443 return true; 444 } 445 446 private boolean initInterMainPart() { 447 if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) { 448 if (DEBUG) { 449 Log.d(LOG_TAG, 450 String.format("mCursor has an error (getCount: %d): ", mCursor.getCount())); 451 } 452 closeCursorIfAppropriate(); 453 return false; 454 } 455 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 456 return mIdColumn >= 0; 457 } 458 459 private boolean initInterLastPart() { 460 mInitDone = true; 461 mTerminateCalled = false; 462 return true; 463 } 464 465 /** 466 * @return a vCard string. 467 */ 468 public String createOneEntry() { 469 return createOneEntry(null); 470 } 471 472 /** 473 * @hide 474 */ 475 public String createOneEntry(Method getEntityIteratorMethod) { 476 if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) { 477 mFirstVCardEmittedInDoCoMoCase = true; 478 // Previously we needed to emit empty data for this specific case, but actually 479 // this doesn't work now, as resolver doesn't return any data with "-1" contactId. 480 // TODO: re-introduce or remove this logic. Needs to modify unit test when we 481 // re-introduce the logic. 482 // return createOneEntryInternal("-1", getEntityIteratorMethod); 483 } 484 485 final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn), 486 getEntityIteratorMethod); 487 if (!mCursor.moveToNext()) { 488 Log.e(LOG_TAG, "Cursor#moveToNext() returned false"); 489 } 490 return vcard; 491 } 492 493 private String createOneEntryInternal(final String contactId, 494 final Method getEntityIteratorMethod) { 495 final Map<String, List<ContentValues>> contentValuesListMap = 496 new HashMap<String, List<ContentValues>>(); 497 // The resolver may return the entity iterator with no data. It is possible. 498 // e.g. If all the data in the contact of the given contact id are not exportable ones, 499 // they are hidden from the view of this method, though contact id itself exists. 500 EntityIterator entityIterator = null; 501 try { 502 final Uri uri = mContentUriForRawContactsEntity; 503 final String selection = Data.CONTACT_ID + "=?"; 504 final String[] selectionArgs = new String[] {contactId}; 505 if (getEntityIteratorMethod != null) { 506 // Please note that this branch is executed by unit tests only 507 try { 508 entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, 509 mContentResolver, uri, selection, selectionArgs, null); 510 } catch (IllegalArgumentException e) { 511 Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + 512 e.getMessage()); 513 } catch (IllegalAccessException e) { 514 Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + 515 e.getMessage()); 516 } catch (InvocationTargetException e) { 517 Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e); 518 throw new RuntimeException("InvocationTargetException has been thrown"); 519 } 520 } else { 521 entityIterator = RawContacts.newEntityIterator(mContentResolver.query( 522 uri, null, selection, selectionArgs, null)); 523 } 524 525 if (entityIterator == null) { 526 Log.e(LOG_TAG, "EntityIterator is null"); 527 return ""; 528 } 529 530 if (!entityIterator.hasNext()) { 531 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); 532 return ""; 533 } 534 535 while (entityIterator.hasNext()) { 536 Entity entity = entityIterator.next(); 537 for (NamedContentValues namedContentValues : entity.getSubValues()) { 538 ContentValues contentValues = namedContentValues.values; 539 String key = contentValues.getAsString(Data.MIMETYPE); 540 if (key != null) { 541 List<ContentValues> contentValuesList = 542 contentValuesListMap.get(key); 543 if (contentValuesList == null) { 544 contentValuesList = new ArrayList<ContentValues>(); 545 contentValuesListMap.put(key, contentValuesList); 546 } 547 contentValuesList.add(contentValues); 548 } 549 } 550 } 551 } finally { 552 if (entityIterator != null) { 553 entityIterator.close(); 554 } 555 } 556 557 return buildVCard(contentValuesListMap); 558 } 559 560 private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback; 561 /** 562 * <p> 563 * Set a callback for phone number formatting. It will be called every time when this object 564 * receives a phone number for printing. 565 * </p> 566 * <p> 567 * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored 568 * and the callback should be responsible for everything about phone number formatting. 569 * </p> 570 * <p> 571 * Caution: This interface will change. Please don't use without any strong reason. 572 * </p> 573 */ 574 public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) { 575 mPhoneTranslationCallback = callback; 576 } 577 578 /** 579 * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in 580 * {ContactsContract}. Developers can override this method to customize the output. 581 */ 582 public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { 583 if (contentValuesListMap == null) { 584 Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); 585 return ""; 586 } else { 587 final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); 588 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) 589 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) 590 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE), 591 mPhoneTranslationCallback) 592 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) 593 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) 594 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) 595 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); 596 if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { 597 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); 598 } 599 builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) 600 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) 601 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) 602 .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE)) 603 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); 604 return builder.toString(); 605 } 606 } 607 608 public void terminate() { 609 closeCursorIfAppropriate(); 610 mTerminateCalled = true; 611 } 612 613 private void closeCursorIfAppropriate() { 614 if (!mCursorSuppliedFromOutside && mCursor != null) { 615 try { 616 mCursor.close(); 617 } catch (SQLiteException e) { 618 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 619 } 620 mCursor = null; 621 } 622 } 623 624 @Override 625 protected void finalize() throws Throwable { 626 try { 627 if (!mTerminateCalled) { 628 Log.e(LOG_TAG, "finalized() is called before terminate() being called"); 629 } 630 } finally { 631 super.finalize(); 632 } 633 } 634 635 /** 636 * @return returns the number of available entities. The return value is undefined 637 * when this object is not ready yet (typically when {{@link #init()} is not called 638 * or when {@link #terminate()} is already called). 639 */ 640 public int getCount() { 641 if (mCursor == null) { 642 Log.w(LOG_TAG, "This object is not ready yet."); 643 return 0; 644 } 645 return mCursor.getCount(); 646 } 647 648 /** 649 * @return true when there's no entity to be built. The return value is undefined 650 * when this object is not ready yet. 651 */ 652 public boolean isAfterLast() { 653 if (mCursor == null) { 654 Log.w(LOG_TAG, "This object is not ready yet."); 655 return false; 656 } 657 return mCursor.isAfterLast(); 658 } 659 660 /** 661 * @return Returns the error reason. 662 */ 663 public String getErrorReason() { 664 return mErrorReason; 665 } 666} 667