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 android.content.ContentResolver;
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.Entity;
22import android.content.Entity.NamedContentValues;
23import android.content.EntityIterator;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteException;
26import android.net.Uri;
27import android.provider.ContactsContract.CommonDataKinds.Email;
28import android.provider.ContactsContract.CommonDataKinds.Event;
29import android.provider.ContactsContract.CommonDataKinds.Im;
30import android.provider.ContactsContract.CommonDataKinds.Nickname;
31import android.provider.ContactsContract.CommonDataKinds.Note;
32import android.provider.ContactsContract.CommonDataKinds.Organization;
33import android.provider.ContactsContract.CommonDataKinds.Phone;
34import android.provider.ContactsContract.CommonDataKinds.Photo;
35import android.provider.ContactsContract.CommonDataKinds.Relation;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredName;
38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
39import android.provider.ContactsContract.CommonDataKinds.Website;
40import android.provider.ContactsContract.Contacts;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.RawContacts;
43import android.provider.ContactsContract.RawContactsEntity;
44import android.provider.ContactsContract;
45import android.text.TextUtils;
46import android.util.Log;
47
48import java.lang.reflect.InvocationTargetException;
49import java.lang.reflect.Method;
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.List;
53import java.util.Map;
54
55/**
56 * <p>
57 * The class for composing vCard from Contacts information.
58 * </p>
59 * <p>
60 * Usually, this class should be used like this.
61 * </p>
62 * <pre class="prettyprint">VCardComposer composer = null;
63 * try {
64 *     composer = new VCardComposer(context);
65 *     composer.addHandler(
66 *             composer.new HandlerForOutputStream(outputStream));
67 *     if (!composer.init()) {
68 *         // Do something handling the situation.
69 *         return;
70 *     }
71 *     while (!composer.isAfterLast()) {
72 *         if (mCanceled) {
73 *             // Assume a user may cancel this operation during the export.
74 *             return;
75 *         }
76 *         if (!composer.createOneEntry()) {
77 *             // Do something handling the error situation.
78 *             return;
79 *         }
80 *     }
81 * } finally {
82 *     if (composer != null) {
83 *         composer.terminate();
84 *     }
85 * }</pre>
86 * <p>
87 * Users have to manually take care of memory efficiency. Even one vCard may contain
88 * image of non-trivial size for mobile devices.
89 * </p>
90 * <p>
91 * {@link VCardBuilder} is used to build each vCard.
92 * </p>
93 */
94public class VCardComposer {
95    private static final String LOG_TAG = "VCardComposer";
96    private static final boolean DEBUG = false;
97
98    public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
99        "Failed to get database information";
100
101    public static final String FAILURE_REASON_NO_ENTRY =
102        "There's no exportable in the database";
103
104    public static final String FAILURE_REASON_NOT_INITIALIZED =
105        "The vCard composer object is not correctly initialized";
106
107    /** Should be visible only from developers... (no need to translate, hopefully) */
108    public static final String FAILURE_REASON_UNSUPPORTED_URI =
109        "The Uri vCard composer received is not supported by the composer.";
110
111    public static final String NO_ERROR = "No error";
112
113    // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
114    // since usual vCard devices for Japanese devices already use it.
115    private static final String SHIFT_JIS = "SHIFT_JIS";
116    private static final String UTF_8 = "UTF-8";
117
118    private static final Map<Integer, String> sImMap;
119
120    static {
121        sImMap = new HashMap<Integer, String>();
122        sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
123        sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
124        sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
125        sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
126        sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
127        sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
128        // We don't add Google talk here since it has to be handled separately.
129    }
130
131    private final int mVCardType;
132    private final ContentResolver mContentResolver;
133
134    private final boolean mIsDoCoMo;
135    /**
136     * Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo
137     * vCard is emitted.
138     */
139    private boolean mFirstVCardEmittedInDoCoMoCase;
140
141    private Cursor mCursor;
142    private boolean mCursorSuppliedFromOutside;
143    private int mIdColumn;
144    private Uri mContentUriForRawContactsEntity;
145
146    private final String mCharset;
147
148    private boolean mInitDone;
149    private String mErrorReason = NO_ERROR;
150
151    /**
152     * Set to false when one of {@link #init()} variants is called, and set to true when
153     * {@link #terminate()} is called. Initially set to true.
154     */
155    private boolean mTerminateCalled = true;
156
157    private static final String[] sContactsProjection = new String[] {
158        Contacts._ID,
159    };
160
161    public VCardComposer(Context context) {
162        this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
163    }
164
165    /**
166     * The variant which sets charset to null and sets careHandlerErrors to true.
167     */
168    public VCardComposer(Context context, int vcardType) {
169        this(context, vcardType, null, true);
170    }
171
172    public VCardComposer(Context context, int vcardType, String charset) {
173        this(context, vcardType, charset, true);
174    }
175
176    /**
177     * The variant which sets charset to null.
178     */
179    public VCardComposer(final Context context, final int vcardType,
180            final boolean careHandlerErrors) {
181        this(context, vcardType, null, careHandlerErrors);
182    }
183
184    /**
185     * Constructs for supporting call log entry vCard composing.
186     *
187     * @param context Context to be used during the composition.
188     * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
189     * @param charset The charset to be used. Use null when you don't need the charset.
190     * @param careHandlerErrors If true, This object returns false everytime
191     */
192    public VCardComposer(final Context context, final int vcardType, String charset,
193            final boolean careHandlerErrors) {
194        this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
195    }
196
197    /**
198     * Just for testing for now.
199     * @param resolver {@link ContentResolver} which used by this object.
200     * @hide
201     */
202    public VCardComposer(final Context context, ContentResolver resolver,
203            final int vcardType, String charset, final boolean careHandlerErrors) {
204        // Not used right now
205        // mContext = context;
206        mVCardType = vcardType;
207        mContentResolver = resolver;
208
209        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
210
211        charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
212        final boolean shouldAppendCharsetParam = !(
213                VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
214
215        if (mIsDoCoMo || shouldAppendCharsetParam) {
216            // TODO: clean up once we're sure CharsetUtils are really unnecessary any more.
217            if (SHIFT_JIS.equalsIgnoreCase(charset)) {
218                /*if (mIsDoCoMo) {
219                    try {
220                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
221                    } catch (UnsupportedCharsetException e) {
222                        Log.e(LOG_TAG,
223                                "DoCoMo-specific SHIFT_JIS was not found. "
224                                + "Use SHIFT_JIS as is.");
225                        charset = SHIFT_JIS;
226                    }
227                } else {
228                    try {
229                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
230                    } catch (UnsupportedCharsetException e) {
231                        // Log.e(LOG_TAG,
232                        // "Career-specific SHIFT_JIS was not found. "
233                        // + "Use SHIFT_JIS as is.");
234                        charset = SHIFT_JIS;
235                    }
236                }*/
237                mCharset = charset;
238            } else {
239                /* Log.w(LOG_TAG,
240                        "The charset \"" + charset + "\" is used while "
241                        + SHIFT_JIS + " is needed to be used."); */
242                if (TextUtils.isEmpty(charset)) {
243                    mCharset = SHIFT_JIS;
244                } else {
245                    /*
246                    try {
247                        charset = CharsetUtils.charsetForVendor(charset).name();
248                    } catch (UnsupportedCharsetException e) {
249                        Log.i(LOG_TAG,
250                                "Career-specific \"" + charset + "\" was not found (as usual). "
251                                + "Use it as is.");
252                    }*/
253                    mCharset = charset;
254                }
255            }
256        } else {
257            if (TextUtils.isEmpty(charset)) {
258                mCharset = UTF_8;
259            } else {
260                /*try {
261                    charset = CharsetUtils.charsetForVendor(charset).name();
262                } catch (UnsupportedCharsetException e) {
263                    Log.i(LOG_TAG,
264                            "Career-specific \"" + charset + "\" was not found (as usual). "
265                            + "Use it as is.");
266                }*/
267                mCharset = charset;
268            }
269        }
270
271        Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
272    }
273
274    /**
275     * Initializes this object using default {@link Contacts#CONTENT_URI}.
276     *
277     * You can call this method or a variant of this method just once. In other words, you cannot
278     * reuse this object.
279     *
280     * @return Returns true when initialization is successful and all the other
281     *          methods are available. Returns false otherwise.
282     */
283    public boolean init() {
284        return init(null, null);
285    }
286
287    /**
288     * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
289     * {@link ContentResolver} with {@link Contacts#_ID}.
290     * <code>
291     * String selection = Data.CONTACT_ID + "=?";
292     * String[] selectionArgs = new String[] {contactId};
293     * Cursor cursor = mContentResolver.query(
294     *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
295     * </code>
296     *
297     * You can call this method or a variant of this method just once. In other words, you cannot
298     * reuse this object.
299     *
300     * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
301     * need to change the default Uri.
302     */
303    @Deprecated
304    public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
305        return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
306                contentUriForRawContactsEntity);
307    }
308
309    /**
310     * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
311     * arguments.
312     */
313    public boolean init(final String selection, final String[] selectionArgs) {
314        return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
315                null, null);
316    }
317
318    /**
319     * Note that this is unstable interface, may be deleted in the future.
320     */
321    public boolean init(final Uri contentUri, final String selection,
322            final String[] selectionArgs, final String sortOrder) {
323        return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
324    }
325
326    /**
327     * @param contentUri Uri for obtaining the list of contactId. Used with
328     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
329     * @param selection selection used with
330     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
331     * @param selectionArgs selectionArgs used with
332     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
333     * @param sortOrder sortOrder used with
334     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
335     * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
336     * contactId.
337     * Note that this is an unstable interface, may be deleted in the future.
338     */
339    public boolean init(final Uri contentUri, final String selection,
340            final String[] selectionArgs, final String sortOrder,
341            final Uri contentUriForRawContactsEntity) {
342        return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
343                contentUriForRawContactsEntity);
344    }
345
346    /**
347     * A variant of init(). Currently just for testing. Use other variants for init().
348     *
349     * First we'll create {@link Cursor} for the list of contactId.
350     *
351     * <code>
352     * Cursor cursorForId = mContentResolver.query(
353     *         contentUri, projection, selection, selectionArgs, sortOrder);
354     * </code>
355     *
356     * After that, we'll obtain data for each contactId in the list.
357     *
358     * <code>
359     * Cursor cursorForContent = mContentResolver.query(
360     *         contentUriForRawContactsEntity, null,
361     *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
362     * </code>
363     *
364     * {@link #createOneEntry()} or its variants let the caller obtain each entry from
365     * <code>cursorForContent</code> above.
366     *
367     * @param contentUri Uri for obtaining the list of contactId. Used with
368     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
369     * @param projection projection used with
370     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
371     * @param selection selection used with
372     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
373     * @param selectionArgs selectionArgs used with
374     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
375     * @param sortOrder sortOrder used with
376     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
377     * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
378     * contactId.
379     * @return true when successful
380     *
381     * @hide
382     */
383    public boolean init(final Uri contentUri, final String[] projection,
384            final String selection, final String[] selectionArgs,
385            final String sortOrder, Uri contentUriForRawContactsEntity) {
386        if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
387            if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
388            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
389            return false;
390        }
391
392        if (!initInterFirstPart(contentUriForRawContactsEntity)) {
393            return false;
394        }
395        if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
396                sortOrder)) {
397            return false;
398        }
399        if (!initInterMainPart()) {
400            return false;
401        }
402        return initInterLastPart();
403    }
404
405    /**
406     * Just for testing for now. Do not use.
407     * @hide
408     */
409    public boolean init(Cursor cursor) {
410        if (!initInterFirstPart(null)) {
411            return false;
412        }
413        mCursorSuppliedFromOutside = true;
414        mCursor = cursor;
415        if (!initInterMainPart()) {
416            return false;
417        }
418        return initInterLastPart();
419    }
420
421    private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
422        mContentUriForRawContactsEntity =
423                (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
424                        RawContactsEntity.CONTENT_URI);
425        if (mInitDone) {
426            Log.e(LOG_TAG, "init() is already called");
427            return false;
428        }
429        return true;
430    }
431
432    private boolean initInterCursorCreationPart(
433            final Uri contentUri, final String[] projection,
434            final String selection, final String[] selectionArgs, final String sortOrder) {
435        mCursorSuppliedFromOutside = false;
436        mCursor = mContentResolver.query(
437                contentUri, projection, selection, selectionArgs, sortOrder);
438        if (mCursor == null) {
439            Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
440            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
441            return false;
442        }
443        return true;
444    }
445
446    private boolean initInterMainPart() {
447        if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
448            if (DEBUG) {
449                Log.d(LOG_TAG,
450                    String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
451            }
452            closeCursorIfAppropriate();
453            return false;
454        }
455        mIdColumn = mCursor.getColumnIndex(Contacts._ID);
456        return mIdColumn >= 0;
457    }
458
459    private boolean initInterLastPart() {
460        mInitDone = true;
461        mTerminateCalled = false;
462        return true;
463    }
464
465    /**
466     * @return a vCard string.
467     */
468    public String createOneEntry() {
469        return createOneEntry(null);
470    }
471
472    /**
473     * @hide
474     */
475    public String createOneEntry(Method getEntityIteratorMethod) {
476        if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
477            mFirstVCardEmittedInDoCoMoCase = true;
478            // Previously we needed to emit empty data for this specific case, but actually
479            // this doesn't work now, as resolver doesn't return any data with "-1" contactId.
480            // TODO: re-introduce or remove this logic. Needs to modify unit test when we
481            // re-introduce the logic.
482            // return createOneEntryInternal("-1", getEntityIteratorMethod);
483        }
484
485        final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
486                getEntityIteratorMethod);
487        if (!mCursor.moveToNext()) {
488            Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
489        }
490        return vcard;
491    }
492
493    private String createOneEntryInternal(final String contactId,
494            final Method getEntityIteratorMethod) {
495        final Map<String, List<ContentValues>> contentValuesListMap =
496                new HashMap<String, List<ContentValues>>();
497        // The resolver may return the entity iterator with no data. It is possible.
498        // e.g. If all the data in the contact of the given contact id are not exportable ones,
499        //      they are hidden from the view of this method, though contact id itself exists.
500        EntityIterator entityIterator = null;
501        try {
502            final Uri uri = mContentUriForRawContactsEntity;
503            final String selection = Data.CONTACT_ID + "=?";
504            final String[] selectionArgs = new String[] {contactId};
505            if (getEntityIteratorMethod != null) {
506                // Please note that this branch is executed by unit tests only
507                try {
508                    entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
509                            mContentResolver, uri, selection, selectionArgs, null);
510                } catch (IllegalArgumentException e) {
511                    Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
512                            e.getMessage());
513                } catch (IllegalAccessException e) {
514                    Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
515                            e.getMessage());
516                } catch (InvocationTargetException e) {
517                    Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
518                    throw new RuntimeException("InvocationTargetException has been thrown");
519                }
520            } else {
521                entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
522                        uri, null, selection, selectionArgs, null));
523            }
524
525            if (entityIterator == null) {
526                Log.e(LOG_TAG, "EntityIterator is null");
527                return "";
528            }
529
530            if (!entityIterator.hasNext()) {
531                Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
532                return "";
533            }
534
535            while (entityIterator.hasNext()) {
536                Entity entity = entityIterator.next();
537                for (NamedContentValues namedContentValues : entity.getSubValues()) {
538                    ContentValues contentValues = namedContentValues.values;
539                    String key = contentValues.getAsString(Data.MIMETYPE);
540                    if (key != null) {
541                        List<ContentValues> contentValuesList =
542                                contentValuesListMap.get(key);
543                        if (contentValuesList == null) {
544                            contentValuesList = new ArrayList<ContentValues>();
545                            contentValuesListMap.put(key, contentValuesList);
546                        }
547                        contentValuesList.add(contentValues);
548                    }
549                }
550            }
551        } finally {
552            if (entityIterator != null) {
553                entityIterator.close();
554            }
555        }
556
557        return buildVCard(contentValuesListMap);
558    }
559
560    private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
561    /**
562     * <p>
563     * Set a callback for phone number formatting. It will be called every time when this object
564     * receives a phone number for printing.
565     * </p>
566     * <p>
567     * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
568     * and the callback should be responsible for everything about phone number formatting.
569     * </p>
570     * <p>
571     * Caution: This interface will change. Please don't use without any strong reason.
572     * </p>
573     */
574    public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
575        mPhoneTranslationCallback = callback;
576    }
577
578    /**
579     * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
580     * {ContactsContract}. Developers can override this method to customize the output.
581     */
582    public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
583        if (contentValuesListMap == null) {
584            Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
585            return "";
586        } else {
587            final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
588            builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
589                    .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
590                    .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
591                            mPhoneTranslationCallback)
592                    .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
593                    .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
594                    .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
595                    .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
596            if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
597                builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
598            }
599            builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
600                    .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
601                    .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
602                    .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
603                    .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
604            return builder.toString();
605        }
606    }
607
608    public void terminate() {
609        closeCursorIfAppropriate();
610        mTerminateCalled = true;
611    }
612
613    private void closeCursorIfAppropriate() {
614        if (!mCursorSuppliedFromOutside && mCursor != null) {
615            try {
616                mCursor.close();
617            } catch (SQLiteException e) {
618                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
619            }
620            mCursor = null;
621        }
622    }
623
624    @Override
625    protected void finalize() throws Throwable {
626        try {
627            if (!mTerminateCalled) {
628                Log.e(LOG_TAG, "finalized() is called before terminate() being called");
629            }
630        } finally {
631            super.finalize();
632        }
633    }
634
635    /**
636     * @return returns the number of available entities. The return value is undefined
637     * when this object is not ready yet (typically when {{@link #init()} is not called
638     * or when {@link #terminate()} is already called).
639     */
640    public int getCount() {
641        if (mCursor == null) {
642            Log.w(LOG_TAG, "This object is not ready yet.");
643            return 0;
644        }
645        return mCursor.getCount();
646    }
647
648    /**
649     * @return true when there's no entity to be built. The return value is undefined
650     * when this object is not ready yet.
651     */
652    public boolean isAfterLast() {
653        if (mCursor == null) {
654            Log.w(LOG_TAG, "This object is not ready yet.");
655            return false;
656        }
657        return mCursor.isAfterLast();
658    }
659
660    /**
661     * @return Returns the error reason.
662     */
663    public String getErrorReason() {
664        return mErrorReason;
665    }
666}
667