VCardComposer.java revision 7e4e86eb5ad2c8a68ca7005ef4dee64a82ce0198
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package com.android.vcard;
17
18import com.android.vcard.exception.VCardException;
19
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Entity;
24import android.content.Entity.NamedContentValues;
25import android.content.EntityIterator;
26import android.database.Cursor;
27import android.database.sqlite.SQLiteException;
28import android.net.Uri;
29import android.provider.ContactsContract.CommonDataKinds.Email;
30import android.provider.ContactsContract.CommonDataKinds.Event;
31import android.provider.ContactsContract.CommonDataKinds.Im;
32import android.provider.ContactsContract.CommonDataKinds.Nickname;
33import android.provider.ContactsContract.CommonDataKinds.Note;
34import android.provider.ContactsContract.CommonDataKinds.Organization;
35import android.provider.ContactsContract.CommonDataKinds.Phone;
36import android.provider.ContactsContract.CommonDataKinds.Photo;
37import android.provider.ContactsContract.CommonDataKinds.Relation;
38import android.provider.ContactsContract.CommonDataKinds.SipAddress;
39import android.provider.ContactsContract.CommonDataKinds.StructuredName;
40import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
41import android.provider.ContactsContract.CommonDataKinds.Website;
42import android.provider.ContactsContract.Contacts;
43import android.provider.ContactsContract.Data;
44import android.provider.ContactsContract.RawContacts;
45import android.provider.ContactsContract.RawContactsEntity;
46import android.text.TextUtils;
47import android.util.Log;
48
49import java.io.BufferedWriter;
50import java.io.IOException;
51import java.io.OutputStream;
52import java.io.OutputStreamWriter;
53import java.io.UnsupportedEncodingException;
54import java.io.Writer;
55import java.lang.reflect.InvocationTargetException;
56import java.lang.reflect.Method;
57import java.util.ArrayList;
58import java.util.HashMap;
59import java.util.List;
60import java.util.Map;
61
62/**
63 * <p>
64 * The class for composing vCard from Contacts information.
65 * </p>
66 * <p>
67 * Usually, this class should be used like this.
68 * </p>
69 * <pre class="prettyprint">VCardComposer composer = null;
70 * try {
71 *     composer = new VCardComposer(context);
72 *     composer.addHandler(
73 *             composer.new HandlerForOutputStream(outputStream));
74 *     if (!composer.init()) {
75 *         // Do something handling the situation.
76 *         return;
77 *     }
78 *     while (!composer.isAfterLast()) {
79 *         if (mCanceled) {
80 *             // Assume a user may cancel this operation during the export.
81 *             return;
82 *         }
83 *         if (!composer.createOneEntry()) {
84 *             // Do something handling the error situation.
85 *             return;
86 *         }
87 *     }
88 * } finally {
89 *     if (composer != null) {
90 *         composer.terminate();
91 *     }
92 * }</pre>
93 * <p>
94 * Users have to manually take care of memory efficiency. Even one vCard may contain
95 * image of non-trivial size for mobile devices.
96 * </p>
97 * <p>
98 * {@link VCardBuilder} is used to build each vCard.
99 * </p>
100 */
101public class VCardComposer {
102    private static final String LOG_TAG = "VCardComposer";
103    private static final boolean DEBUG = false;
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    // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
121    // since usual vCard devices for Japanese devices already use it.
122    private static final String SHIFT_JIS = "SHIFT_JIS";
123    private static final String UTF_8 = "UTF-8";
124
125    private static final Map<Integer, String> sImMap;
126
127    static {
128        sImMap = new HashMap<Integer, String>();
129        sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
130        sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
131        sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
132        sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
133        sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
134        sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
135        // We don't add Google talk here since it has to be handled separately.
136    }
137
138    public static interface OneEntryHandler {
139        public boolean onInit(Context context);
140        public boolean onEntryCreated(String vcard);
141        public void onTerminate();
142    }
143
144    /**
145     * <p>
146     * An useful handler for emitting vCard String to an OutputStream object one by one.
147     * </p>
148     * <p>
149     * The input OutputStream object is closed() on {@link #onTerminate()}.
150     * Must not close the stream outside this class.
151     * </p>
152     */
153    public final class HandlerForOutputStream implements OneEntryHandler {
154        private final OutputStream mOutputStream; // mWriter will close this.
155        private Writer mWriter;
156
157        /**
158         * Input stream will be closed on the detruction of this object.
159         */
160        public HandlerForOutputStream(final OutputStream outputStream) {
161            mOutputStream = outputStream;
162        }
163
164        @Override
165        public boolean onInit(final Context context) {
166            try {
167                mWriter = new BufferedWriter(new OutputStreamWriter(
168                        mOutputStream, mCharset));
169            } catch (UnsupportedEncodingException e1) {
170                Log.e(LOG_TAG, "Unsupported charset: " + mCharset);
171                mErrorReason = "Encoding is not supported (usually this does not happen!): "
172                        + mCharset;
173                return false;
174            }
175
176            if (mIsDoCoMo) {
177                try {
178                    // Create one empty entry.
179                    mWriter.write(createOneEntryInternal("-1", null));
180                } catch (VCardException e) {
181                    Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
182                            e.getMessage());
183                    return false;
184                } catch (IOException e) {
185                    Log.e(LOG_TAG,
186                            "IOException occurred during exportOneContactData: "
187                                    + e.getMessage());
188                    mErrorReason = "IOException occurred: " + e.getMessage();
189                    return false;
190                }
191            }
192            return true;
193        }
194
195        @Override
196        public boolean onEntryCreated(String vcard) {
197            try {
198                mWriter.write(vcard);
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            return true;
207        }
208
209        @Override
210        public void onTerminate() {
211            if (mWriter != null) {
212                try {
213                    mWriter.close();
214                } catch (IOException e) {
215                    Log.w(LOG_TAG, "IOException is thrown during close(). Ignored.", e);
216                }
217            }
218        }
219    }
220
221    private final Context mContext;
222    private final int mVCardType;
223    private final boolean mCareHandlerErrors;
224    private final ContentResolver mContentResolver;
225
226    private final boolean mIsDoCoMo;
227    private Cursor mCursor;
228    private boolean mCursorSuppliedFromOutside;
229    private int mIdColumn;
230    private Uri mContentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI;
231
232    private final String mCharset;
233    private final List<OneEntryHandler> mHandlerList;
234
235    private boolean mInitDone;
236    private String mErrorReason = NO_ERROR;
237
238    /**
239     * Set to false when one of {@link #init()} variants is called, and set to true when
240     * {@link #terminate()} is called. Initially set to true.
241     */
242    private boolean mTerminateCalled = true;
243
244    private static final String[] sContactsProjection = new String[] {
245        Contacts._ID,
246    };
247
248    public VCardComposer(Context context) {
249        this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
250    }
251
252    /**
253     * The variant which sets charset to null and sets careHandlerErrors to true.
254     */
255    public VCardComposer(Context context, int vcardType) {
256        this(context, vcardType, null, true);
257    }
258
259    public VCardComposer(Context context, int vcardType, String charset) {
260        this(context, vcardType, charset, true);
261    }
262
263    /**
264     * The variant which sets charset to null.
265     */
266    public VCardComposer(final Context context, final int vcardType,
267            final boolean careHandlerErrors) {
268        this(context, vcardType, null, careHandlerErrors);
269    }
270
271    /**
272     * Constructs for supporting call log entry vCard composing.
273     *
274     * @param context Context to be used during the composition.
275     * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
276     * @param charset The charset to be used. Use null when you don't need the charset.
277     * @param careHandlerErrors If true, This object returns false everytime
278     * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false.
279     * If false, this ignores those errors.
280     */
281    public VCardComposer(final Context context, final int vcardType, String charset,
282            final boolean careHandlerErrors) {
283        this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
284    }
285
286    /**
287     * Just for testing for now.
288     * @param resolver {@link ContentResolver} which used by this object.
289     * @hide
290     */
291    public VCardComposer(final Context context, ContentResolver resolver,
292            final int vcardType, String charset, final boolean careHandlerErrors) {
293        mContext = context;
294        mVCardType = vcardType;
295        mCareHandlerErrors = careHandlerErrors;
296        mContentResolver = resolver;
297
298        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
299        mHandlerList = new ArrayList<OneEntryHandler>();
300
301        charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
302        final boolean shouldAppendCharsetParam = !(
303                VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
304
305        if (mIsDoCoMo || shouldAppendCharsetParam) {
306            // TODO: clean up once we're sure CharsetUtils are really unnecessary any more.
307            if (SHIFT_JIS.equalsIgnoreCase(charset)) {
308                /*if (mIsDoCoMo) {
309                    try {
310                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
311                    } catch (UnsupportedCharsetException e) {
312                        Log.e(LOG_TAG,
313                                "DoCoMo-specific SHIFT_JIS was not found. "
314                                + "Use SHIFT_JIS as is.");
315                        charset = SHIFT_JIS;
316                    }
317                } else {
318                    try {
319                        charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
320                    } catch (UnsupportedCharsetException e) {
321                        // Log.e(LOG_TAG,
322                        // "Career-specific SHIFT_JIS was not found. "
323                        // + "Use SHIFT_JIS as is.");
324                        charset = SHIFT_JIS;
325                    }
326                }*/
327                mCharset = charset;
328            } else {
329                /* Log.w(LOG_TAG,
330                        "The charset \"" + charset + "\" is used while "
331                        + SHIFT_JIS + " is needed to be used."); */
332                if (TextUtils.isEmpty(charset)) {
333                    mCharset = SHIFT_JIS;
334                } else {
335                    /*
336                    try {
337                        charset = CharsetUtils.charsetForVendor(charset).name();
338                    } catch (UnsupportedCharsetException e) {
339                        Log.i(LOG_TAG,
340                                "Career-specific \"" + charset + "\" was not found (as usual). "
341                                + "Use it as is.");
342                    }*/
343                    mCharset = charset;
344                }
345            }
346        } else {
347            if (TextUtils.isEmpty(charset)) {
348                mCharset = UTF_8;
349            } else {
350                /*try {
351                    charset = CharsetUtils.charsetForVendor(charset).name();
352                } catch (UnsupportedCharsetException e) {
353                    Log.i(LOG_TAG,
354                            "Career-specific \"" + charset + "\" was not found (as usual). "
355                            + "Use it as is.");
356                }*/
357                mCharset = charset;
358            }
359        }
360
361        Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
362    }
363
364    /**
365     * Must be called before {@link #init()}.
366     */
367    public void addHandler(OneEntryHandler handler) {
368        if (handler != null) {
369            mHandlerList.add(handler);
370        }
371    }
372
373    /**
374     * Initializes this object using default {@link Contacts#CONTENT_URI}.
375     *
376     * You can call this method or a variant of this method just once. In other words, you cannot
377     * reuse this object.
378     *
379     * @return Returns true when initialization is successful and all the other
380     *          methods are available. Returns false otherwise.
381     */
382    public boolean init() {
383        return init(null, null);
384    }
385
386    /**
387     * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
388     * {@link ContentResolver} with {@link Contacts#_ID}.
389     * <code>
390     * String selection = Data.CONTACT_ID + "=?";
391     * String[] selectionArgs = new String[] {contactId};
392     * Cursor cursor = mContentResolver.query(
393     *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
394     * </code>
395     *
396     * You can call this method or a variant of this method just once. In other words, you cannot
397     * reuse this object.
398     */
399    public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
400        mContentUriForRawContactsEntity = contentUriForRawContactsEntity;
401        return init(null, null);
402    }
403
404    /**
405     * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
406     * arguments.
407     */
408    public boolean init(final String selection, final String[] selectionArgs) {
409        return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs, null);
410    }
411
412    /**
413     * Note that this is unstable interface, may be deleted in the future.
414     */
415    public boolean init(final Uri contentUri, final String selection,
416            final String[] selectionArgs, final String sortOrder) {
417        return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder);
418    }
419
420    private boolean init(final Uri contentUri, final String[] projection,
421            final String selection, final String[] selectionArgs, final String sortOrder) {
422        if (!Contacts.CONTENT_URI.equals(contentUri)) {
423            if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
424            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
425            return false;
426        }
427        if (!initInterFirstPart()) {
428            return false;
429        }
430        if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
431                sortOrder)) {
432            return false;
433        }
434        if (!initInterMainPart()) {
435            return false;
436        }
437        return initInterLastPart();
438    }
439
440    /**
441     * Just for testing for now. Do not use.
442     * @hide
443     */
444    public boolean init(Cursor cursor) {
445        if (!initInterFirstPart()) {
446            return false;
447        }
448        mCursorSuppliedFromOutside = true;
449        mCursor = cursor;
450        if (!initInterMainPart()) {
451            return false;
452        }
453        return initInterLastPart();
454    }
455
456    private boolean initInterFirstPart() {
457        if (mInitDone) {
458            Log.e(LOG_TAG, "init() is already called");
459            return false;
460        }
461
462        if (mCareHandlerErrors) {
463            final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
464                    mHandlerList.size());
465            for (OneEntryHandler handler : mHandlerList) {
466                if (!handler.onInit(mContext)) {
467                    if (DEBUG) {
468                        Log.d(LOG_TAG,
469                                String.format("One of OneEntryHandler (%s) return false on init.",
470                                        handler.toString()));
471                    }
472                    for (OneEntryHandler finished : finishedList) {
473                        finished.onTerminate();
474                    }
475                    return false;
476                }
477            }
478        } else {
479            // Just ignore the false returned from onInit().
480            for (OneEntryHandler handler : mHandlerList) {
481                handler.onInit(mContext);
482            }
483        }
484
485        return true;
486    }
487
488    private boolean initInterCursorCreationPart(
489            final Uri contentUri, final String[] projection,
490            final String selection, final String[] selectionArgs, final String sortOrder) {
491        mCursorSuppliedFromOutside = false;
492        mCursor = mContentResolver.query(
493                contentUri, projection, selection, selectionArgs, sortOrder);
494
495        if (mCursor == null) {
496            Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
497            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
498            return false;
499        }
500        return true;
501    }
502
503    private boolean initInterMainPart() {
504        if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
505            if (DEBUG) {
506                Log.d(LOG_TAG,
507                    String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
508            }
509            closeCursorIfAppropriate();
510            return false;
511        }
512        mIdColumn = mCursor.getColumnIndex(Contacts._ID);
513        return true;
514    }
515
516    private boolean initInterLastPart() {
517        mInitDone = true;
518        mTerminateCalled = false;
519        return true;
520    }
521
522    public boolean createOneEntry() {
523        return createOneEntry(null);
524    }
525
526    /**
527     * @param getEntityIteratorMethod For Dependency Injection.
528     * @hide just for testing.
529     */
530    public boolean createOneEntry(Method getEntityIteratorMethod) {
531        if (!mInitDone) {
532            mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
533            return false;
534        }
535        final String vcard;
536        try {
537            if (mIdColumn >= 0) {
538                vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
539                        getEntityIteratorMethod);
540            } else {
541                Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
542                return true;
543            }
544        } catch (VCardException e) {
545            Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
546            return false;
547        } catch (OutOfMemoryError error) {
548            // Maybe some data (e.g. photo) is too big to have in memory. But it
549            // should be rare.
550            Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
551            System.gc();
552            // TODO: should tell users what happened?
553            return true;
554        } finally {
555            mCursor.moveToNext();
556        }
557
558        // This function does not care the OutOfMemoryError on the handler side :-P
559        if (mCareHandlerErrors) {
560            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
561                    mHandlerList.size());
562            for (OneEntryHandler handler : mHandlerList) {
563                if (!handler.onEntryCreated(vcard)) {
564                    return false;
565                }
566            }
567        } else {
568            for (OneEntryHandler handler : mHandlerList) {
569                handler.onEntryCreated(vcard);
570            }
571        }
572
573        return true;
574    }
575
576    private String createOneEntryInternal(final String contactId,
577            final Method getEntityIteratorMethod) throws VCardException {
578        final Map<String, List<ContentValues>> contentValuesListMap =
579                new HashMap<String, List<ContentValues>>();
580        // The resolver may return the entity iterator with no data. It is possible.
581        // e.g. If all the data in the contact of the given contact id are not exportable ones,
582        //      they are hidden from the view of this method, though contact id itself exists.
583        EntityIterator entityIterator = null;
584        try {
585            final Uri uri = mContentUriForRawContactsEntity;
586            final String selection = Data.CONTACT_ID + "=?";
587            final String[] selectionArgs = new String[] {contactId};
588            if (getEntityIteratorMethod != null) {
589                // Please note that this branch is executed by unit tests only
590                try {
591                    entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
592                            mContentResolver, uri, selection, selectionArgs, null);
593                } catch (IllegalArgumentException e) {
594                    Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
595                            e.getMessage());
596                } catch (IllegalAccessException e) {
597                    Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
598                            e.getMessage());
599                } catch (InvocationTargetException e) {
600                    Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
601                    StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
602                    for (StackTraceElement element : stackTraceElements) {
603                        Log.e(LOG_TAG, "    at " + element.toString());
604                    }
605                    throw new VCardException("InvocationTargetException has been thrown: " +
606                            e.getCause().getMessage());
607                }
608            } else {
609                entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
610                        uri, null, selection, selectionArgs, null));
611            }
612
613            if (entityIterator == null) {
614                Log.e(LOG_TAG, "EntityIterator is null");
615                return "";
616            }
617
618            if (!entityIterator.hasNext()) {
619                Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
620                return "";
621            }
622
623            while (entityIterator.hasNext()) {
624                Entity entity = entityIterator.next();
625                for (NamedContentValues namedContentValues : entity.getSubValues()) {
626                    ContentValues contentValues = namedContentValues.values;
627                    String key = contentValues.getAsString(Data.MIMETYPE);
628                    if (key != null) {
629                        List<ContentValues> contentValuesList =
630                                contentValuesListMap.get(key);
631                        if (contentValuesList == null) {
632                            contentValuesList = new ArrayList<ContentValues>();
633                            contentValuesListMap.put(key, contentValuesList);
634                        }
635                        contentValuesList.add(contentValues);
636                    }
637                }
638            }
639        } finally {
640            if (entityIterator != null) {
641                entityIterator.close();
642            }
643        }
644
645        return buildVCard(contentValuesListMap);
646    }
647
648    /**
649     * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
650     * {ContactsContract}. Developers can override this method to customize the output.
651     */
652    public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
653        if (contentValuesListMap == null) {
654            Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
655            return "";
656        } else {
657            final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
658            builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
659                    .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
660                    .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
661                    .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
662                    .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
663                    .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
664                    .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
665            if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
666                builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
667            }
668            builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
669                    .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
670                    .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
671                    .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
672                    .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
673            return builder.toString();
674        }
675    }
676
677    public void terminate() {
678        for (OneEntryHandler handler : mHandlerList) {
679            handler.onTerminate();
680        }
681
682        closeCursorIfAppropriate();
683        mTerminateCalled = true;
684    }
685
686    private void closeCursorIfAppropriate() {
687        if (!mCursorSuppliedFromOutside && mCursor != null) {
688            try {
689                mCursor.close();
690            } catch (SQLiteException e) {
691                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
692            }
693            mCursor = null;
694        }
695    }
696
697    @Override
698    protected void finalize() throws Throwable {
699        try {
700            if (!mTerminateCalled) {
701                Log.e(LOG_TAG, "finalized() is called before terminate() being called");
702            }
703        } finally {
704            super.finalize();
705        }
706    }
707
708    /**
709     * @return returns the number of available entities. The return value is undefined
710     * when this object is not ready yet (typically when {{@link #init()} is not called
711     * or when {@link #terminate()} is already called).
712     */
713    public int getCount() {
714        if (mCursor == null) {
715            Log.w(LOG_TAG, "This object is not ready yet.");
716            return 0;
717        }
718        return mCursor.getCount();
719    }
720
721    /**
722     * @return true when there's no entity to be built. The return value is undefined
723     * when this object is not ready yet.
724     */
725    public boolean isAfterLast() {
726        if (mCursor == null) {
727            Log.w(LOG_TAG, "This object is not ready yet.");
728            return false;
729        }
730        return mCursor.isAfterLast();
731    }
732
733    /**
734     * @return Returns the error reason.
735     */
736    public String getErrorReason() {
737        return mErrorReason;
738    }
739}
740