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>();
sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM)122         sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN)123         sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO)124         sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ)125         sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER)126         sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME)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 RawContactEntitlesInfoCallback mRawContactEntitlesInfoCallback;
158 
159     private static final String[] sContactsProjection = new String[] {
160         Contacts._ID,
161     };
162 
VCardComposer(Context context)163     public VCardComposer(Context context) {
164         this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
165     }
166 
167     /**
168      * The variant which sets charset to null and sets careHandlerErrors to true.
169      */
VCardComposer(Context context, int vcardType)170     public VCardComposer(Context context, int vcardType) {
171         this(context, vcardType, null, true);
172     }
173 
VCardComposer(Context context, int vcardType, String charset)174     public VCardComposer(Context context, int vcardType, String charset) {
175         this(context, vcardType, charset, true);
176     }
177 
178     /**
179      * The variant which sets charset to null.
180      */
VCardComposer(final Context context, final int vcardType, final boolean careHandlerErrors)181     public VCardComposer(final Context context, final int vcardType,
182             final boolean careHandlerErrors) {
183         this(context, vcardType, null, careHandlerErrors);
184     }
185 
186     /**
187      * Constructs for supporting call log entry vCard composing.
188      *
189      * @param context Context to be used during the composition.
190      * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
191      * @param charset The charset to be used. Use null when you don't need the charset.
192      * @param careHandlerErrors If true, This object returns false everytime
193      */
VCardComposer(final Context context, final int vcardType, String charset, final boolean careHandlerErrors)194     public VCardComposer(final Context context, final int vcardType, String charset,
195             final boolean careHandlerErrors) {
196         this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
197     }
198 
199     /**
200      * Just for testing for now.
201      * @param resolver {@link ContentResolver} which used by this object.
202      * @hide
203      */
VCardComposer(final Context context, ContentResolver resolver, final int vcardType, String charset, final boolean careHandlerErrors)204     public VCardComposer(final Context context, ContentResolver resolver,
205             final int vcardType, String charset, final boolean careHandlerErrors) {
206         // Not used right now
207         // mContext = context;
208         mVCardType = vcardType;
209         mContentResolver = resolver;
210 
211         mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
212 
213         charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
214         final boolean shouldAppendCharsetParam = !(
215                 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
216 
217         if (mIsDoCoMo || shouldAppendCharsetParam) {
218             if (SHIFT_JIS.equalsIgnoreCase(charset)) {
219                 mCharset = charset;
220             } else {
221                 /* Log.w(LOG_TAG,
222                         "The charset \"" + charset + "\" is used while "
223                         + SHIFT_JIS + " is needed to be used."); */
224                 if (TextUtils.isEmpty(charset)) {
225                     mCharset = SHIFT_JIS;
226                 } else {
227                     mCharset = charset;
228                 }
229             }
230         } else {
231             if (TextUtils.isEmpty(charset)) {
232                 mCharset = UTF_8;
233             } else {
234                 mCharset = charset;
235             }
236         }
237 
238         Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
239     }
240 
241     /**
242      * Initializes this object using default {@link Contacts#CONTENT_URI}.
243      *
244      * You can call this method or a variant of this method just once. In other words, you cannot
245      * reuse this object.
246      *
247      * @return Returns true when initialization is successful and all the other
248      *          methods are available. Returns false otherwise.
249      */
init()250     public boolean init() {
251         return init(null, null);
252     }
253 
254     /**
255      * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
256      * {@link ContentResolver} with {@link Contacts#_ID}.
257      * <code>
258      * String selection = Data.CONTACT_ID + "=?";
259      * String[] selectionArgs = new String[] {contactId};
260      * Cursor cursor = mContentResolver.query(
261      *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
262      * </code>
263      *
264      * You can call this method or a variant of this method just once. In other words, you cannot
265      * reuse this object.
266      *
267      * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
268      * need to change the default Uri.
269      */
270     @Deprecated
initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity)271     public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
272         return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
273                 contentUriForRawContactsEntity);
274     }
275 
276     /**
277      * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
278      * arguments.
279      */
init(final String selection, final String[] selectionArgs)280     public boolean init(final String selection, final String[] selectionArgs) {
281         return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
282                 null, null);
283     }
284 
285     /**
286      * Note that this is unstable interface, may be deleted in the future.
287      */
init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)288     public boolean init(final Uri contentUri, final String selection,
289             final String[] selectionArgs, final String sortOrder) {
290         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
291     }
292 
293     /**
294      * @param contentUri Uri for obtaining the list of contactId. Used with
295      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
296      * @param selection selection used with
297      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
298      * @param selectionArgs selectionArgs used with
299      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
300      * @param sortOrder sortOrder used with
301      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
302      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
303      * contactId.
304      * Note that this is an unstable interface, may be deleted in the future.
305      */
init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder, final Uri contentUriForRawContactsEntity)306     public boolean init(final Uri contentUri, final String selection,
307             final String[] selectionArgs, final String sortOrder,
308             final Uri contentUriForRawContactsEntity) {
309         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
310                 contentUriForRawContactsEntity);
311     }
312 
313     /**
314      * A variant of init(). Currently just for testing. Use other variants for init().
315      *
316      * First we'll create {@link Cursor} for the list of contactId.
317      *
318      * <code>
319      * Cursor cursorForId = mContentResolver.query(
320      *         contentUri, projection, selection, selectionArgs, sortOrder);
321      * </code>
322      *
323      * After that, we'll obtain data for each contactId in the list.
324      *
325      * <code>
326      * Cursor cursorForContent = mContentResolver.query(
327      *         contentUriForRawContactsEntity, null,
328      *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
329      * </code>
330      *
331      * {@link #createOneEntry()} or its variants let the caller obtain each entry from
332      * <code>cursorForContent</code> above.
333      *
334      * @param contentUri Uri for obtaining the list of contactId. Used with
335      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
336      * @param projection projection used with
337      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
338      * @param selection selection used with
339      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
340      * @param selectionArgs selectionArgs used with
341      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
342      * @param sortOrder sortOrder used with
343      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
344      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
345      * contactId.
346      * @return true when successful
347      *
348      * @hide
349      */
init(final Uri contentUri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder, Uri contentUriForRawContactsEntity)350     public boolean init(final Uri contentUri, final String[] projection,
351             final String selection, final String[] selectionArgs,
352             final String sortOrder, Uri contentUriForRawContactsEntity) {
353         if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
354             if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
355             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
356             return false;
357         }
358 
359         if (!initInterFirstPart(contentUriForRawContactsEntity)) {
360             return false;
361         }
362         if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
363                 sortOrder)) {
364             return false;
365         }
366         if (!initInterMainPart()) {
367             return false;
368         }
369         return initInterLastPart();
370     }
371 
372     /**
373      * Just for testing for now. Do not use.
374      * @hide
375      */
init(Cursor cursor)376     public boolean init(Cursor cursor) {
377         return initWithCallback(cursor, null);
378     }
379 
380     /**
381     * @param cursor Cursor that used to get contact id
382     * @param rawContactEntitlesInfoCallback Callback that return RawContactEntitlesInfo
383     * Note that this is an unstable interface, may be deleted in the future.
384     *
385     * @return true when successful
386     */
initWithCallback(Cursor cursor, RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback)387     public boolean initWithCallback(Cursor cursor,
388             RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback) {
389         if (!initInterFirstPart(null)) {
390             return false;
391         }
392         mCursorSuppliedFromOutside = true;
393         mCursor = cursor;
394         mRawContactEntitlesInfoCallback = rawContactEntitlesInfoCallback;
395         if (!initInterMainPart()) {
396             return false;
397         }
398         return initInterLastPart();
399     }
400 
initInterFirstPart(Uri contentUriForRawContactsEntity)401     private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
402         mContentUriForRawContactsEntity =
403                 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
404                         RawContactsEntity.CONTENT_URI);
405         if (mInitDone) {
406             Log.e(LOG_TAG, "init() is already called");
407             return false;
408         }
409         return true;
410     }
411 
initInterCursorCreationPart( final Uri contentUri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder)412     private boolean initInterCursorCreationPart(
413             final Uri contentUri, final String[] projection,
414             final String selection, final String[] selectionArgs, final String sortOrder) {
415         mCursorSuppliedFromOutside = false;
416         mCursor = mContentResolver.query(
417                 contentUri, projection, selection, selectionArgs, sortOrder);
418         if (mCursor == null) {
419             Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
420             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
421             return false;
422         }
423         return true;
424     }
425 
initInterMainPart()426     private boolean initInterMainPart() {
427         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
428             if (DEBUG) {
429                 Log.d(LOG_TAG,
430                     String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
431             }
432             closeCursorIfAppropriate();
433             return false;
434         }
435         mIdColumn = mCursor.getColumnIndex(Data.CONTACT_ID);
436         if (mIdColumn < 0) {
437             mIdColumn = mCursor.getColumnIndex(Contacts._ID);
438         }
439         return mIdColumn >= 0;
440     }
441 
initInterLastPart()442     private boolean initInterLastPart() {
443         mInitDone = true;
444         mTerminateCalled = false;
445         return true;
446     }
447 
448     /**
449      * @return a vCard string.
450      */
createOneEntry()451     public String createOneEntry() {
452         return createOneEntry(null);
453     }
454 
455     /**
456      * @hide
457      */
createOneEntry(Method getEntityIteratorMethod)458     public String createOneEntry(Method getEntityIteratorMethod) {
459         if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
460             mFirstVCardEmittedInDoCoMoCase = true;
461             // Previously we needed to emit empty data for this specific case, but actually
462             // this doesn't work now, as resolver doesn't return any data with "-1" contactId.
463             // TODO: re-introduce or remove this logic. Needs to modify unit test when we
464             // re-introduce the logic.
465             // return createOneEntryInternal("-1", getEntityIteratorMethod);
466         }
467 
468         final String vcard = createOneEntryInternal(mCursor.getLong(mIdColumn),
469                 getEntityIteratorMethod);
470         if (!mCursor.moveToNext()) {
471             Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
472         }
473         return vcard;
474     }
475 
476     /**
477      *  Class that store rawContactEntitlesUri and contactId
478      */
479     public static class RawContactEntitlesInfo {
480         public final Uri rawContactEntitlesUri;
481         public final long contactId;
RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId)482         public RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId) {
483             this.rawContactEntitlesUri = rawContactEntitlesUri;
484             this.contactId = contactId;
485         }
486     }
487 
488     /**
489     * Listener for getting raw contact entitles info
490     */
491     public interface RawContactEntitlesInfoCallback {
492         /**
493         * Callback to get RawContactEntitlesInfo from contact id
494         *
495         * @param contactId Contact id that you want to process.
496         * @return RawContactEntitlesInfo that ready to process.
497         */
getRawContactEntitlesInfo(long contactId)498         RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId);
499     }
500 
createOneEntryInternal(long contactId, final Method getEntityIteratorMethod)501     private String createOneEntryInternal(long contactId,
502             final Method getEntityIteratorMethod) {
503         final Map<String, List<ContentValues>> contentValuesListMap =
504                 new HashMap<String, List<ContentValues>>();
505         // The resolver may return the entity iterator with no data. It is possible.
506         // e.g. If all the data in the contact of the given contact id are not exportable ones,
507         //      they are hidden from the view of this method, though contact id itself exists.
508         EntityIterator entityIterator = null;
509         try {
510             Uri uri = mContentUriForRawContactsEntity;
511             if (mRawContactEntitlesInfoCallback != null) {
512                 RawContactEntitlesInfo rawContactEntitlesInfo =
513                         mRawContactEntitlesInfoCallback.getRawContactEntitlesInfo(contactId);
514                 uri = rawContactEntitlesInfo.rawContactEntitlesUri;
515                 contactId = rawContactEntitlesInfo.contactId;
516             }
517             final String selection = Data.CONTACT_ID + "=?";
518             final String[] selectionArgs = new String[] {String.valueOf(contactId)};
519             if (getEntityIteratorMethod != null) {
520                 // Please note that this branch is executed by unit tests only
521                 try {
522                     entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
523                             mContentResolver, uri, selection, selectionArgs, null);
524                 } catch (IllegalArgumentException e) {
525                     Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
526                             e.getMessage());
527                 } catch (IllegalAccessException e) {
528                     Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
529                             e.getMessage());
530                 } catch (InvocationTargetException e) {
531                     Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
532                     throw new RuntimeException("InvocationTargetException has been thrown");
533                 }
534             } else {
535                 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
536                         uri, null, selection, selectionArgs, null));
537             }
538 
539             if (entityIterator == null) {
540                 Log.e(LOG_TAG, "EntityIterator is null");
541                 return "";
542             }
543 
544             if (!entityIterator.hasNext()) {
545                 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
546                 return "";
547             }
548 
549             while (entityIterator.hasNext()) {
550                 Entity entity = entityIterator.next();
551                 for (NamedContentValues namedContentValues : entity.getSubValues()) {
552                     ContentValues contentValues = namedContentValues.values;
553                     String key = contentValues.getAsString(Data.MIMETYPE);
554                     if (key != null) {
555                         List<ContentValues> contentValuesList =
556                                 contentValuesListMap.get(key);
557                         if (contentValuesList == null) {
558                             contentValuesList = new ArrayList<ContentValues>();
559                             contentValuesListMap.put(key, contentValuesList);
560                         }
561                         contentValuesList.add(contentValues);
562                     }
563                 }
564             }
565         } finally {
566             if (entityIterator != null) {
567                 entityIterator.close();
568             }
569         }
570 
571         return buildVCard(contentValuesListMap);
572     }
573 
574     private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
575     /**
576      * <p>
577      * Set a callback for phone number formatting. It will be called every time when this object
578      * receives a phone number for printing.
579      * </p>
580      * <p>
581      * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
582      * and the callback should be responsible for everything about phone number formatting.
583      * </p>
584      * <p>
585      * Caution: This interface will change. Please don't use without any strong reason.
586      * </p>
587      */
setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback)588     public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
589         mPhoneTranslationCallback = callback;
590     }
591 
592     /**
593      * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
594      * {ContactsContract}. Developers can override this method to customize the output.
595      */
buildVCard(final Map<String, List<ContentValues>> contentValuesListMap)596     public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
597         if (contentValuesListMap == null) {
598             Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
599             return "";
600         } else {
601             final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
602             builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
603                     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
604                     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
605                             mPhoneTranslationCallback)
606                     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
607                     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
608                     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
609                     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
610             if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
611                 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
612             }
613             builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
614                     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
615                     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
616                     .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
617                     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
618             return builder.toString();
619         }
620     }
621 
terminate()622     public void terminate() {
623         closeCursorIfAppropriate();
624         mTerminateCalled = true;
625     }
626 
closeCursorIfAppropriate()627     private void closeCursorIfAppropriate() {
628         if (!mCursorSuppliedFromOutside && mCursor != null) {
629             try {
630                 mCursor.close();
631             } catch (SQLiteException e) {
632                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
633             }
634             mCursor = null;
635         }
636     }
637 
638     @Override
finalize()639     protected void finalize() throws Throwable {
640         try {
641             if (!mTerminateCalled) {
642                 Log.e(LOG_TAG, "finalized() is called before terminate() being called");
643             }
644         } finally {
645             super.finalize();
646         }
647     }
648 
649     /**
650      * @return returns the number of available entities. The return value is undefined
651      * when this object is not ready yet (typically when {{@link #init()} is not called
652      * or when {@link #terminate()} is already called).
653      */
getCount()654     public int getCount() {
655         if (mCursor == null) {
656             Log.w(LOG_TAG, "This object is not ready yet.");
657             return 0;
658         }
659         return mCursor.getCount();
660     }
661 
662     /**
663      * @return true when there's no entity to be built. The return value is undefined
664      * when this object is not ready yet.
665      */
isAfterLast()666     public boolean isAfterLast() {
667         if (mCursor == null) {
668             Log.w(LOG_TAG, "This object is not ready yet.");
669             return false;
670         }
671         return mCursor.isAfterLast();
672     }
673 
674     /**
675      * @return Returns the error reason.
676      */
getErrorReason()677     public String getErrorReason() {
678         return mErrorReason;
679     }
680 }
681