2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
16 package android.pim.vcard;
18 import android.content.ContentResolver;
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.content.Entity;
22 import android.content.Entity.NamedContentValues;
23 import android.content.EntityIterator;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteException;
26 import android.net.Uri;
27 import android.pim.vcard.exception.VCardException;
28 import android.provider.ContactsContract.CommonDataKinds.Email;
29 import android.provider.ContactsContract.CommonDataKinds.Event;
30 import android.provider.ContactsContract.CommonDataKinds.Im;
31 import android.provider.ContactsContract.CommonDataKinds.Nickname;
32 import android.provider.ContactsContract.CommonDataKinds.Note;
33 import android.provider.ContactsContract.CommonDataKinds.Organization;
34 import android.provider.ContactsContract.CommonDataKinds.Phone;
35 import android.provider.ContactsContract.CommonDataKinds.Photo;
36 import android.provider.ContactsContract.CommonDataKinds.Relation;
37 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
38 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
39 import android.provider.ContactsContract.CommonDataKinds.Website;
40 import android.provider.ContactsContract.Contacts;
41 import android.provider.ContactsContract.Data;
42 import android.provider.ContactsContract.RawContacts;
43 import android.provider.ContactsContract.RawContactsEntity;
44 import android.text.TextUtils;
45 import android.util.CharsetUtils;
46 import android.util.Log;
48 import java.io.BufferedWriter;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.OutputStream;
52 import java.io.OutputStreamWriter;
53 import java.io.UnsupportedEncodingException;
54 import java.io.Writer;
55 import java.lang.reflect.InvocationTargetException;
56 import java.lang.reflect.Method;
57 import java.nio.charset.UnsupportedCharsetException;
58 import java.util.ArrayList;
59 import java.util.HashMap;
60 import java.util.List;
65 * The class for composing vCard from Contacts information.
68 * Usually, this class should be used like this.
70 * <pre class="prettyprint">VCardComposer composer = null;
72 * composer = new VCardComposer(context);
73 * composer.addHandler(
74 * composer.new HandlerForOutputStream(outputStream));
75 * if (!composer.init()) {
76 * // Do something handling the situation.
79 * while (!composer.isAfterLast()) {
81 * // Assume a user may cancel this operation during the export.
84 * if (!composer.createOneEntry()) {
85 * // Do something handling the error situation.
90 * if (composer != null) {
91 * composer.terminate();
95 * Users have to manually take care of memory efficiency. Even one vCard may contain
96 * image of non-trivial size for mobile devices.
99 * {@link VCardBuilder} is used to build each vCard.
102 public class VCardComposer {
103 private static final String LOG_TAG = "VCardComposer";
105 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
106 "Failed to get database information";
108 public static final String FAILURE_REASON_NO_ENTRY =
109 "There's no exportable in the database";
111 public static final String FAILURE_REASON_NOT_INITIALIZED =
112 "The vCard composer object is not correctly initialized";
114 /** Should be visible only from developers... (no need to translate, hopefully) */
115 public static final String FAILURE_REASON_UNSUPPORTED_URI =
116 "The Uri vCard composer received is not supported by the composer.";
118 public static final String NO_ERROR = "No error";
120 public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
122 // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
123 // since usual vCard devices for Japanese devices already use it.
124 private static final String SHIFT_JIS = "SHIFT_JIS";
125 private static final String UTF_8 = "UTF-8";
128 * Special URI for testing.
130 public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
131 public static final Uri VCARD_TEST_AUTHORITY_URI =
132 Uri.parse("content://" + VCARD_TEST_AUTHORITY);
133 public static final Uri CONTACTS_TEST_CONTENT_URI =
134 Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
136 private static final Map<Integer, String> sImMap;
139 sImMap = new HashMap<Integer, String>();
140 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
141 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
142 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
143 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
144 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
145 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
146 // We don't add Google talk here since it has to be handled separately.
149 public static interface OneEntryHandler {
150 public boolean onInit(Context context);
151 public boolean onEntryCreated(String vcard);
152 public void onTerminate();
157 * An useful handler for emitting vCard String to an OutputStream object one by one.
160 * The input OutputStream object is closed() on {@link #onTerminate()}.
161 * Must not close the stream outside this class.
164 public final class HandlerForOutputStream implements OneEntryHandler {
165 @SuppressWarnings("hiding")
166 private static final String LOG_TAG = "VCardComposer.HandlerForOutputStream";
168 private boolean mOnTerminateIsCalled = false;
170 private final OutputStream mOutputStream; // mWriter will close this.
171 private Writer mWriter;
174 * Input stream will be closed on the detruction of this object.
176 public HandlerForOutputStream(final OutputStream outputStream) {
177 mOutputStream = outputStream;
180 public boolean onInit(final Context context) {
182 mWriter = new BufferedWriter(new OutputStreamWriter(
183 mOutputStream, mCharset));
184 } catch (UnsupportedEncodingException e1) {
185 Log.e(LOG_TAG, "Unsupported charset: " + mCharset);
186 mErrorReason = "Encoding is not supported (usually this does not happen!): "
193 // Create one empty entry.
194 mWriter.write(createOneEntryInternal("-1", null));
195 } catch (VCardException e) {
196 Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
199 } catch (IOException e) {
201 "IOException occurred during exportOneContactData: "
203 mErrorReason = "IOException occurred: " + e.getMessage();
210 public boolean onEntryCreated(String vcard) {
212 mWriter.write(vcard);
213 } catch (IOException e) {
215 "IOException occurred during exportOneContactData: "
217 mErrorReason = "IOException occurred: " + e.getMessage();
223 public void onTerminate() {
224 mOnTerminateIsCalled = true;
225 if (mWriter != null) {
227 // Flush and sync the data so that a user is able to pull
228 // the SDCard just after
231 if (mOutputStream != null
232 && mOutputStream instanceof FileOutputStream) {
233 ((FileOutputStream) mOutputStream).getFD().sync();
235 } catch (IOException e) {
237 "IOException during closing the output stream: "
245 public void closeOutputStream() {
248 } catch (IOException e) {
249 Log.w(LOG_TAG, "IOException is thrown during close(). Ignoring.");
254 public void finalize() {
255 if (!mOnTerminateIsCalled) {
261 private final Context mContext;
262 private final int mVCardType;
263 private final boolean mCareHandlerErrors;
264 private final ContentResolver mContentResolver;
266 private final boolean mIsDoCoMo;
267 private Cursor mCursor;
268 private int mIdColumn;
270 private final String mCharset;
271 private boolean mTerminateIsCalled;
272 private final List<OneEntryHandler> mHandlerList;
274 private String mErrorReason = NO_ERROR;
276 private static final String[] sContactsProjection = new String[] {
280 public VCardComposer(Context context) {
281 this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
285 * The variant which sets charset to null and sets careHandlerErrors to true.
287 public VCardComposer(Context context, int vcardType) {
288 this(context, vcardType, null, true);
291 public VCardComposer(Context context, int vcardType, String charset) {
292 this(context, vcardType, charset, true);
296 * The variant which sets charset to null.
298 public VCardComposer(final Context context, final int vcardType,
299 final boolean careHandlerErrors) {
300 this(context, vcardType, null, careHandlerErrors);
304 * Construct for supporting call log entry vCard composing.
306 * @param context Context to be used during the composition.
307 * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
308 * @param charset The charset to be used. Use null when you don't need the charset.
309 * @param careHandlerErrors If true, This object returns false everytime
310 * a Handler object given via {{@link #addHandler(OneEntryHandler)} returns false.
311 * If false, this ignores those errors.
313 public VCardComposer(final Context context, final int vcardType, String charset,
314 final boolean careHandlerErrors) {
316 mVCardType = vcardType;
317 mCareHandlerErrors = careHandlerErrors;
318 mContentResolver = context.getContentResolver();
320 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
321 mHandlerList = new ArrayList<OneEntryHandler>();
323 charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
324 final boolean shouldAppendCharsetParam = !(
325 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
327 if (mIsDoCoMo || shouldAppendCharsetParam) {
328 if (SHIFT_JIS.equalsIgnoreCase(charset)) {
331 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
332 } catch (UnsupportedCharsetException e) {
334 "DoCoMo-specific SHIFT_JIS was not found. "
335 + "Use SHIFT_JIS as is.");
340 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
341 } catch (UnsupportedCharsetException e) {
343 "Career-specific SHIFT_JIS was not found. "
344 + "Use SHIFT_JIS as is.");
351 "The charset \"" + charset + "\" is used while "
352 + SHIFT_JIS + " is needed to be used.");
353 if (TextUtils.isEmpty(charset)) {
354 mCharset = SHIFT_JIS;
357 charset = CharsetUtils.charsetForVendor(charset).name();
358 } catch (UnsupportedCharsetException e) {
360 "Career-specific \"" + charset + "\" was not found (as usual). "
367 if (TextUtils.isEmpty(charset)) {
371 charset = CharsetUtils.charsetForVendor(charset).name();
372 } catch (UnsupportedCharsetException e) {
374 "Career-specific \"" + charset + "\" was not found (as usual). "
381 Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
385 * Must be called before {@link #init()}.
387 public void addHandler(OneEntryHandler handler) {
388 if (handler != null) {
389 mHandlerList.add(handler);
394 * @return Returns true when initialization is successful and all the other
395 * methods are available. Returns false otherwise.
397 public boolean init() {
398 return init(null, null);
401 public boolean init(final String selection, final String[] selectionArgs) {
402 return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
406 * Note that this is unstable interface, may be deleted in the future.
408 public boolean init(final Uri contentUri, final String selection,
409 final String[] selectionArgs, final String sortOrder) {
410 if (contentUri == null) {
414 if (mCareHandlerErrors) {
415 final List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
416 mHandlerList.size());
417 for (OneEntryHandler handler : mHandlerList) {
418 if (!handler.onInit(mContext)) {
419 for (OneEntryHandler finished : finishedList) {
420 finished.onTerminate();
426 // Just ignore the false returned from onInit().
427 for (OneEntryHandler handler : mHandlerList) {
428 handler.onInit(mContext);
432 final String[] projection;
433 if (Contacts.CONTENT_URI.equals(contentUri) ||
434 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
435 projection = sContactsProjection;
437 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
440 mCursor = mContentResolver.query(
441 contentUri, projection, selection, selectionArgs, sortOrder);
443 if (mCursor == null) {
444 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
448 if (getCount() == 0 || !mCursor.moveToFirst()) {
451 } catch (SQLiteException e) {
452 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
455 mErrorReason = FAILURE_REASON_NO_ENTRY;
460 mIdColumn = mCursor.getColumnIndex(Contacts._ID);
465 public boolean createOneEntry() {
466 return createOneEntry(null);
470 * @param getEntityIteratorMethod For Dependency Injection.
471 * @hide just for testing.
473 public boolean createOneEntry(Method getEntityIteratorMethod) {
474 if (mCursor == null || mCursor.isAfterLast()) {
475 mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
480 if (mIdColumn >= 0) {
481 vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
482 getEntityIteratorMethod);
484 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
487 } catch (VCardException e) {
488 Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
490 } catch (OutOfMemoryError error) {
491 // Maybe some data (e.g. photo) is too big to have in memory. But it
493 Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
495 // TODO: should tell users what happened?
498 mCursor.moveToNext();
501 // This function does not care the OutOfMemoryError on the handler side :-P
502 if (mCareHandlerErrors) {
503 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
504 mHandlerList.size());
505 for (OneEntryHandler handler : mHandlerList) {
506 if (!handler.onEntryCreated(vcard)) {
511 for (OneEntryHandler handler : mHandlerList) {
512 handler.onEntryCreated(vcard);
519 private String createOneEntryInternal(final String contactId,
520 final Method getEntityIteratorMethod) throws VCardException {
521 final Map<String, List<ContentValues>> contentValuesListMap =
522 new HashMap<String, List<ContentValues>>();
523 // The resolver may return the entity iterator with no data. It is possible.
524 // e.g. If all the data in the contact of the given contact id are not exportable ones,
525 // they are hidden from the view of this method, though contact id itself exists.
526 EntityIterator entityIterator = null;
528 final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
529 // .appendQueryParameter("for_export_only", "1")
530 .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1")
532 final String selection = Data.CONTACT_ID + "=?";
533 final String[] selectionArgs = new String[] {contactId};
534 if (getEntityIteratorMethod != null) {
535 // Please note that this branch is executed by unit tests only
537 entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
538 mContentResolver, uri, selection, selectionArgs, null);
539 } catch (IllegalArgumentException e) {
540 Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
542 } catch (IllegalAccessException e) {
543 Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
545 } catch (InvocationTargetException e) {
546 Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
547 StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
548 for (StackTraceElement element : stackTraceElements) {
549 Log.e(LOG_TAG, " at " + element.toString());
551 throw new VCardException("InvocationTargetException has been thrown: " +
552 e.getCause().getMessage());
555 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
556 uri, null, selection, selectionArgs, null));
559 if (entityIterator == null) {
560 Log.e(LOG_TAG, "EntityIterator is null");
564 if (!entityIterator.hasNext()) {
565 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
569 while (entityIterator.hasNext()) {
570 Entity entity = entityIterator.next();
571 for (NamedContentValues namedContentValues : entity.getSubValues()) {
572 ContentValues contentValues = namedContentValues.values;
573 String key = contentValues.getAsString(Data.MIMETYPE);
575 List<ContentValues> contentValuesList =
576 contentValuesListMap.get(key);
577 if (contentValuesList == null) {
578 contentValuesList = new ArrayList<ContentValues>();
579 contentValuesListMap.put(key, contentValuesList);
581 contentValuesList.add(contentValues);
586 if (entityIterator != null) {
587 entityIterator.close();
591 return buildVCard(contentValuesListMap);
595 * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
596 * {ContactsContract}. Developers can override this method to customize the output.
598 public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
599 if (contentValuesListMap == null) {
600 Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
603 final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
604 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
605 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
606 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
607 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
608 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
609 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
610 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
611 if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
612 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
614 builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
615 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
616 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
617 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
618 return builder.toString();
622 public void terminate() {
623 for (OneEntryHandler handler : mHandlerList) {
624 handler.onTerminate();
627 if (mCursor != null) {
630 } catch (SQLiteException e) {
631 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
636 mTerminateIsCalled = true;
640 public void finalize() {
641 if (!mTerminateIsCalled) {
642 Log.w(LOG_TAG, "terminate() is not called yet. We call it in finalize() step.");
648 * @return returns the number of available entities. The return value is undefined
649 * when this object is not ready yet (typically when {{@link #init()} is not called
650 * or when {@link #terminate()} is already called).
652 public int getCount() {
653 if (mCursor == null) {
654 Log.w(LOG_TAG, "This object is not ready yet.");
657 return mCursor.getCount();
661 * @return true when there's no entity to be built. The return value is undefined
662 * when this object is not ready yet.
664 public boolean isAfterLast() {
665 if (mCursor == null) {
666 Log.w(LOG_TAG, "This object is not ready yet.");
669 return mCursor.isAfterLast();
673 * @return Returns the error reason.
675 public String getErrorReason() {