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