Home | History | Annotate | Download | only in vcard
      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  */
     16 package com.android.vcard;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.content.Entity;
     22 import android.content.Entity.NamedContentValues;
     23 import android.content.EntityIterator;
     24 import android.database.Cursor;
     25 import android.database.sqlite.SQLiteException;
     26 import android.net.Uri;
     27 import android.provider.ContactsContract.CommonDataKinds.Email;
     28 import android.provider.ContactsContract.CommonDataKinds.Event;
     29 import android.provider.ContactsContract.CommonDataKinds.Im;
     30 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     31 import android.provider.ContactsContract.CommonDataKinds.Note;
     32 import android.provider.ContactsContract.CommonDataKinds.Organization;
     33 import android.provider.ContactsContract.CommonDataKinds.Phone;
     34 import android.provider.ContactsContract.CommonDataKinds.Photo;
     35 import android.provider.ContactsContract.CommonDataKinds.Relation;
     36 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     37 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     38 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     39 import android.provider.ContactsContract.CommonDataKinds.Website;
     40 import android.provider.ContactsContract.Contacts;
     41 import android.provider.ContactsContract.Data;
     42 import android.provider.ContactsContract.RawContacts;
     43 import android.provider.ContactsContract.RawContactsEntity;
     44 import android.provider.ContactsContract;
     45 import android.text.TextUtils;
     46 import android.util.Log;
     47 
     48 import java.lang.reflect.InvocationTargetException;
     49 import java.lang.reflect.Method;
     50 import java.util.ArrayList;
     51 import java.util.HashMap;
     52 import java.util.List;
     53 import 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  */
     94 public 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             if (SHIFT_JIS.equalsIgnoreCase(charset)) {
    217                 mCharset = charset;
    218             } else {
    219                 /* Log.w(LOG_TAG,
    220                         "The charset \"" + charset + "\" is used while "
    221                         + SHIFT_JIS + " is needed to be used."); */
    222                 if (TextUtils.isEmpty(charset)) {
    223                     mCharset = SHIFT_JIS;
    224                 } else {
    225                     mCharset = charset;
    226                 }
    227             }
    228         } else {
    229             if (TextUtils.isEmpty(charset)) {
    230                 mCharset = UTF_8;
    231             } else {
    232                 mCharset = charset;
    233             }
    234         }
    235 
    236         Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
    237     }
    238 
    239     /**
    240      * Initializes this object using default {@link Contacts#CONTENT_URI}.
    241      *
    242      * You can call this method or a variant of this method just once. In other words, you cannot
    243      * reuse this object.
    244      *
    245      * @return Returns true when initialization is successful and all the other
    246      *          methods are available. Returns false otherwise.
    247      */
    248     public boolean init() {
    249         return init(null, null);
    250     }
    251 
    252     /**
    253      * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
    254      * {@link ContentResolver} with {@link Contacts#_ID}.
    255      * <code>
    256      * String selection = Data.CONTACT_ID + "=?";
    257      * String[] selectionArgs = new String[] {contactId};
    258      * Cursor cursor = mContentResolver.query(
    259      *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
    260      * </code>
    261      *
    262      * You can call this method or a variant of this method just once. In other words, you cannot
    263      * reuse this object.
    264      *
    265      * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
    266      * need to change the default Uri.
    267      */
    268     @Deprecated
    269     public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
    270         return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
    271                 contentUriForRawContactsEntity);
    272     }
    273 
    274     /**
    275      * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
    276      * arguments.
    277      */
    278     public boolean init(final String selection, final String[] selectionArgs) {
    279         return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
    280                 null, null);
    281     }
    282 
    283     /**
    284      * Note that this is unstable interface, may be deleted in the future.
    285      */
    286     public boolean init(final Uri contentUri, final String selection,
    287             final String[] selectionArgs, final String sortOrder) {
    288         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
    289     }
    290 
    291     /**
    292      * @param contentUri Uri for obtaining the list of contactId. Used with
    293      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    294      * @param selection selection used with
    295      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    296      * @param selectionArgs selectionArgs used with
    297      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    298      * @param sortOrder sortOrder used with
    299      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    300      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
    301      * contactId.
    302      * Note that this is an unstable interface, may be deleted in the future.
    303      */
    304     public boolean init(final Uri contentUri, final String selection,
    305             final String[] selectionArgs, final String sortOrder,
    306             final Uri contentUriForRawContactsEntity) {
    307         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
    308                 contentUriForRawContactsEntity);
    309     }
    310 
    311     /**
    312      * A variant of init(). Currently just for testing. Use other variants for init().
    313      *
    314      * First we'll create {@link Cursor} for the list of contactId.
    315      *
    316      * <code>
    317      * Cursor cursorForId = mContentResolver.query(
    318      *         contentUri, projection, selection, selectionArgs, sortOrder);
    319      * </code>
    320      *
    321      * After that, we'll obtain data for each contactId in the list.
    322      *
    323      * <code>
    324      * Cursor cursorForContent = mContentResolver.query(
    325      *         contentUriForRawContactsEntity, null,
    326      *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
    327      * </code>
    328      *
    329      * {@link #createOneEntry()} or its variants let the caller obtain each entry from
    330      * <code>cursorForContent</code> above.
    331      *
    332      * @param contentUri Uri for obtaining the list of contactId. Used with
    333      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    334      * @param projection projection used with
    335      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    336      * @param selection selection used with
    337      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    338      * @param selectionArgs selectionArgs used with
    339      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    340      * @param sortOrder sortOrder used with
    341      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
    342      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
    343      * contactId.
    344      * @return true when successful
    345      *
    346      * @hide
    347      */
    348     public boolean init(final Uri contentUri, final String[] projection,
    349             final String selection, final String[] selectionArgs,
    350             final String sortOrder, Uri contentUriForRawContactsEntity) {
    351         if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
    352             if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
    353             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
    354             return false;
    355         }
    356 
    357         if (!initInterFirstPart(contentUriForRawContactsEntity)) {
    358             return false;
    359         }
    360         if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
    361                 sortOrder)) {
    362             return false;
    363         }
    364         if (!initInterMainPart()) {
    365             return false;
    366         }
    367         return initInterLastPart();
    368     }
    369 
    370     /**
    371      * Just for testing for now. Do not use.
    372      * @hide
    373      */
    374     public boolean init(Cursor cursor) {
    375         if (!initInterFirstPart(null)) {
    376             return false;
    377         }
    378         mCursorSuppliedFromOutside = true;
    379         mCursor = cursor;
    380         if (!initInterMainPart()) {
    381             return false;
    382         }
    383         return initInterLastPart();
    384     }
    385 
    386     private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
    387         mContentUriForRawContactsEntity =
    388                 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
    389                         RawContactsEntity.CONTENT_URI);
    390         if (mInitDone) {
    391             Log.e(LOG_TAG, "init() is already called");
    392             return false;
    393         }
    394         return true;
    395     }
    396 
    397     private boolean initInterCursorCreationPart(
    398             final Uri contentUri, final String[] projection,
    399             final String selection, final String[] selectionArgs, final String sortOrder) {
    400         mCursorSuppliedFromOutside = false;
    401         mCursor = mContentResolver.query(
    402                 contentUri, projection, selection, selectionArgs, sortOrder);
    403         if (mCursor == null) {
    404             Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
    405             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
    406             return false;
    407         }
    408         return true;
    409     }
    410 
    411     private boolean initInterMainPart() {
    412         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
    413             if (DEBUG) {
    414                 Log.d(LOG_TAG,
    415                     String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
    416             }
    417             closeCursorIfAppropriate();
    418             return false;
    419         }
    420         mIdColumn = mCursor.getColumnIndex(Contacts._ID);
    421         return mIdColumn >= 0;
    422     }
    423 
    424     private boolean initInterLastPart() {
    425         mInitDone = true;
    426         mTerminateCalled = false;
    427         return true;
    428     }
    429 
    430     /**
    431      * @return a vCard string.
    432      */
    433     public String createOneEntry() {
    434         return createOneEntry(null);
    435     }
    436 
    437     /**
    438      * @hide
    439      */
    440     public String createOneEntry(Method getEntityIteratorMethod) {
    441         if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
    442             mFirstVCardEmittedInDoCoMoCase = true;
    443             // Previously we needed to emit empty data for this specific case, but actually
    444             // this doesn't work now, as resolver doesn't return any data with "-1" contactId.
    445             // TODO: re-introduce or remove this logic. Needs to modify unit test when we
    446             // re-introduce the logic.
    447             // return createOneEntryInternal("-1", getEntityIteratorMethod);
    448         }
    449 
    450         final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
    451                 getEntityIteratorMethod);
    452         if (!mCursor.moveToNext()) {
    453             Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
    454         }
    455         return vcard;
    456     }
    457 
    458     private String createOneEntryInternal(final String contactId,
    459             final Method getEntityIteratorMethod) {
    460         final Map<String, List<ContentValues>> contentValuesListMap =
    461                 new HashMap<String, List<ContentValues>>();
    462         // The resolver may return the entity iterator with no data. It is possible.
    463         // e.g. If all the data in the contact of the given contact id are not exportable ones,
    464         //      they are hidden from the view of this method, though contact id itself exists.
    465         EntityIterator entityIterator = null;
    466         try {
    467             final Uri uri = mContentUriForRawContactsEntity;
    468             final String selection = Data.CONTACT_ID + "=?";
    469             final String[] selectionArgs = new String[] {contactId};
    470             if (getEntityIteratorMethod != null) {
    471                 // Please note that this branch is executed by unit tests only
    472                 try {
    473                     entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
    474                             mContentResolver, uri, selection, selectionArgs, null);
    475                 } catch (IllegalArgumentException e) {
    476                     Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
    477                             e.getMessage());
    478                 } catch (IllegalAccessException e) {
    479                     Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
    480                             e.getMessage());
    481                 } catch (InvocationTargetException e) {
    482                     Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
    483                     throw new RuntimeException("InvocationTargetException has been thrown");
    484                 }
    485             } else {
    486                 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
    487                         uri, null, selection, selectionArgs, null));
    488             }
    489 
    490             if (entityIterator == null) {
    491                 Log.e(LOG_TAG, "EntityIterator is null");
    492                 return "";
    493             }
    494 
    495             if (!entityIterator.hasNext()) {
    496                 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
    497                 return "";
    498             }
    499 
    500             while (entityIterator.hasNext()) {
    501                 Entity entity = entityIterator.next();
    502                 for (NamedContentValues namedContentValues : entity.getSubValues()) {
    503                     ContentValues contentValues = namedContentValues.values;
    504                     String key = contentValues.getAsString(Data.MIMETYPE);
    505                     if (key != null) {
    506                         List<ContentValues> contentValuesList =
    507                                 contentValuesListMap.get(key);
    508                         if (contentValuesList == null) {
    509                             contentValuesList = new ArrayList<ContentValues>();
    510                             contentValuesListMap.put(key, contentValuesList);
    511                         }
    512                         contentValuesList.add(contentValues);
    513                     }
    514                 }
    515             }
    516         } finally {
    517             if (entityIterator != null) {
    518                 entityIterator.close();
    519             }
    520         }
    521 
    522         return buildVCard(contentValuesListMap);
    523     }
    524 
    525     private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
    526     /**
    527      * <p>
    528      * Set a callback for phone number formatting. It will be called every time when this object
    529      * receives a phone number for printing.
    530      * </p>
    531      * <p>
    532      * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
    533      * and the callback should be responsible for everything about phone number formatting.
    534      * </p>
    535      * <p>
    536      * Caution: This interface will change. Please don't use without any strong reason.
    537      * </p>
    538      */
    539     public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
    540         mPhoneTranslationCallback = callback;
    541     }
    542 
    543     /**
    544      * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
    545      * {ContactsContract}. Developers can override this method to customize the output.
    546      */
    547     public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
    548         if (contentValuesListMap == null) {
    549             Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
    550             return "";
    551         } else {
    552             final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
    553             builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
    554                     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
    555                     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
    556                             mPhoneTranslationCallback)
    557                     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
    558                     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
    559                     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
    560                     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
    561             if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
    562                 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
    563             }
    564             builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
    565                     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
    566                     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
    567                     .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
    568                     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
    569             return builder.toString();
    570         }
    571     }
    572 
    573     public void terminate() {
    574         closeCursorIfAppropriate();
    575         mTerminateCalled = true;
    576     }
    577 
    578     private void closeCursorIfAppropriate() {
    579         if (!mCursorSuppliedFromOutside && mCursor != null) {
    580             try {
    581                 mCursor.close();
    582             } catch (SQLiteException e) {
    583                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    584             }
    585             mCursor = null;
    586         }
    587     }
    588 
    589     @Override
    590     protected void finalize() throws Throwable {
    591         try {
    592             if (!mTerminateCalled) {
    593                 Log.e(LOG_TAG, "finalized() is called before terminate() being called");
    594             }
    595         } finally {
    596             super.finalize();
    597         }
    598     }
    599 
    600     /**
    601      * @return returns the number of available entities. The return value is undefined
    602      * when this object is not ready yet (typically when {{@link #init()} is not called
    603      * or when {@link #terminate()} is already called).
    604      */
    605     public int getCount() {
    606         if (mCursor == null) {
    607             Log.w(LOG_TAG, "This object is not ready yet.");
    608             return 0;
    609         }
    610         return mCursor.getCount();
    611     }
    612 
    613     /**
    614      * @return true when there's no entity to be built. The return value is undefined
    615      * when this object is not ready yet.
    616      */
    617     public boolean isAfterLast() {
    618         if (mCursor == null) {
    619             Log.w(LOG_TAG, "This object is not ready yet.");
    620             return false;
    621         }
    622         return mCursor.isAfterLast();
    623     }
    624 
    625     /**
    626      * @return Returns the error reason.
    627      */
    628     public String getErrorReason() {
    629         return mErrorReason;
    630     }
    631 }
    632