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