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 RawContactEntitlesInfoCallback mRawContactEntitlesInfoCallback;
158
159    private static final String[] sContactsProjection = new String[] {
160        Contacts._ID,
161    };
162
163    public VCardComposer(Context context) {
164        this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
165    }
166
167    /**
168     * The variant which sets charset to null and sets careHandlerErrors to true.
169     */
170    public VCardComposer(Context context, int vcardType) {
171        this(context, vcardType, null, true);
172    }
173
174    public VCardComposer(Context context, int vcardType, String charset) {
175        this(context, vcardType, charset, true);
176    }
177
178    /**
179     * The variant which sets charset to null.
180     */
181    public VCardComposer(final Context context, final int vcardType,
182            final boolean careHandlerErrors) {
183        this(context, vcardType, null, careHandlerErrors);
184    }
185
186    /**
187     * Constructs for supporting call log entry vCard composing.
188     *
189     * @param context Context to be used during the composition.
190     * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
191     * @param charset The charset to be used. Use null when you don't need the charset.
192     * @param careHandlerErrors If true, This object returns false everytime
193     */
194    public VCardComposer(final Context context, final int vcardType, String charset,
195            final boolean careHandlerErrors) {
196        this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
197    }
198
199    /**
200     * Just for testing for now.
201     * @param resolver {@link ContentResolver} which used by this object.
202     * @hide
203     */
204    public VCardComposer(final Context context, ContentResolver resolver,
205            final int vcardType, String charset, final boolean careHandlerErrors) {
206        // Not used right now
207        // mContext = context;
208        mVCardType = vcardType;
209        mContentResolver = resolver;
210
211        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
212
213        charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
214        final boolean shouldAppendCharsetParam = !(
215                VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
216
217        if (mIsDoCoMo || shouldAppendCharsetParam) {
218            if (SHIFT_JIS.equalsIgnoreCase(charset)) {
219                mCharset = charset;
220            } else {
221                /* Log.w(LOG_TAG,
222                        "The charset \"" + charset + "\" is used while "
223                        + SHIFT_JIS + " is needed to be used."); */
224                if (TextUtils.isEmpty(charset)) {
225                    mCharset = SHIFT_JIS;
226                } else {
227                    mCharset = charset;
228                }
229            }
230        } else {
231            if (TextUtils.isEmpty(charset)) {
232                mCharset = UTF_8;
233            } else {
234                mCharset = charset;
235            }
236        }
237
238        Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
239    }
240
241    /**
242     * Initializes this object using default {@link Contacts#CONTENT_URI}.
243     *
244     * You can call this method or a variant of this method just once. In other words, you cannot
245     * reuse this object.
246     *
247     * @return Returns true when initialization is successful and all the other
248     *          methods are available. Returns false otherwise.
249     */
250    public boolean init() {
251        return init(null, null);
252    }
253
254    /**
255     * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
256     * {@link ContentResolver} with {@link Contacts#_ID}.
257     * <code>
258     * String selection = Data.CONTACT_ID + "=?";
259     * String[] selectionArgs = new String[] {contactId};
260     * Cursor cursor = mContentResolver.query(
261     *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
262     * </code>
263     *
264     * You can call this method or a variant of this method just once. In other words, you cannot
265     * reuse this object.
266     *
267     * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
268     * need to change the default Uri.
269     */
270    @Deprecated
271    public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
272        return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
273                contentUriForRawContactsEntity);
274    }
275
276    /**
277     * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
278     * arguments.
279     */
280    public boolean init(final String selection, final String[] selectionArgs) {
281        return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
282                null, null);
283    }
284
285    /**
286     * Note that this is unstable interface, may be deleted in the future.
287     */
288    public boolean init(final Uri contentUri, final String selection,
289            final String[] selectionArgs, final String sortOrder) {
290        return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
291    }
292
293    /**
294     * @param contentUri Uri for obtaining the list of contactId. Used with
295     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
296     * @param selection selection used with
297     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
298     * @param selectionArgs selectionArgs used with
299     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
300     * @param sortOrder sortOrder used with
301     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
302     * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
303     * contactId.
304     * Note that this is an unstable interface, may be deleted in the future.
305     */
306    public boolean init(final Uri contentUri, final String selection,
307            final String[] selectionArgs, final String sortOrder,
308            final Uri contentUriForRawContactsEntity) {
309        return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
310                contentUriForRawContactsEntity);
311    }
312
313    /**
314     * A variant of init(). Currently just for testing. Use other variants for init().
315     *
316     * First we'll create {@link Cursor} for the list of contactId.
317     *
318     * <code>
319     * Cursor cursorForId = mContentResolver.query(
320     *         contentUri, projection, selection, selectionArgs, sortOrder);
321     * </code>
322     *
323     * After that, we'll obtain data for each contactId in the list.
324     *
325     * <code>
326     * Cursor cursorForContent = mContentResolver.query(
327     *         contentUriForRawContactsEntity, null,
328     *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
329     * </code>
330     *
331     * {@link #createOneEntry()} or its variants let the caller obtain each entry from
332     * <code>cursorForContent</code> above.
333     *
334     * @param contentUri Uri for obtaining the list of contactId. Used with
335     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
336     * @param projection projection used with
337     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
338     * @param selection selection used with
339     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
340     * @param selectionArgs selectionArgs used with
341     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
342     * @param sortOrder sortOrder used with
343     * {@link ContentResolver#query(Uri, String[], String, String[], String)}
344     * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
345     * contactId.
346     * @return true when successful
347     *
348     * @hide
349     */
350    public boolean init(final Uri contentUri, final String[] projection,
351            final String selection, final String[] selectionArgs,
352            final String sortOrder, Uri contentUriForRawContactsEntity) {
353        if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
354            if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
355            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
356            return false;
357        }
358
359        if (!initInterFirstPart(contentUriForRawContactsEntity)) {
360            return false;
361        }
362        if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
363                sortOrder)) {
364            return false;
365        }
366        if (!initInterMainPart()) {
367            return false;
368        }
369        return initInterLastPart();
370    }
371
372    /**
373     * Just for testing for now. Do not use.
374     * @hide
375     */
376    public boolean init(Cursor cursor) {
377        return initWithCallback(cursor, null);
378    }
379
380    /**
381    * @param cursor Cursor that used to get contact id
382    * @param rawContactEntitlesInfoCallback Callback that return RawContactEntitlesInfo
383    * Note that this is an unstable interface, may be deleted in the future.
384    *
385    * @return true when successful
386    */
387    public boolean initWithCallback(Cursor cursor,
388            RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback) {
389        if (!initInterFirstPart(null)) {
390            return false;
391        }
392        mCursorSuppliedFromOutside = true;
393        mCursor = cursor;
394        mRawContactEntitlesInfoCallback = rawContactEntitlesInfoCallback;
395        if (!initInterMainPart()) {
396            return false;
397        }
398        return initInterLastPart();
399    }
400
401    private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
402        mContentUriForRawContactsEntity =
403                (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
404                        RawContactsEntity.CONTENT_URI);
405        if (mInitDone) {
406            Log.e(LOG_TAG, "init() is already called");
407            return false;
408        }
409        return true;
410    }
411
412    private boolean initInterCursorCreationPart(
413            final Uri contentUri, final String[] projection,
414            final String selection, final String[] selectionArgs, final String sortOrder) {
415        mCursorSuppliedFromOutside = false;
416        mCursor = mContentResolver.query(
417                contentUri, projection, selection, selectionArgs, sortOrder);
418        if (mCursor == null) {
419            Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
420            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
421            return false;
422        }
423        return true;
424    }
425
426    private boolean initInterMainPart() {
427        if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
428            if (DEBUG) {
429                Log.d(LOG_TAG,
430                    String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
431            }
432            closeCursorIfAppropriate();
433            return false;
434        }
435        mIdColumn = mCursor.getColumnIndex(Data.CONTACT_ID);
436        if (mIdColumn < 0) {
437            mIdColumn = mCursor.getColumnIndex(Contacts._ID);
438        }
439        return mIdColumn >= 0;
440    }
441
442    private boolean initInterLastPart() {
443        mInitDone = true;
444        mTerminateCalled = false;
445        return true;
446    }
447
448    /**
449     * @return a vCard string.
450     */
451    public String createOneEntry() {
452        return createOneEntry(null);
453    }
454
455    /**
456     * @hide
457     */
458    public String createOneEntry(Method getEntityIteratorMethod) {
459        if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
460            mFirstVCardEmittedInDoCoMoCase = true;
461            // Previously we needed to emit empty data for this specific case, but actually
462            // this doesn't work now, as resolver doesn't return any data with "-1" contactId.
463            // TODO: re-introduce or remove this logic. Needs to modify unit test when we
464            // re-introduce the logic.
465            // return createOneEntryInternal("-1", getEntityIteratorMethod);
466        }
467
468        final String vcard = createOneEntryInternal(mCursor.getLong(mIdColumn),
469                getEntityIteratorMethod);
470        if (!mCursor.moveToNext()) {
471            Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
472        }
473        return vcard;
474    }
475
476    /**
477     *  Class that store rawContactEntitlesUri and contactId
478     */
479    public static class RawContactEntitlesInfo {
480        public final Uri rawContactEntitlesUri;
481        public final long contactId;
482        public RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId) {
483            this.rawContactEntitlesUri = rawContactEntitlesUri;
484            this.contactId = contactId;
485        }
486    }
487
488    /**
489    * Listener for getting raw contact entitles info
490    */
491    public interface RawContactEntitlesInfoCallback {
492        /**
493        * Callback to get RawContactEntitlesInfo from contact id
494        *
495        * @param contactId Contact id that you want to process.
496        * @return RawContactEntitlesInfo that ready to process.
497        */
498        RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId);
499    }
500
501    private String createOneEntryInternal(long contactId,
502            final Method getEntityIteratorMethod) {
503        final Map<String, List<ContentValues>> contentValuesListMap =
504                new HashMap<String, List<ContentValues>>();
505        // The resolver may return the entity iterator with no data. It is possible.
506        // e.g. If all the data in the contact of the given contact id are not exportable ones,
507        //      they are hidden from the view of this method, though contact id itself exists.
508        EntityIterator entityIterator = null;
509        try {
510            Uri uri = mContentUriForRawContactsEntity;
511            if (mRawContactEntitlesInfoCallback != null) {
512                RawContactEntitlesInfo rawContactEntitlesInfo =
513                        mRawContactEntitlesInfoCallback.getRawContactEntitlesInfo(contactId);
514                uri = rawContactEntitlesInfo.rawContactEntitlesUri;
515                contactId = rawContactEntitlesInfo.contactId;
516            }
517            final String selection = Data.CONTACT_ID + "=?";
518            final String[] selectionArgs = new String[] {String.valueOf(contactId)};
519            if (getEntityIteratorMethod != null) {
520                // Please note that this branch is executed by unit tests only
521                try {
522                    entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
523                            mContentResolver, uri, selection, selectionArgs, null);
524                } catch (IllegalArgumentException e) {
525                    Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
526                            e.getMessage());
527                } catch (IllegalAccessException e) {
528                    Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
529                            e.getMessage());
530                } catch (InvocationTargetException e) {
531                    Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
532                    throw new RuntimeException("InvocationTargetException has been thrown");
533                }
534            } else {
535                entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
536                        uri, null, selection, selectionArgs, null));
537            }
538
539            if (entityIterator == null) {
540                Log.e(LOG_TAG, "EntityIterator is null");
541                return "";
542            }
543
544            if (!entityIterator.hasNext()) {
545                Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
546                return "";
547            }
548
549            while (entityIterator.hasNext()) {
550                Entity entity = entityIterator.next();
551                for (NamedContentValues namedContentValues : entity.getSubValues()) {
552                    ContentValues contentValues = namedContentValues.values;
553                    String key = contentValues.getAsString(Data.MIMETYPE);
554                    if (key != null) {
555                        List<ContentValues> contentValuesList =
556                                contentValuesListMap.get(key);
557                        if (contentValuesList == null) {
558                            contentValuesList = new ArrayList<ContentValues>();
559                            contentValuesListMap.put(key, contentValuesList);
560                        }
561                        contentValuesList.add(contentValues);
562                    }
563                }
564            }
565        } finally {
566            if (entityIterator != null) {
567                entityIterator.close();
568            }
569        }
570
571        return buildVCard(contentValuesListMap);
572    }
573
574    private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
575    /**
576     * <p>
577     * Set a callback for phone number formatting. It will be called every time when this object
578     * receives a phone number for printing.
579     * </p>
580     * <p>
581     * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
582     * and the callback should be responsible for everything about phone number formatting.
583     * </p>
584     * <p>
585     * Caution: This interface will change. Please don't use without any strong reason.
586     * </p>
587     */
588    public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
589        mPhoneTranslationCallback = callback;
590    }
591
592    /**
593     * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
594     * {ContactsContract}. Developers can override this method to customize the output.
595     */
596    public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
597        if (contentValuesListMap == null) {
598            Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
599            return "";
600        } else {
601            final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
602            builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
603                    .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
604                    .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
605                            mPhoneTranslationCallback)
606                    .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
607                    .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
608                    .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
609                    .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
610            if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
611                builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
612            }
613            builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
614                    .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
615                    .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
616                    .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
617                    .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
618            return builder.toString();
619        }
620    }
621
622    public void terminate() {
623        closeCursorIfAppropriate();
624        mTerminateCalled = true;
625    }
626
627    private void closeCursorIfAppropriate() {
628        if (!mCursorSuppliedFromOutside && mCursor != null) {
629            try {
630                mCursor.close();
631            } catch (SQLiteException e) {
632                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
633            }
634            mCursor = null;
635        }
636    }
637
638    @Override
639    protected void finalize() throws Throwable {
640        try {
641            if (!mTerminateCalled) {
642                Log.e(LOG_TAG, "finalized() is called before terminate() being called");
643            }
644        } finally {
645            super.finalize();
646        }
647    }
648
649    /**
650     * @return returns the number of available entities. The return value is undefined
651     * when this object is not ready yet (typically when {{@link #init()} is not called
652     * or when {@link #terminate()} is already called).
653     */
654    public int getCount() {
655        if (mCursor == null) {
656            Log.w(LOG_TAG, "This object is not ready yet.");
657            return 0;
658        }
659        return mCursor.getCount();
660    }
661
662    /**
663     * @return true when there's no entity to be built. The return value is undefined
664     * when this object is not ready yet.
665     */
666    public boolean isAfterLast() {
667        if (mCursor == null) {
668            Log.w(LOG_TAG, "This object is not ready yet.");
669            return false;
670        }
671        return mCursor.isAfterLast();
672    }
673
674    /**
675     * @return Returns the error reason.
676     */
677    public String getErrorReason() {
678        return mErrorReason;
679    }
680}
681