/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package android.pim.vcard; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Entity; import android.content.EntityIterator; import android.content.Entity.NamedContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.pim.vcard.exception.VCardException; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.RawContactsEntity; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.util.CharsetUtils; import android.util.Log; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * <p> * The class for composing VCard from Contacts information. Note that this is * completely differnt implementation from * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. * </p> * * <p> * Usually, this class should be used like this. * </p> * * <pre class="prettyprint">VCardComposer composer = null; * try { * composer = new VCardComposer(context); * composer.addHandler( * composer.new HandlerForOutputStream(outputStream)); * if (!composer.init()) { * // Do something handling the situation. * return; * } * while (!composer.isAfterLast()) { * if (mCanceled) { * // Assume a user may cancel this operation during the export. * return; * } * if (!composer.createOneEntry()) { * // Do something handling the error situation. * return; * } * } * } finally { * if (composer != null) { * composer.terminate(); * } * } </pre> */ public class VCardComposer { private static final String LOG_TAG = "VCardComposer"; public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; public static final String FAILURE_REASON_NO_ENTRY = "There's no exportable in the database"; public static final String FAILURE_REASON_NOT_INITIALIZED = "The vCard composer object is not correctly initialized"; /** Should be visible only from developers... (no need to translate, hopefully) */ public static final String FAILURE_REASON_UNSUPPORTED_URI = "The Uri vCard composer received is not supported by the composer."; public static final String NO_ERROR = "No error"; public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; private static final String SHIFT_JIS = "SHIFT_JIS"; private static final String UTF_8 = "UTF-8"; /** * Special URI for testing. */ public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; public static final Uri VCARD_TEST_AUTHORITY_URI = Uri.parse("content://" + VCARD_TEST_AUTHORITY); public static final Uri CONTACTS_TEST_CONTENT_URI = Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); private static final Map<Integer, String> sImMap; static { sImMap = new HashMap<Integer, String>(); sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); // Google talk is a special case. } public static interface OneEntryHandler { public boolean onInit(Context context); public boolean onEntryCreated(String vcard); public void onTerminate(); } /** * <p> * An useful example handler, which emits VCard String to outputstream one by one. * </p> * <p> * The input OutputStream object is closed() on {@link #onTerminate()}. * Must not close the stream outside. * </p> */ public class HandlerForOutputStream implements OneEntryHandler { @SuppressWarnings("hiding") private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; final private OutputStream mOutputStream; // mWriter will close this. private Writer mWriter; private boolean mOnTerminateIsCalled = false; /** * Input stream will be closed on the detruction of this object. */ public HandlerForOutputStream(OutputStream outputStream) { mOutputStream = outputStream; } public boolean onInit(Context context) { try { mWriter = new BufferedWriter(new OutputStreamWriter( mOutputStream, mCharsetString)); } catch (UnsupportedEncodingException e1) { Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); mErrorReason = "Encoding is not supported (usually this does not happen!): " + mCharsetString; return false; } if (mIsDoCoMo) { try { // Create one empty entry. mWriter.write(createOneEntryInternal("-1", null)); } catch (VCardException e) { Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + e.getMessage()); return false; } catch (IOException e) { Log.e(LOG_TAG, "IOException occurred during exportOneContactData: " + e.getMessage()); mErrorReason = "IOException occurred: " + e.getMessage(); return false; } } return true; } public boolean onEntryCreated(String vcard) { try { mWriter.write(vcard); } catch (IOException e) { Log.e(LOG_TAG, "IOException occurred during exportOneContactData: " + e.getMessage()); mErrorReason = "IOException occurred: " + e.getMessage(); return false; } return true; } public void onTerminate() { mOnTerminateIsCalled = true; if (mWriter != null) { try { // Flush and sync the data so that a user is able to pull // the SDCard just after // the export. mWriter.flush(); if (mOutputStream != null && mOutputStream instanceof FileOutputStream) { ((FileOutputStream) mOutputStream).getFD().sync(); } } catch (IOException e) { Log.d(LOG_TAG, "IOException during closing the output stream: " + e.getMessage()); } finally { try { mWriter.close(); } catch (IOException e) { } } } } @Override public void finalize() { if (!mOnTerminateIsCalled) { onTerminate(); } } } private final Context mContext; private final int mVCardType; private final boolean mCareHandlerErrors; private final ContentResolver mContentResolver; private final boolean mIsDoCoMo; private final boolean mUsesShiftJis; private Cursor mCursor; private int mIdColumn; private final String mCharsetString; private boolean mTerminateIsCalled; private final List<OneEntryHandler> mHandlerList; private String mErrorReason = NO_ERROR; private static final String[] sContactsProjection = new String[] { Contacts._ID, }; public VCardComposer(Context context) { this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); } public VCardComposer(Context context, int vcardType) { this(context, vcardType, true); } public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); } /** * Construct for supporting call log entry vCard composing. */ public VCardComposer(final Context context, final int vcardType, final boolean careHandlerErrors) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; mContentResolver = context.getContentResolver(); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { String charset; try { charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); } catch (UnsupportedCharsetException e) { Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); charset = SHIFT_JIS; } mCharsetString = charset; } else if (mUsesShiftJis) { String charset; try { charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); } catch (UnsupportedCharsetException e) { Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); charset = SHIFT_JIS; } mCharsetString = charset; } else { mCharsetString = UTF_8; } } /** * Must be called before {@link #init()}. */ public void addHandler(OneEntryHandler handler) { if (handler != null) { mHandlerList.add(handler); } } /** * @return Returns true when initialization is successful and all the other * methods are available. Returns false otherwise. */ public boolean init() { return init(null, null); } public boolean init(final String selection, final String[] selectionArgs) { return init(Contacts.CONTENT_URI, selection, selectionArgs, null); } /** * Note that this is unstable interface, may be deleted in the future. */ public boolean init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder) { if (contentUri == null) { return false; } if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); for (OneEntryHandler handler : mHandlerList) { if (!handler.onInit(mContext)) { for (OneEntryHandler finished : finishedList) { finished.onTerminate(); } return false; } } } else { // Just ignore the false returned from onInit(). for (OneEntryHandler handler : mHandlerList) { handler.onInit(mContext); } } final String[] projection; if (Contacts.CONTENT_URI.equals(contentUri) || CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { projection = sContactsProjection; } else { mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; return false; } mCursor = mContentResolver.query( contentUri, projection, selection, selectionArgs, sortOrder); if (mCursor == null) { mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; return false; } if (getCount() == 0 || !mCursor.moveToFirst()) { try { mCursor.close(); } catch (SQLiteException e) { Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); } finally { mCursor = null; mErrorReason = FAILURE_REASON_NO_ENTRY; } return false; } mIdColumn = mCursor.getColumnIndex(Contacts._ID); return true; } public boolean createOneEntry() { return createOneEntry(null); } /** * @param getEntityIteratorMethod For Dependency Injection. * @hide just for testing. */ public boolean createOneEntry(Method getEntityIteratorMethod) { if (mCursor == null || mCursor.isAfterLast()) { mErrorReason = FAILURE_REASON_NOT_INITIALIZED; return false; } String vcard; try { if (mIdColumn >= 0) { vcard = createOneEntryInternal(mCursor.getString(mIdColumn), getEntityIteratorMethod); } else { Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); return true; } } catch (VCardException e) { Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); return false; } catch (OutOfMemoryError error) { // Maybe some data (e.g. photo) is too big to have in memory. But it // should be rare. Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry."); System.gc(); // TODO: should tell users what happened? return true; } finally { mCursor.moveToNext(); } // This function does not care the OutOfMemoryError on the handler side // :-P if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); for (OneEntryHandler handler : mHandlerList) { if (!handler.onEntryCreated(vcard)) { return false; } } } else { for (OneEntryHandler handler : mHandlerList) { handler.onEntryCreated(vcard); } } return true; } private String createOneEntryInternal(final String contactId, Method getEntityIteratorMethod) throws VCardException { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); // The resolver may return the entity iterator with no data. It is possible. // e.g. If all the data in the contact of the given contact id are not exportable ones, // they are hidden from the view of this method, though contact id itself exists. EntityIterator entityIterator = null; try { final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") .build(); final String selection = Data.CONTACT_ID + "=?"; final String[] selectionArgs = new String[] {contactId}; if (getEntityIteratorMethod != null) { // Please note that this branch is executed by some tests only try { entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, mContentResolver, uri, selection, selectionArgs, null); } catch (IllegalArgumentException e) { Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + e.getMessage()); } catch (IllegalAccessException e) { Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + e.getMessage()); } catch (InvocationTargetException e) { Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); for (StackTraceElement element : stackTraceElements) { Log.e(LOG_TAG, " at " + element.toString()); } throw new VCardException("InvocationTargetException has been thrown: " + e.getCause().getMessage()); } } else { entityIterator = RawContacts.newEntityIterator(mContentResolver.query( uri, null, selection, selectionArgs, null)); } if (entityIterator == null) { Log.e(LOG_TAG, "EntityIterator is null"); return ""; } if (!entityIterator.hasNext()) { Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); return ""; } while (entityIterator.hasNext()) { Entity entity = entityIterator.next(); for (NamedContentValues namedContentValues : entity.getSubValues()) { ContentValues contentValues = namedContentValues.values; String key = contentValues.getAsString(Data.MIMETYPE); if (key != null) { List<ContentValues> contentValuesList = contentValuesListMap.get(key); if (contentValuesList == null) { contentValuesList = new ArrayList<ContentValues>(); contentValuesListMap.put(key, contentValuesList); } contentValuesList.add(contentValues); } } } } finally { if (entityIterator != null) { entityIterator.close(); } } final VCardBuilder builder = new VCardBuilder(mVCardType); builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); } builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); return builder.toString(); } public void terminate() { for (OneEntryHandler handler : mHandlerList) { handler.onTerminate(); } if (mCursor != null) { try { mCursor.close(); } catch (SQLiteException e) { Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); } mCursor = null; } mTerminateIsCalled = true; } @Override public void finalize() { if (!mTerminateIsCalled) { terminate(); } } public int getCount() { if (mCursor == null) { return 0; } return mCursor.getCount(); } public boolean isAfterLast() { if (mCursor == null) { return false; } return mCursor.isAfterLast(); } /** * @return Return the error reason if possible. */ public String getErrorReason() { return mErrorReason; } }