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