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