summaryrefslogtreecommitdiffstats
path: root/vcard/java/com/android/vcard/VCardComposer.java
diff options
context:
space:
mode:
Diffstat (limited to 'vcard/java/com/android/vcard/VCardComposer.java')
-rw-r--r--vcard/java/com/android/vcard/VCardComposer.java677
1 files changed, 677 insertions, 0 deletions
diff --git a/vcard/java/com/android/vcard/VCardComposer.java b/vcard/java/com/android/vcard/VCardComposer.java
new file mode 100644
index 0000000..7038955
--- /dev/null
+++ b/vcard/java/com/android/vcard/VCardComposer.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.vcard;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.TextUtils;
+import android.util.CharsetUtils;
+import android.util.Log;
+
+import com.android.vcard.exception.VCardException;
+
+import java.io.BufferedWriter;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * The class for composing vCard from Contacts information.
+ * </p>
+ * <p>
+ * Usually, this class should be used like this.
+ * </p>
+ * <pre class="prettyprint">VCardComposer composer = null;
+ * try {
+ * composer = new VCardComposer(context);
+ * composer.addHandler(
+ * composer.new HandlerForOutputStream(outputStream));
+ * if (!composer.init()) {
+ * // Do something handling the situation.
+ * return;
+ * }
+ * while (!composer.isAfterLast()) {
+ * if (mCanceled) {
+ * // Assume a user may cancel this operation during the export.
+ * return;
+ * }
+ * if (!composer.createOneEntry()) {
+ * // Do something handling the error situation.
+ * return;
+ * }
+ * }
+ * } finally {
+ * if (composer != null) {
+ * composer.terminate();
+ * }
+ * }</pre>
+ * <p>
+ * Users have to manually take care of memory efficiency. Even one vCard may contain
+ * image of non-trivial size for mobile devices.
+ * </p>
+ * <p>
+ * {@link VCardBuilder} is used to build each vCard.
+ * </p>
+ */
+public class VCardComposer {
+ private static final String LOG_TAG = "VCardComposer";
+
+ public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
+ "Failed to get database information";
+
+ public static final String FAILURE_REASON_NO_ENTRY =
+ "There's no exportable in the database";
+
+ public static final String FAILURE_REASON_NOT_INITIALIZED =
+ "The vCard composer object is not correctly initialized";
+
+ /** Should be visible only from developers... (no need to translate, hopefully) */
+ public static final String FAILURE_REASON_UNSUPPORTED_URI =
+ "The Uri vCard composer received is not supported by the composer.";
+
+ public static final String NO_ERROR = "No error";
+
+ public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
+
+ // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
+ // since usual vCard devices for Japanese devices already use it.
+ private static final String SHIFT_JIS = "SHIFT_JIS";
+ private static final String UTF_8 = "UTF-8";
+
+ /**
+ * Special URI for testing.
+ */
+ public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
+ public static final Uri VCARD_TEST_AUTHORITY_URI =
+ Uri.parse("content://" + VCARD_TEST_AUTHORITY);
+ public static final Uri CONTACTS_TEST_CONTENT_URI =
+ Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
+
+ private static final Map<Integer, String> sImMap;
+
+ static {
+ sImMap = new HashMap<Integer, String>();
+ sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
+ sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
+ sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
+ sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
+ sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
+ sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
+ // We don't add Google talk here since it has to be handled separately.
+ }
+
+ public static interface OneEntryHandler {
+ public boolean onInit(Context context);
+ public boolean onEntryCreated(String vcard);
+ public void onTerminate();
+ }
+
+ /**
+ * <p>
+ * An useful handler for emitting vCard String to an OutputStream object one by one.
+ * </p>
+ * <p>
+ * The input OutputStream object is closed() on {@link #onTerminate()}.
+ * Must not close the stream outside this class.
+ * </p>
+ */
+ public final class HandlerForOutputStream implements OneEntryHandler {
+ @SuppressWarnings("hiding")
+ private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream";
+
+ private boolean mOnTerminateIsCalled = false;
+
+ private final OutputStream mOutputStream; // mWriter will close this.
+ private Writer mWriter;
+
+ /**
+ * Input stream will be closed on the detruction of this object.
+ */
+ public HandlerForOutputStream(final OutputStream outputStream) {
+ mOutputStream = outputStream;
+ }
+
+ public boolean onInit(final Context context) {
+ try {
+ mWriter = new BufferedWriter(new OutputStreamWriter(
+ mOutputStream, mCharset));
+ } catch (UnsupportedEncodingException e1) {
+ Log.e(LOG_TAG, "Unsupported charset: " + mCharset);
+ mErrorReason = "Encoding is not supported (usually this does not happen!): "
+ + mCharset;
+ return false;
+ }
+
+ if (mIsDoCoMo) {
+ try {
+ // Create one empty entry.
+ mWriter.write(createOneEntryInternal("-1", null));
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
+ e.getMessage());
+ return false;
+ } catch (IOException e) {
+ Log.e(LOG_TAG,
+ "IOException occurred during exportOneContactData: "
+ + e.getMessage());
+ mErrorReason = "IOException occurred: " + e.getMessage();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean onEntryCreated(String vcard) {
+ try {
+ mWriter.write(vcard);
+ } catch (IOException e) {
+ Log.e(LOG_TAG,
+ "IOException occurred during exportOneContactData: "
+ + e.getMessage());
+ mErrorReason = "IOException occurred: " + e.getMessage();
+ return false;
+ }
+ return true;
+ }
+
+ public void onTerminate() {
+ mOnTerminateIsCalled = true;
+ if (mWriter != null) {
+ try {
+ // Flush and sync the data so that a user is able to pull
+ // the SDCard just after
+ // the export.
+ mWriter.flush();
+ if (mOutputStream != null
+ && mOutputStream instanceof FileOutputStream) {
+ ((FileOutputStream) mOutputStream).getFD().sync();
+ }
+ } catch (IOException e) {
+ Log.d(LOG_TAG,
+ "IOException during closing the output stream: "
+ + e.getMessage());
+ } finally {
+ closeOutputStream();
+ }
+ }
+ }
+
+ public void closeOutputStream() {
+ try {
+ mWriter.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring.");
+ }
+ }
+
+ @Override
+ public void finalize() {
+ if (!mOnTerminateIsCalled) {
+ onTerminate();
+ }
+ }
+ }
+
+ private final Context mContext;
+ private final int mVCardType;
+ private final boolean mCareHandlerErrors;
+ private final ContentResolver mContentResolver;
+
+ private final boolean mIsDoCoMo;
+ private Cursor mCursor;
+ private int mIdColumn;
+
+ private final String mCharset;
+ private boolean mTerminateIsCalled;
+ private final List<OneEntryHandler> mHandlerList;
+
+ private String mErrorReason = NO_ERROR;
+
+ private static final String[] sContactsProjection = new String[] {
+ Contacts._ID,
+ };
+
+ public VCardComposer(Context context) {
+ this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
+ }
+
+ /**
+ * The variant which sets charset to null and sets careHandlerErrors to true.
+ */
+ public VCardComposer(Context context, int vcardType) {
+ this(context, vcardType, null, true);
+ }
+
+ public VCardComposer(Context context, int vcardType, String charset) {
+ this(context, vcardType, charset, true);
+ }
+
+ /**
+ * The variant which sets charset to null.
+ */
+ public VCardComposer(final Context context, final int vcardType,
+ final boolean careHandlerErrors) {
+ this(context, vcardType, null, careHandlerErrors);
+ }
+
+ /**
+ * Construct for supporting call log entry vCard composing.
+ *
+ * @param context Context to be used during the composition.
+ * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
+ * @param charset The charset to be used. Use null when you don't need the charset.
+ * @param careHandlerErrors If true, This object returns false everytime
+ * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false.
+ * If false, this ignores those errors.
+ */
+ public VCardComposer(final Context context, final int vcardType, String charset,
+ final boolean careHandlerErrors) {
+ mContext = context;
+ mVCardType = vcardType;
+ mCareHandlerErrors = careHandlerErrors;
+ mContentResolver = context.getContentResolver();
+
+ mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
+ mHandlerList = new ArrayList<OneEntryHandler>();
+
+ charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
+ final boolean shouldAppendCharsetParam = !(
+ VCardConfig.isV30(vcardType) && UTF_8.equalsIgnoreCase(charset));
+
+ if (mIsDoCoMo || shouldAppendCharsetParam) {
+ if (SHIFT_JIS.equalsIgnoreCase(charset)) {
+ if (mIsDoCoMo) {
+ try {
+ charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
+ } catch (UnsupportedCharsetException e) {
+ Log.e(LOG_TAG,
+ "DoCoMo-specific SHIFT_JIS was not found. "
+ + "Use SHIFT_JIS as is.");
+ charset = SHIFT_JIS;
+ }
+ } else {
+ try {
+ charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
+ } catch (UnsupportedCharsetException e) {
+ Log.e(LOG_TAG,
+ "Career-specific SHIFT_JIS was not found. "
+ + "Use SHIFT_JIS as is.");
+ charset = SHIFT_JIS;
+ }
+ }
+ mCharset = charset;
+ } else {
+ Log.w(LOG_TAG,
+ "The charset \"" + charset + "\" is used while "
+ + SHIFT_JIS + " is needed to be used.");
+ if (TextUtils.isEmpty(charset)) {
+ mCharset = SHIFT_JIS;
+ } else {
+ try {
+ charset = CharsetUtils.charsetForVendor(charset).name();
+ } catch (UnsupportedCharsetException e) {
+ Log.i(LOG_TAG,
+ "Career-specific \"" + charset + "\" was not found (as usual). "
+ + "Use it as is.");
+ }
+ mCharset = charset;
+ }
+ }
+ } else {
+ if (TextUtils.isEmpty(charset)) {
+ mCharset = UTF_8;
+ } else {
+ try {
+ charset = CharsetUtils.charsetForVendor(charset).name();
+ } catch (UnsupportedCharsetException e) {
+ Log.i(LOG_TAG,
+ "Career-specific \"" + charset + "\" was not found (as usual). "
+ + "Use it as is.");
+ }
+ mCharset = charset;
+ }
+ }
+
+ Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
+ }
+
+ /**
+ * Must be called before {@link #init()}.
+ */
+ public void addHandler(OneEntryHandler handler) {
+ if (handler != null) {
+ mHandlerList.add(handler);
+ }
+ }
+
+ /**
+ * @return Returns true when initialization is successful and all the other
+ * methods are available. Returns false otherwise.
+ */
+ public boolean init() {
+ return init(null, null);
+ }
+
+ public boolean init(final String selection, final String[] selectionArgs) {
+ return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
+ }
+
+ /**
+ * Note that this is unstable interface, may be deleted in the future.
+ */
+ public boolean init(final Uri contentUri, final String selection,
+ final String[] selectionArgs, final String sortOrder) {
+ if (contentUri == null) {
+ return false;
+ }
+
+ if (mCareHandlerErrors) {
+ final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
+ mHandlerList.size());
+ for (OneEntryHandler handler : mHandlerList) {
+ if (!handler.onInit(mContext)) {
+ for (OneEntryHandler finished : finishedList) {
+ finished.onTerminate();
+ }
+ return false;
+ }
+ }
+ } else {
+ // Just ignore the false returned from onInit().
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onInit(mContext);
+ }
+ }
+
+ final String[] projection;
+ if (Contacts.CONTENT_URI.equals(contentUri) ||
+ CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
+ projection = sContactsProjection;
+ } else {
+ mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
+ return false;
+ }
+ mCursor = mContentResolver.query(
+ contentUri, projection, selection, selectionArgs, sortOrder);
+
+ if (mCursor == null) {
+ mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
+ return false;
+ }
+
+ if (getCount() == 0 || !mCursor.moveToFirst()) {
+ try {
+ mCursor.close();
+ } catch (SQLiteException e) {
+ Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
+ } finally {
+ mCursor = null;
+ mErrorReason = FAILURE_REASON_NO_ENTRY;
+ }
+ return false;
+ }
+
+ mIdColumn = mCursor.getColumnIndex(Contacts._ID);
+
+ return true;
+ }
+
+ public boolean createOneEntry() {
+ return createOneEntry(null);
+ }
+
+ /**
+ * @param getEntityIteratorMethod For Dependency Injection.
+ * @hide just for testing.
+ */
+ public boolean createOneEntry(Method getEntityIteratorMethod) {
+ if (mCursor == null || mCursor.isAfterLast()) {
+ mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
+ return false;
+ }
+ final String vcard;
+ try {
+ if (mIdColumn >= 0) {
+ vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
+ getEntityIteratorMethod);
+ } else {
+ Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
+ return true;
+ }
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
+ return false;
+ } catch (OutOfMemoryError error) {
+ // Maybe some data (e.g. photo) is too big to have in memory. But it
+ // should be rare.
+ Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
+ System.gc();
+ // TODO: should tell users what happened?
+ return true;
+ } finally {
+ mCursor.moveToNext();
+ }
+
+ // This function does not care the OutOfMemoryError on the handler side :-P
+ if (mCareHandlerErrors) {
+ List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
+ mHandlerList.size());
+ for (OneEntryHandler handler : mHandlerList) {
+ if (!handler.onEntryCreated(vcard)) {
+ return false;
+ }
+ }
+ } else {
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onEntryCreated(vcard);
+ }
+ }
+
+ return true;
+ }
+
+ private String createOneEntryInternal(final String contactId,
+ final Method getEntityIteratorMethod) throws VCardException {
+ final Map<String, List<ContentValues>> contentValuesListMap =
+ new HashMap<String, List<ContentValues>>();
+ // The resolver may return the entity iterator with no data. It is possible.
+ // e.g. If all the data in the contact of the given contact id are not exportable ones,
+ // they are hidden from the view of this method, though contact id itself exists.
+ EntityIterator entityIterator = null;
+ try {
+ final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
+ // .appendQueryParameter("for_export_only", "1")
+ .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1")
+ .build();
+ final String selection = Data.CONTACT_ID + "=?";
+ final String[] selectionArgs = new String[] {contactId};
+ if (getEntityIteratorMethod != null) {
+ // Please note that this branch is executed by unit tests only
+ try {
+ entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
+ mContentResolver, uri, selection, selectionArgs, null);
+ } catch (IllegalArgumentException e) {
+ Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
+ e.getMessage());
+ } catch (IllegalAccessException e) {
+ Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
+ e.getMessage());
+ } catch (InvocationTargetException e) {
+ Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
+ StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
+ for (StackTraceElement element : stackTraceElements) {
+ Log.e(LOG_TAG, " at " + element.toString());
+ }
+ throw new VCardException("InvocationTargetException has been thrown: " +
+ e.getCause().getMessage());
+ }
+ } else {
+ entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
+ uri, null, selection, selectionArgs, null));
+ }
+
+ if (entityIterator == null) {
+ Log.e(LOG_TAG, "EntityIterator is null");
+ return "";
+ }
+
+ if (!entityIterator.hasNext()) {
+ Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
+ return "";
+ }
+
+ while (entityIterator.hasNext()) {
+ Entity entity = entityIterator.next();
+ for (NamedContentValues namedContentValues : entity.getSubValues()) {
+ ContentValues contentValues = namedContentValues.values;
+ String key = contentValues.getAsString(Data.MIMETYPE);
+ if (key != null) {
+ List<ContentValues> contentValuesList =
+ contentValuesListMap.get(key);
+ if (contentValuesList == null) {
+ contentValuesList = new ArrayList<ContentValues>();
+ contentValuesListMap.put(key, contentValuesList);
+ }
+ contentValuesList.add(contentValues);
+ }
+ }
+ }
+ } finally {
+ if (entityIterator != null) {
+ entityIterator.close();
+ }
+ }
+
+ return buildVCard(contentValuesListMap);
+ }
+
+ /**
+ * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
+ * {ContactsContract}. Developers can override this method to customize the output.
+ */
+ public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
+ if (contentValuesListMap == null) {
+ Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
+ return "";
+ } else {
+ final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
+ builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
+ .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
+ .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
+ .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
+ .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
+ .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
+ .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
+ .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
+ .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
+ .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
+ .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
+ .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
+ return builder.toString();
+ }
+ }
+
+ public void terminate() {
+ for (OneEntryHandler handler : mHandlerList) {
+ handler.onTerminate();
+ }
+
+ if (mCursor != null) {
+ try {
+ mCursor.close();
+ } catch (SQLiteException e) {
+ Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
+ }
+ mCursor = null;
+ }
+
+ mTerminateIsCalled = true;
+ }
+
+ @Override
+ public void finalize() {
+ if (!mTerminateIsCalled) {
+ Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step.");
+ terminate();
+ }
+ }
+
+ /**
+ * @return returns the number of available entities. The return value is undefined
+ * when this object is not ready yet (typically when {{@link #init()} is not called
+ * or when {@link #terminate()} is already called).
+ */
+ public int getCount() {
+ if (mCursor == null) {
+ Log.w(LOG_TAG, "This object is not ready yet.");
+ return 0;
+ }
+ return mCursor.getCount();
+ }
+
+ /**
+ * @return true when there's no entity to be built. The return value is undefined
+ * when this object is not ready yet.
+ */
+ public boolean isAfterLast() {
+ if (mCursor == null) {
+ Log.w(LOG_TAG, "This object is not ready yet.");
+ return false;
+ }
+ return mCursor.isAfterLast();
+ }
+
+ /**
+ * @return Returns the error reason.
+ */
+ public String getErrorReason() {
+ return mErrorReason;
+ }
+}