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