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