blob: 194fe339d65a374e8396a927252281ccb20fc3a4 [file] [log] [blame]
/*
* 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.os.RemoteException;
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);
}
}
}
} catch (RemoteException e) {
Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)",
contactId, e.getMessage()));
return "";
} 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))
.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
.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;
}
}