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 android.pim.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.pim.vcard.exception.VCardException;
     28 import android.provider.ContactsContract.CommonDataKinds.Email;
     29 import android.provider.ContactsContract.CommonDataKinds.Event;
     30 import android.provider.ContactsContract.CommonDataKinds.Im;
     31 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     32 import android.provider.ContactsContract.CommonDataKinds.Note;
     33 import android.provider.ContactsContract.CommonDataKinds.Organization;
     34 import android.provider.ContactsContract.CommonDataKinds.Phone;
     35 import android.provider.ContactsContract.CommonDataKinds.Photo;
     36 import android.provider.ContactsContract.CommonDataKinds.Relation;
     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.text.TextUtils;
     45 import android.util.CharsetUtils;
     46 import android.util.Log;
     47 
     48 import java.io.BufferedWriter;
     49 import java.io.FileOutputStream;
     50 import java.io.IOException;
     51 import java.io.OutputStream;
     52 import java.io.OutputStreamWriter;
     53 import java.io.UnsupportedEncodingException;
     54 import java.io.Writer;
     55 import java.lang.reflect.InvocationTargetException;
     56 import java.lang.reflect.Method;
     57 import java.nio.charset.UnsupportedCharsetException;
     58 import java.util.ArrayList;
     59 import java.util.HashMap;
     60 import java.util.List;
     61 import 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  */
    102 public 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         public boolean onInit(final Context context) {
    181             try {
    182                 mWriter = new BufferedWriter(new OutputStreamWriter(
    183                         mOutputStream, mCharset));
    184             } catch (UnsupportedEncodingException e1) {
    185                 Log.e(LOG_TAG, "Unsupported charset: " + mCharset);
    186                 mErrorReason = "Encoding is not supported (usually this does not happen!): "
    187                         + mCharset;
    188                 return false;
    189             }
    190 
    191             if (mIsDoCoMo) {
    192                 try {
    193                     // Create one empty entry.
    194                     mWriter.write(createOneEntryInternal("-1", null));
    195                 } catch (VCardException e) {
    196                     Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
    197                             e.getMessage());
    198                     return false;
    199                 } catch (IOException e) {
    200                     Log.e(LOG_TAG,
    201                             "IOException occurred during exportOneContactData: "
    202                                     + e.getMessage());
    203                     mErrorReason = "IOException occurred: " + e.getMessage();
    204                     return false;
    205                 }
    206             }
    207             return true;
    208         }
    209 
    210         public boolean onEntryCreated(String vcard) {
    211             try {
    212                 mWriter.write(vcard);
    213             } catch (IOException e) {
    214                 Log.e(LOG_TAG,
    215                         "IOException occurred during exportOneContactData: "
    216                                 + e.getMessage());
    217                 mErrorReason = "IOException occurred: " + e.getMessage();
    218                 return false;
    219             }
    220             return true;
    221         }
    222 
    223         public void onTerminate() {
    224             mOnTerminateIsCalled = true;
    225             if (mWriter != null) {
    226                 try {
    227                     // Flush and sync the data so that a user is able to pull
    228                     // the SDCard just after
    229                     // the export.
    230                     mWriter.flush();
    231                     if (mOutputStream != null
    232                             && mOutputStream instanceof FileOutputStream) {
    233                             ((FileOutputStream) mOutputStream).getFD().sync();
    234                     }
    235                 } catch (IOException e) {
    236                     Log.d(LOG_TAG,
    237                             "IOException during closing the output stream: "
    238                                     + e.getMessage());
    239                 } finally {
    240                     closeOutputStream();
    241                 }
    242             }
    243         }
    244 
    245         public void closeOutputStream() {
    246             try {
    247                 mWriter.close();
    248             } catch (IOException e) {
    249                 Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring.");
    250             }
    251         }
    252 
    253         @Override
    254         public void finalize() {
    255             if (!mOnTerminateIsCalled) {
    256                 onTerminate();
    257             }
    258         }
    259     }
    260 
    261     private final Context mContext;
    262     private final int mVCardType;
    263     private final boolean mCareHandlerErrors;
    264     private final ContentResolver mContentResolver;
    265 
    266     private final boolean mIsDoCoMo;
    267     private Cursor mCursor;
    268     private int mIdColumn;
    269 
    270     private final String mCharset;
    271     private boolean mTerminateIsCalled;
    272     private final List<OneEntryHandler> mHandlerList;
    273 
    274     private String mErrorReason = NO_ERROR;
    275 
    276     private static final String[] sContactsProjection = new String[] {
    277         Contacts._ID,
    278     };
    279 
    280     public VCardComposer(Context context) {
    281         this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
    282     }
    283 
    284     /**
    285      * The variant which sets charset to null and sets careHandlerErrors to true.
    286      */
    287     public VCardComposer(Context context, int vcardType) {
    288         this(context, vcardType, null, true);
    289     }
    290 
    291     public VCardComposer(Context context, int vcardType, String charset) {
    292         this(context, vcardType, charset, true);
    293     }
    294 
    295     /**
    296      * The variant which sets charset to null.
    297      */
    298     public VCardComposer(final Context context, final int vcardType,
    299             final boolean careHandlerErrors) {
    300         this(context, vcardType, null, careHandlerErrors);
    301     }
    302 
    303     /**
    304      * Construct for supporting call log entry vCard composing.
    305      *
    306      * @param context Context to be used during the composition.
    307      * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
    308      * @param charset The charset to be used. Use null when you don't need the charset.
    309      * @param careHandlerErrors If true, This object returns false everytime
    310      * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false.
    311      * If false, this ignores those errors.
    312      */
    313     public VCardComposer(final Context context, final int vcardType, String charset,
    314             final boolean careHandlerErrors) {
    315         mContext = context;
    316         mVCardType = vcardType;
    317         mCareHandlerErrors = careHandlerErrors;
    318         mContentResolver = context.getContentResolver();
    319 
    320         mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
    321         mHandlerList = new ArrayList<OneEntryHandler>();
    322 
    323         charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
    324         final boolean shouldAppendCharsetParam = !(
    325                 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
    326 
    327         if (mIsDoCoMo || shouldAppendCharsetParam) {
    328             if (SHIFT_JIS.equalsIgnoreCase(charset)) {
    329                 if (mIsDoCoMo) {
    330                     try {
    331                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
    332                     } catch (UnsupportedCharsetException e) {
    333                         Log.e(LOG_TAG,
    334                                 "DoCoMo-specific SHIFT_JIS was not found. "
    335                                 + "Use SHIFT_JIS as is.");
    336                         charset = SHIFT_JIS;
    337                     }
    338                 } else {
    339                     try {
    340                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
    341                     } catch (UnsupportedCharsetException e) {
    342                         Log.e(LOG_TAG,
    343                                 "Career-specific SHIFT_JIS was not found. "
    344                                 + "Use SHIFT_JIS as is.");
    345                         charset = SHIFT_JIS;
    346                     }
    347                 }
    348                 mCharset = charset;
    349             } else {
    350                 Log.w(LOG_TAG,
    351                         "The charset \"" + charset + "\" is used while "
    352                         + SHIFT_JIS + " is needed to be used.");
    353                 if (TextUtils.isEmpty(charset)) {
    354                     mCharset = SHIFT_JIS;
    355                 } else {
    356                     try {
    357                         charset = CharsetUtils.charsetForVendor(charset).name();
    358                     } catch (UnsupportedCharsetException e) {
    359                         Log.i(LOG_TAG,
    360                                 "Career-specific \"" + charset + "\" was not found (as usual). "
    361                                 + "Use it as is.");
    362                     }
    363                     mCharset = charset;
    364                 }
    365             }
    366         } else {
    367             if (TextUtils.isEmpty(charset)) {
    368                 mCharset = UTF_8;
    369             } else {
    370                 try {
    371                     charset = CharsetUtils.charsetForVendor(charset).name();
    372                 } catch (UnsupportedCharsetException e) {
    373                     Log.i(LOG_TAG,
    374                             "Career-specific \"" + charset + "\" was not found (as usual). "
    375                             + "Use it as is.");
    376                 }
    377                 mCharset = charset;
    378             }
    379         }
    380 
    381         Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
    382     }
    383 
    384     /**
    385      * Must be called before {@link #init()}.
    386      */
    387     public void addHandler(OneEntryHandler handler) {
    388         if (handler != null) {
    389             mHandlerList.add(handler);
    390         }
    391     }
    392 
    393     /**
    394      * @return Returns true when initialization is successful and all the other
    395      *          methods are available. Returns false otherwise.
    396      */
    397     public boolean init() {
    398         return init(null, null);
    399     }
    400 
    401     public boolean init(final String selection, final String[] selectionArgs) {
    402         return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
    403     }
    404 
    405     /**
    406      * Note that this is unstable interface, may be deleted in the future.
    407      */
    408     public boolean init(final Uri contentUri, final String selection,
    409             final String[] selectionArgs, final String sortOrder) {
    410         if (contentUri == null) {
    411             return false;
    412         }
    413 
    414         if (mCareHandlerErrors) {
    415             final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    416                     mHandlerList.size());
    417             for (OneEntryHandler handler : mHandlerList) {
    418                 if (!handler.onInit(mContext)) {
    419                     for (OneEntryHandler finished : finishedList) {
    420                         finished.onTerminate();
    421                     }
    422                     return false;
    423                 }
    424             }
    425         } else {
    426             // Just ignore the false returned from onInit().
    427             for (OneEntryHandler handler : mHandlerList) {
    428                 handler.onInit(mContext);
    429             }
    430         }
    431 
    432         final String[] projection;
    433         if (Contacts.CONTENT_URI.equals(contentUri) ||
    434                 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
    435             projection = sContactsProjection;
    436         } else {
    437             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
    438             return false;
    439         }
    440         mCursor = mContentResolver.query(
    441                 contentUri, projection, selection, selectionArgs, sortOrder);
    442 
    443         if (mCursor == null) {
    444             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
    445             return false;
    446         }
    447 
    448         if (getCount() == 0 || !mCursor.moveToFirst()) {
    449             try {
    450                 mCursor.close();
    451             } catch (SQLiteException e) {
    452                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    453             } finally {
    454                 mCursor = null;
    455                 mErrorReason = FAILURE_REASON_NO_ENTRY;
    456             }
    457             return false;
    458         }
    459 
    460         mIdColumn = mCursor.getColumnIndex(Contacts._ID);
    461 
    462         return true;
    463     }
    464 
    465     public boolean createOneEntry() {
    466         return createOneEntry(null);
    467     }
    468 
    469     /**
    470      * @param getEntityIteratorMethod For Dependency Injection.
    471      * @hide just for testing.
    472      */
    473     public boolean createOneEntry(Method getEntityIteratorMethod) {
    474         if (mCursor == null || mCursor.isAfterLast()) {
    475             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
    476             return false;
    477         }
    478         final String vcard;
    479         try {
    480             if (mIdColumn >= 0) {
    481                 vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
    482                         getEntityIteratorMethod);
    483             } else {
    484                 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
    485                 return true;
    486             }
    487         } catch (VCardException e) {
    488             Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
    489             return false;
    490         } catch (OutOfMemoryError error) {
    491             // Maybe some data (e.g. photo) is too big to have in memory. But it
    492             // should be rare.
    493             Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
    494             System.gc();
    495             // TODO: should tell users what happened?
    496             return true;
    497         } finally {
    498             mCursor.moveToNext();
    499         }
    500 
    501         // This function does not care the OutOfMemoryError on the handler side :-P
    502         if (mCareHandlerErrors) {
    503             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    504                     mHandlerList.size());
    505             for (OneEntryHandler handler : mHandlerList) {
    506                 if (!handler.onEntryCreated(vcard)) {
    507                     return false;
    508                 }
    509             }
    510         } else {
    511             for (OneEntryHandler handler : mHandlerList) {
    512                 handler.onEntryCreated(vcard);
    513             }
    514         }
    515 
    516         return true;
    517     }
    518 
    519     private String createOneEntryInternal(final String contactId,
    520             final Method getEntityIteratorMethod) throws VCardException {
    521         final Map<String, List<ContentValues>> contentValuesListMap =
    522                 new HashMap<String, List<ContentValues>>();
    523         // The resolver may return the entity iterator with no data. It is possible.
    524         // e.g. If all the data in the contact of the given contact id are not exportable ones,
    525         //      they are hidden from the view of this method, though contact id itself exists.
    526         EntityIterator entityIterator = null;
    527         try {
    528             final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
    529                     // .appendQueryParameter("for_export_only", "1")
    530                     .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1")
    531                     .build();
    532             final String selection = Data.CONTACT_ID + "=?";
    533             final String[] selectionArgs = new String[] {contactId};
    534             if (getEntityIteratorMethod != null) {
    535                 // Please note that this branch is executed by unit tests only
    536                 try {
    537                     entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
    538                             mContentResolver, uri, selection, selectionArgs, null);
    539                 } catch (IllegalArgumentException e) {
    540                     Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
    541                             e.getMessage());
    542                 } catch (IllegalAccessException e) {
    543                     Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
    544                             e.getMessage());
    545                 } catch (InvocationTargetException e) {
    546                     Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
    547                     StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
    548                     for (StackTraceElement element : stackTraceElements) {
    549                         Log.e(LOG_TAG, "    at " + element.toString());
    550                     }
    551                     throw new VCardException("InvocationTargetException has been thrown: " +
    552                             e.getCause().getMessage());
    553                 }
    554             } else {
    555                 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
    556                         uri, null, selection, selectionArgs, null));
    557             }
    558 
    559             if (entityIterator == null) {
    560                 Log.e(LOG_TAG, "EntityIterator is null");
    561                 return "";
    562             }
    563 
    564             if (!entityIterator.hasNext()) {
    565                 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
    566                 return "";
    567             }
    568 
    569             while (entityIterator.hasNext()) {
    570                 Entity entity = entityIterator.next();
    571                 for (NamedContentValues namedContentValues : entity.getSubValues()) {
    572                     ContentValues contentValues = namedContentValues.values;
    573                     String key = contentValues.getAsString(Data.MIMETYPE);
    574                     if (key != null) {
    575                         List<ContentValues> contentValuesList =
    576                                 contentValuesListMap.get(key);
    577                         if (contentValuesList == null) {
    578                             contentValuesList = new ArrayList<ContentValues>();
    579                             contentValuesListMap.put(key, contentValuesList);
    580                         }
    581                         contentValuesList.add(contentValues);
    582                     }
    583                 }
    584             }
    585         } finally {
    586             if (entityIterator != null) {
    587                 entityIterator.close();
    588             }
    589         }
    590 
    591         return buildVCard(contentValuesListMap);
    592     }
    593 
    594     /**
    595      * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
    596      * {ContactsContract}. Developers can override this method to customize the output.
    597      */
    598     public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
    599         if (contentValuesListMap == null) {
    600             Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
    601             return "";
    602         } else {
    603             final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
    604             builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
    605                     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
    606                     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
    607                     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
    608                     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
    609                     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
    610                     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
    611             if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
    612                 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
    613             }
    614             builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
    615                     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
    616                     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
    617                     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
    618             return builder.toString();
    619         }
    620     }
    621 
    622     public void terminate() {
    623         for (OneEntryHandler handler : mHandlerList) {
    624             handler.onTerminate();
    625         }
    626 
    627         if (mCursor != null) {
    628             try {
    629                 mCursor.close();
    630             } catch (SQLiteException e) {
    631                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    632             }
    633             mCursor = null;
    634         }
    635 
    636         mTerminateIsCalled = true;
    637     }
    638 
    639     @Override
    640     public void finalize() {
    641         if (!mTerminateIsCalled) {
    642             Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step.");
    643             terminate();
    644         }
    645     }
    646 
    647     /**
    648      * @return returns the number of available entities. The return value is undefined
    649      * when this object is not ready yet (typically when {{@link #init()} is not called
    650      * or when {@link #terminate()} is already called).
    651      */
    652     public int getCount() {
    653         if (mCursor == null) {
    654             Log.w(LOG_TAG, "This object is not ready yet.");
    655             return 0;
    656         }
    657         return mCursor.getCount();
    658     }
    659 
    660     /**
    661      * @return true when there's no entity to be built. The return value is undefined
    662      * when this object is not ready yet.
    663      */
    664     public boolean isAfterLast() {
    665         if (mCursor == null) {
    666             Log.w(LOG_TAG, "This object is not ready yet.");
    667             return false;
    668         }
    669         return mCursor.isAfterLast();
    670     }
    671 
    672     /**
    673      * @return Returns the error reason.
    674      */
    675     public String getErrorReason() {
    676         return mErrorReason;
    677     }
    678 }
    679