/* * 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 com.android.vcard; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Entity; import android.content.Entity.NamedContentValues; import android.content.EntityIterator; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; 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.SipAddress; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.RawContactsEntity; import android.provider.ContactsContract; import android.text.TextUtils; import android.util.Log; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** *
* The class for composing vCard from Contacts information. *
** Usually, this class should be used like this. *
*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(); * } * }*
* Users have to manually take care of memory efficiency. Even one vCard may contain * image of non-trivial size for mobile devices. *
** {@link VCardBuilder} is used to build each vCard. *
*/ public class VCardComposer { private static final String LOG_TAG = "VCardComposer"; private static final boolean DEBUG = false; 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"; // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, // since usual vCard devices for Japanese devices already use it. private static final String SHIFT_JIS = "SHIFT_JIS"; private static final String UTF_8 = "UTF-8"; private static final Map
* String selection = Data.CONTACT_ID + "=?";
* String[] selectionArgs = new String[] {contactId};
* Cursor cursor = mContentResolver.query(
* contentUriForRawContactsEntity, null, selection, selectionArgs, null)
*
*
* You can call this method or a variant of this method just once. In other words, you cannot
* reuse this object.
*
* @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
* need to change the default Uri.
*/
@Deprecated
public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
contentUriForRawContactsEntity);
}
/**
* Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
* arguments.
*/
public boolean init(final String selection, final String[] selectionArgs) {
return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
null, 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) {
return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
}
/**
* @param contentUri Uri for obtaining the list of contactId. Used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selection selection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selectionArgs selectionArgs used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param sortOrder sortOrder used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
* contactId.
* Note that this is an unstable interface, may be deleted in the future.
*/
public boolean init(final Uri contentUri, final String selection,
final String[] selectionArgs, final String sortOrder,
final Uri contentUriForRawContactsEntity) {
return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
contentUriForRawContactsEntity);
}
/**
* A variant of init(). Currently just for testing. Use other variants for init().
*
* First we'll create {@link Cursor} for the list of contactId.
*
*
* Cursor cursorForId = mContentResolver.query(
* contentUri, projection, selection, selectionArgs, sortOrder);
*
*
* After that, we'll obtain data for each contactId in the list.
*
*
* Cursor cursorForContent = mContentResolver.query(
* contentUriForRawContactsEntity, null,
* Data.CONTACT_ID + "=?", new String[] {contactId}, null)
*
*
* {@link #createOneEntry()} or its variants let the caller obtain each entry from
* cursorForContent
above.
*
* @param contentUri Uri for obtaining the list of contactId. Used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param projection projection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selection selection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selectionArgs selectionArgs used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param sortOrder sortOrder used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
* contactId.
* @return true when successful
*
* @hide
*/
public boolean init(final Uri contentUri, final String[] projection,
final String selection, final String[] selectionArgs,
final String sortOrder, Uri contentUriForRawContactsEntity) {
if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
return false;
}
if (!initInterFirstPart(contentUriForRawContactsEntity)) {
return false;
}
if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
sortOrder)) {
return false;
}
if (!initInterMainPart()) {
return false;
}
return initInterLastPart();
}
/**
* Just for testing for now. Do not use.
* @hide
*/
public boolean init(Cursor cursor) {
return initWithCallback(cursor, null);
}
/**
* @param cursor Cursor that used to get contact id
* @param rawContactEntitlesInfoCallback Callback that return RawContactEntitlesInfo
* Note that this is an unstable interface, may be deleted in the future.
*
* @return true when successful
*/
public boolean initWithCallback(Cursor cursor,
RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback) {
if (!initInterFirstPart(null)) {
return false;
}
mCursorSuppliedFromOutside = true;
mCursor = cursor;
mRawContactEntitlesInfoCallback = rawContactEntitlesInfoCallback;
if (!initInterMainPart()) {
return false;
}
return initInterLastPart();
}
private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
mContentUriForRawContactsEntity =
(contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
RawContactsEntity.CONTENT_URI);
if (mInitDone) {
Log.e(LOG_TAG, "init() is already called");
return false;
}
return true;
}
private boolean initInterCursorCreationPart(
final Uri contentUri, final String[] projection,
final String selection, final String[] selectionArgs, final String sortOrder) {
mCursorSuppliedFromOutside = false;
mCursor = mContentResolver.query(
contentUri, projection, selection, selectionArgs, sortOrder);
if (mCursor == null) {
Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
return false;
}
return true;
}
private boolean initInterMainPart() {
if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
if (DEBUG) {
Log.d(LOG_TAG,
String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
}
closeCursorIfAppropriate();
return false;
}
mIdColumn = mCursor.getColumnIndex(Data.CONTACT_ID);
if (mIdColumn < 0) {
mIdColumn = mCursor.getColumnIndex(Contacts._ID);
}
return mIdColumn >= 0;
}
private boolean initInterLastPart() {
mInitDone = true;
mTerminateCalled = false;
return true;
}
/**
* @return a vCard string.
*/
public String createOneEntry() {
return createOneEntry(null);
}
/**
* @hide
*/
public String createOneEntry(Method getEntityIteratorMethod) {
if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
mFirstVCardEmittedInDoCoMoCase = true;
// Previously we needed to emit empty data for this specific case, but actually
// this doesn't work now, as resolver doesn't return any data with "-1" contactId.
// TODO: re-introduce or remove this logic. Needs to modify unit test when we
// re-introduce the logic.
// return createOneEntryInternal("-1", getEntityIteratorMethod);
}
final String vcard = createOneEntryInternal(mCursor.getLong(mIdColumn),
getEntityIteratorMethod);
if (!mCursor.moveToNext()) {
Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
}
return vcard;
}
/**
* Class that store rawContactEntitlesUri and contactId
*/
public static class RawContactEntitlesInfo {
public final Uri rawContactEntitlesUri;
public final long contactId;
public RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId) {
this.rawContactEntitlesUri = rawContactEntitlesUri;
this.contactId = contactId;
}
}
/**
* Listener for getting raw contact entitles info
*/
public interface RawContactEntitlesInfoCallback {
/**
* Callback to get RawContactEntitlesInfo from contact id
*
* @param contactId Contact id that you want to process.
* @return RawContactEntitlesInfo that ready to process.
*/
RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId);
}
private String createOneEntryInternal(long contactId,
final Method getEntityIteratorMethod) {
final Map* Set a callback for phone number formatting. It will be called every time when this object * receives a phone number for printing. *
** When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored * and the callback should be responsible for everything about phone number formatting. *
** Caution: This interface will change. Please don't use without any strong reason. *
*/ public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) { mPhoneTranslationCallback = callback; } /** * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in * {ContactsContract}. Developers can override this method to customize the output. */ public String buildVCard(final Map