/* * 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.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; 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.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.telephony.PhoneNumberUtils; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Time; 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.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * <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 = "vcard.VCardComposer"; private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; 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"; public static final String NO_ERROR = "No error"; private static final Uri sDataRequestUri; static { Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); sDataRequestUri = builder.build(); } 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")); } 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(); } } } public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; private static final String VCARD_PROPERTY_ADR = "ADR"; private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; private static final String VCARD_PROPERTY_END = "END"; private static final String VCARD_PROPERTY_NAME = "N"; private static final String VCARD_PROPERTY_FULL_NAME = "FN"; private static final String VCARD_PROPERTY_NOTE = "NOTE"; private static final String VCARD_PROPERTY_ORG = "ORG"; private static final String VCARD_PROPERTY_SOUND = "SOUND"; private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; private static final String VCARD_PROPERTY_TEL = "TEL"; private static final String VCARD_PROPERTY_TITLE = "TITLE"; private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; private static final String VCARD_PROPERTY_VERSION = "VERSION"; private static final String VCARD_PROPERTY_URL = "URL"; private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; // Android specific properties // TODO: ues extra MIME-TYPE instead of adding this kind of inflexible fields private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; // Property for call log entry private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; // Properties for DoCoMo vCard. private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; private static final String VCARD_PROPERTY_X_NO = "X-NO"; private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; private static final String VCARD_DATA_VCARD = "VCARD"; private static final String VCARD_DATA_PUBLIC = "PUBLIC"; private static final String VCARD_ATTR_SEPARATOR = ";"; private static final String VCARD_COL_SEPARATOR = "\r\n"; private static final String VCARD_DATA_SEPARATOR = ":"; private static final String VCARD_ITEM_SEPARATOR = ";"; private static final String VCARD_WS = " "; private static final String VCARD_ATTR_EQUAL = "="; // Type strings are now in VCardConstants.java. private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; private static final String SHIFT_JIS = "SHIFT_JIS"; private final Context mContext; private final int mVCardType; private final boolean mCareHandlerErrors; private final ContentResolver mContentResolver; // Convenient member variables about the restriction of the vCard format. // Used for not calling the same methods returning same results. private final boolean mIsV30; private final boolean mIsJapaneseMobilePhone; private final boolean mOnlyOneNoteFieldIsAvailable; private final boolean mIsDoCoMo; private final boolean mUsesQuotedPrintable; private final boolean mUsesAndroidProperty; private final boolean mUsesDefactProperty; private final boolean mUsesUtf8; private final boolean mUsesShiftJis; private final boolean mUsesQPToPrimaryProperties; private Cursor mCursor; private int mIdColumn; private final String mCharsetString; private final String mVCardAttributeCharset; private boolean mTerminateIsCalled; final private List<OneEntryHandler> mHandlerList; private String mErrorReason = NO_ERROR; private static final Map<Integer, String> sImMap; static { sImMap = new HashMap<Integer, String>(); sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); // Google talk is a special case. } private boolean mIsCallLogComposer = false; private boolean mNeedPhotoForVCard = true; private static final String[] sContactsProjection = new String[] { Contacts._ID, }; /** The projection to use when querying the call log table */ private static final String[] sCallLogProjection = new String[] { Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_LABEL }; private static final int NUMBER_COLUMN_INDEX = 0; private static final int DATE_COLUMN_INDEX = 1; private static final int CALL_TYPE_COLUMN_INDEX = 2; private static final int CALLER_NAME_COLUMN_INDEX = 3; private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; private static final String FLAG_TIMEZONE_UTC = "Z"; public VCardComposer(Context context) { this(context, VCardConfig.VCARD_TYPE_DEFAULT, true, false, true); } public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors, false, true); } public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { this(context, vcardType, careHandlerErrors, false, true); } /** * Construct for supporting call log entry vCard composing. * * @param isCallLogComposer true if this composer is for creating Call Log vCard. */ public VCardComposer(Context context, int vcardType, boolean careHandlerErrors, boolean isCallLogComposer, boolean needPhotoInVCard) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; mIsCallLogComposer = isCallLogComposer; mNeedPhotoForVCard = needPhotoInVCard; mContentResolver = context.getContentResolver(); mIsV30 = VCardConfig.isV30(vcardType); mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); mIsJapaneseMobilePhone = VCardConfig .needsToConvertPhoneticString(vcardType); mOnlyOneNoteFieldIsAvailable = VCardConfig .onlyOneNoteFieldIsAvailable(vcardType); mUsesAndroidProperty = VCardConfig .usesAndroidSpecificProperty(vcardType); mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); mUsesUtf8 = VCardConfig.usesUtf8(vcardType); mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in // Android, not shown to the public). mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; } else if (mUsesShiftJis) { mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; } else { mCharsetString = "UTF-8"; mVCardAttributeCharset = "CHARSET=UTF-8"; } } /** * This static function is to compose vCard for phone own number */ public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, String phoneNumber, boolean vcardVer21) { final StringBuilder builder = new StringBuilder(); appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); if (!vcardVer21) { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); } else { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); } boolean needCharset = false; if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { needCharset = true; } // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, phoneName, needCharset, false); appendVCardLine(builder, VCARD_PROPERTY_NAME, phoneName, needCharset, false); String label = Integer.toString(phonetype); appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); return builder.toString(); } /** * Must call before {{@link #init()}. */ public void addHandler(OneEntryHandler handler) { mHandlerList.add(handler); } public boolean init() { return init(null, null); } /** * @return Returns true when initialization is successful and all the other * methods are available. Returns false otherwise. */ public boolean init(final String selection, final String[] selectionArgs) { 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); } } if (mIsCallLogComposer) { mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection, selection, selectionArgs, null); } else { mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs, null); } 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; } if (mIsCallLogComposer) { mIdColumn = -1; } else { mIdColumn = mCursor.getColumnIndex(Contacts._ID); } return true; } public boolean createOneEntry() { if (mCursor == null || mCursor.isAfterLast()) { mErrorReason = FAILURE_REASON_NOT_INITIALIZED; return false; } String name = null; String vcard; try { if (mIsCallLogComposer) { vcard = createOneCallLogEntryInternal(); } else { if (mIdColumn >= 0) { vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); } else { Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); return true; } } } 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: " + name); 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; } /** * Format according to RFC 2445 DATETIME type. * The format is: ("%Y%m%dT%H%M%SZ"). */ private final String toRfc2455Format(final long millSecs) { Time startDate = new Time(); startDate.set(millSecs); String date = startDate.format2445(); return date + FLAG_TIMEZONE_UTC; } /** * Try to append the property line for a call history time stamp field if possible. * Do nothing if the call log type gotton from the database is invalid. */ private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { // Extension for call history as defined in // in the Specification for Ic Mobile Communcation - ver 1.1, // Oct 2000. This is used to send the details of the call // history - missed, incoming, outgoing along with date and time // to the requesting device (For example, transferring phone book // when connected over bluetooth) // // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); final String callLogTypeStr; switch (callLogType) { case Calls.INCOMING_TYPE: { callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; break; } case Calls.OUTGOING_TYPE: { callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; break; } case Calls.MISSED_TYPE: { callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; break; } default: { Log.w(LOG_TAG, "Call log type not correct."); return; } } final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); builder.append(VCARD_PROPERTY_X_TIMESTAMP); builder.append(VCARD_ATTR_SEPARATOR); appendTypeAttribute(builder, callLogTypeStr); builder.append(VCARD_DATA_SEPARATOR); builder.append(toRfc2455Format(dateAsLong)); builder.append(VCARD_COL_SEPARATOR); } private String createOneCallLogEntryInternal() { final StringBuilder builder = new StringBuilder(); appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); if (mIsV30) { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); } else { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); } String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); if (TextUtils.isEmpty(name)) { name = mCursor.getString(NUMBER_COLUMN_INDEX); } final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, name, needCharset, false); appendVCardLine(builder, VCARD_PROPERTY_NAME, name, needCharset, false); String number = mCursor.getString(NUMBER_COLUMN_INDEX); int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); if (TextUtils.isEmpty(label)) { label = Integer.toString(type); } appendVCardTelephoneLine(builder, type, label, number); tryAppendCallHistoryTimeStampField(builder); appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); return builder.toString(); } private String createOneEntryInternal(final String contactId) { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); final String selection = Data.CONTACT_ID + "=?"; final String[] selectionArgs = new String[] {contactId}; // The resolver may return the entity iterator with no data. It is possiible. // 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. boolean dataExists = false; EntityIterator entityIterator = null; try { entityIterator = mContentResolver.queryEntities( sDataRequestUri, selection, selectionArgs, null); dataExists = entityIterator.hasNext(); 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(); } } if (!dataExists) { return ""; } final StringBuilder builder = new StringBuilder(); appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); if (mIsV30) { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); } else { appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); } appendStructuredNames(builder, contentValuesListMap); appendNickNames(builder, contentValuesListMap); appendPhones(builder, contentValuesListMap); appendEmails(builder, contentValuesListMap); appendPostals(builder, contentValuesListMap); appendIms(builder, contentValuesListMap); appendWebsites(builder, contentValuesListMap); appendBirthday(builder, contentValuesListMap); appendOrganizations(builder, contentValuesListMap); if (mNeedPhotoForVCard) { appendPhotos(builder, contentValuesListMap); } appendNotes(builder, contentValuesListMap); // TODO: GroupMembership if (mIsDoCoMo) { appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); } appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); 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; } private void appendStructuredNames(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(StructuredName.CONTENT_ITEM_TYPE); if (contentValuesList != null && contentValuesList.size() > 0) { appendStructuredNamesInternal(builder, contentValuesList); } else if (mIsDoCoMo) { appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); } else if (mIsV30) { // vCard 3.0 requires "N" and "FN" properties. appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); } } private boolean containsNonEmptyName(ContentValues contentValues) { final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); final String prefix = contentValues.getAsString(StructuredName.PREFIX); final String suffix = contentValues.getAsString(StructuredName.SUFFIX); final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && TextUtils.isEmpty(suffix) && TextUtils.isEmpty(displayName)); } private void appendStructuredNamesInternal(final StringBuilder builder, final List<ContentValues> contentValuesList) { // For safety, we'll emit just one value around StructuredName, as external importers // may get confused with multiple "N", "FN", etc. properties, though it is valid in // vCard spec. ContentValues primaryContentValues = null; ContentValues subprimaryContentValues = null; for (ContentValues contentValues : contentValuesList) { if (contentValues == null){ continue; } Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); if (isSuperPrimary != null && isSuperPrimary > 0) { // We choose "super primary" ContentValues. primaryContentValues = contentValues; break; } else if (primaryContentValues == null) { // We choose the first "primary" ContentValues // if "super primary" ContentValues does not exist. Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); if (isPrimary != null && isPrimary > 0 && containsNonEmptyName(contentValues)) { primaryContentValues = contentValues; // Do not break, since there may be ContentValues with "super primary" // afterword. } else if (subprimaryContentValues == null && containsNonEmptyName(contentValues)) { subprimaryContentValues = contentValues; } } } if (primaryContentValues == null) { if (subprimaryContentValues != null) { // We choose the first ContentValues if any "primary" ContentValues does not exist. primaryContentValues = subprimaryContentValues; } else { Log.e(LOG_TAG, "All ContentValues given from database is empty."); primaryContentValues = new ContentValues(); } } final String familyName = primaryContentValues .getAsString(StructuredName.FAMILY_NAME); final String middleName = primaryContentValues .getAsString(StructuredName.MIDDLE_NAME); final String givenName = primaryContentValues .getAsString(StructuredName.GIVEN_NAME); final String prefix = primaryContentValues .getAsString(StructuredName.PREFIX); final String suffix = primaryContentValues .getAsString(StructuredName.SUFFIX); final String displayName = primaryContentValues .getAsString(StructuredName.DISPLAY_NAME); if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { final String encodedFamily; final String encodedGiven; final String encodedMiddle; final String encodedPrefix; final String encodedSuffix; final boolean reallyUseQuotedPrintableToName = (mUsesQPToPrimaryProperties && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); if (reallyUseQuotedPrintableToName) { encodedFamily = encodeQuotedPrintable(familyName); encodedGiven = encodeQuotedPrintable(givenName); encodedMiddle = encodeQuotedPrintable(middleName); encodedPrefix = encodeQuotedPrintable(prefix); encodedSuffix = encodeQuotedPrintable(suffix); } else { encodedFamily = escapeCharacters(familyName); encodedGiven = escapeCharacters(givenName); encodedMiddle = escapeCharacters(middleName); encodedPrefix = escapeCharacters(prefix); encodedSuffix = escapeCharacters(suffix); } // N property. This order is specified by vCard spec and does not depend on countries. builder.append(VCARD_PROPERTY_NAME); if (shouldAppendCharsetAttribute(Arrays.asList( familyName, givenName, middleName, prefix, suffix))) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintableToName) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedFamily); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedGiven); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedMiddle); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedPrefix); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedSuffix); builder.append(VCARD_COL_SEPARATOR); final String fullname = VCardUtils.constructNameFromElements( VCardConfig.getNameOrderType(mVCardType), encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); final boolean reallyUseQuotedPrintableToFullname = mUsesQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); final String encodedFullname = reallyUseQuotedPrintableToFullname ? encodeQuotedPrintable(fullname) : escapeCharacters(fullname); // FN property builder.append(VCARD_PROPERTY_FULL_NAME); if (shouldAppendCharsetAttribute(encodedFullname)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintableToFullname) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedFullname); builder.append(VCARD_COL_SEPARATOR); } else if (!TextUtils.isEmpty(displayName)) { final boolean reallyUseQuotedPrintableToDisplayName = (mUsesQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); final String encodedDisplayName = reallyUseQuotedPrintableToDisplayName ? encodeQuotedPrintable(displayName) : escapeCharacters(displayName); builder.append(VCARD_PROPERTY_NAME); if (shouldAppendCharsetAttribute(encodedDisplayName)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintableToDisplayName) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedDisplayName); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); } else if (mIsDoCoMo) { appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); } else if (mIsV30) { appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); } String phoneticFamilyName = primaryContentValues .getAsString(StructuredName.PHONETIC_FAMILY_NAME); String phoneticMiddleName = primaryContentValues .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); String phoneticGivenName = primaryContentValues .getAsString(StructuredName.PHONETIC_GIVEN_NAME); if (!(TextUtils.isEmpty(phoneticFamilyName) && TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName))) { // if not empty if (mIsJapaneseMobilePhone) { phoneticFamilyName = VCardUtils .toHalfWidthString(phoneticFamilyName); phoneticMiddleName = VCardUtils .toHalfWidthString(phoneticMiddleName); phoneticGivenName = VCardUtils .toHalfWidthString(phoneticGivenName); } if (mIsV30) { final String sortString = VCardUtils .constructNameFromElements(mVCardType, phoneticFamilyName, phoneticMiddleName, phoneticGivenName); builder.append(VCARD_PROPERTY_SORT_STRING); // Do not need to care about QP, since vCard 3.0 does not allow it. final String encodedSortString = escapeCharacters(sortString); if (shouldAppendCharsetAttribute(encodedSortString)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedSortString); builder.append(VCARD_COL_SEPARATOR); } else { // Note: There is no appropriate property for expressing // phonetic name in vCard 2.1, while there is in // vCard 3.0 (SORT-STRING). // We chose to use DoCoMo's way since it is supported by // a lot of Japanese mobile phones. This is "X-" property, so // any parser hopefully would not get confused with this. builder.append(VCARD_PROPERTY_SOUND); builder.append(VCARD_ATTR_SEPARATOR); builder.append(Constants.ATTR_TYPE_X_IRMC_N); boolean reallyUseQuotedPrintable = (mUsesQPToPrimaryProperties && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( phoneticFamilyName) && VCardUtils.containsOnlyNonCrLfPrintableAscii( phoneticMiddleName) && VCardUtils.containsOnlyNonCrLfPrintableAscii( phoneticGivenName))); final String encodedPhoneticFamilyName; final String encodedPhoneticMiddleName; final String encodedPhoneticGivenName; if (reallyUseQuotedPrintable) { encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); } else { encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } if (shouldAppendCharsetAttribute(Arrays.asList( encodedPhoneticFamilyName, encodedPhoneticMiddleName, encodedPhoneticGivenName))) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticFamilyName); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedPhoneticGivenName); builder.append(VCARD_ITEM_SEPARATOR); builder.append(encodedPhoneticMiddleName); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); } } else if (mIsDoCoMo) { builder.append(VCARD_PROPERTY_SOUND); builder.append(VCARD_ATTR_SEPARATOR); builder.append(Constants.ATTR_TYPE_X_IRMC_N); builder.append(VCARD_DATA_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); } if (mUsesDefactProperty) { if (!TextUtils.isEmpty(phoneticGivenName)) { final boolean reallyUseQuotedPrintable = (mUsesQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); final String encodedPhoneticGivenName; if (reallyUseQuotedPrintable) { encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); } else { encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintable) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticGivenName); builder.append(VCARD_COL_SEPARATOR); } if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = (mUsesQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); final String encodedPhoneticMiddleName; if (reallyUseQuotedPrintable) { encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); } else { encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); } builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintable) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticMiddleName); builder.append(VCARD_COL_SEPARATOR); } if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = (mUsesQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); final String encodedPhoneticFamilyName; if (reallyUseQuotedPrintable) { encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); } else { encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); } builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintable) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticFamilyName); builder.append(VCARD_COL_SEPARATOR); } } } private void appendNickNames(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Nickname.CONTENT_ITEM_TYPE); if (contentValuesList != null) { final String propertyNickname; if (mIsV30) { propertyNickname = VCARD_PROPERTY_NICKNAME; } else if (mUsesAndroidProperty) { propertyNickname = VCARD_PROPERTY_X_NICKNAME; } else { // There's no way to add this field. return; } for (ContentValues contentValues : contentValuesList) { final String nickname = contentValues.getAsString(Nickname.NAME); if (TextUtils.isEmpty(nickname)) { continue; } final String encodedNickname; final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); if (reallyUseQuotedPrintable) { encodedNickname = encodeQuotedPrintable(nickname); } else { encodedNickname = escapeCharacters(nickname); } builder.append(propertyNickname); if (shouldAppendCharsetAttribute(propertyNickname)) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } if (reallyUseQuotedPrintable) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedNickname); builder.append(VCARD_COL_SEPARATOR); } } } private void appendPhones(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Phone.CONTENT_ITEM_TYPE); boolean phoneLineExists = false; if (contentValuesList != null) { Set<String> phoneSet = new HashSet<String>(); for (ContentValues contentValues : contentValuesList) { final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); final String label = contentValues.getAsString(Phone.LABEL); String phoneNumber = contentValues.getAsString(Phone.NUMBER); if (phoneNumber != null) { phoneNumber = phoneNumber.trim(); } if (TextUtils.isEmpty(phoneNumber)) { continue; } int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); phoneLineExists = true; if (type == Phone.TYPE_PAGER) { phoneLineExists = true; if (!phoneSet.contains(phoneNumber)) { phoneSet.add(phoneNumber); appendVCardTelephoneLine(builder, type, label, phoneNumber); } } else { // The entry "may" have several phone numbers when the contact entry is // corrupted because of its original source. // // e.g. I encountered the entry like the following. // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." // This kind of entry is not able to be inserted via Android devices, but // possible if the source of the data is already corrupted. List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); if (phoneNumberList.isEmpty()) { continue; } phoneLineExists = true; for (String actualPhoneNumber : phoneNumberList) { if (!phoneSet.contains(actualPhoneNumber)) { final int format = VCardUtils.getPhoneNumberFormat(mVCardType); SpannableStringBuilder tmpBuilder = new SpannableStringBuilder(actualPhoneNumber); PhoneNumberUtils.formatNumber(tmpBuilder, format); final String formattedPhoneNumber = tmpBuilder.toString(); phoneSet.add(actualPhoneNumber); appendVCardTelephoneLine(builder, type, label, formattedPhoneNumber); } } } } } if (!phoneLineExists && mIsDoCoMo) { appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); } } private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { List<String> phoneList = new ArrayList<String>(); StringBuilder builder = new StringBuilder(); final int length = phoneNumber.length(); for (int i = 0; i < length; i++) { final char ch = phoneNumber.charAt(i); if (Character.isDigit(ch)) { builder.append(ch); } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { phoneList.add(builder.toString()); builder = new StringBuilder(); } } if (builder.length() > 0) { phoneList.add(builder.toString()); } return phoneList; } private void appendEmails(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Email.CONTENT_ITEM_TYPE); boolean emailAddressExists = false; if (contentValuesList != null) { Set<String> addressSet = new HashSet<String>(); for (ContentValues contentValues : contentValuesList) { Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); final int type = (typeAsObject != null ? typeAsObject : Email.TYPE_OTHER); final String label = contentValues.getAsString(Email.LABEL); String emailAddress = contentValues.getAsString(Email.DATA); if (emailAddress != null) { emailAddress = emailAddress.trim(); } if (TextUtils.isEmpty(emailAddress)) { continue; } emailAddressExists = true; if (!addressSet.contains(emailAddress)) { addressSet.add(emailAddress); appendVCardEmailLine(builder, type, label, emailAddress); } } } if (!emailAddressExists && mIsDoCoMo) { appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); } } private void appendPostals(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(StructuredPostal.CONTENT_ITEM_TYPE); if (contentValuesList != null) { if (mIsDoCoMo) { appendPostalsForDoCoMo(builder, contentValuesList); } else { appendPostalsForGeneric(builder, contentValuesList); } } else if (mIsDoCoMo) { builder.append(VCARD_PROPERTY_ADR); builder.append(VCARD_ATTR_SEPARATOR); builder.append(Constants.ATTR_TYPE_HOME); builder.append(VCARD_DATA_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); } } /** * Tries to append just one line. If there's no appropriate address * information, append an empty line. */ private void appendPostalsForDoCoMo(final StringBuilder builder, final List<ContentValues> contentValuesList) { // TODO: from old, inefficient code. fix this. if (appendPostalsForDoCoMoInternal(builder, contentValuesList, StructuredPostal.TYPE_HOME)) { return; } if (appendPostalsForDoCoMoInternal(builder, contentValuesList, StructuredPostal.TYPE_WORK)) { return; } if (appendPostalsForDoCoMoInternal(builder, contentValuesList, StructuredPostal.TYPE_OTHER)) { return; } if (appendPostalsForDoCoMoInternal(builder, contentValuesList, StructuredPostal.TYPE_CUSTOM)) { return; } Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); } private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, final List<ContentValues> contentValuesList, Integer preferedType) { for (ContentValues contentValues : contentValuesList) { final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); final String label = contentValues.getAsString(StructuredPostal.LABEL); if (type == preferedType) { appendVCardPostalLine(builder, type, label, contentValues); return true; } } return false; } private void appendPostalsForGeneric(final StringBuilder builder, final List<ContentValues> contentValuesList) { for (ContentValues contentValues : contentValuesList) { final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); final String label = contentValues.getAsString(StructuredPostal.LABEL); if (type != null) { appendVCardPostalLine(builder, type, label, contentValues); } } } private void appendIms(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Im.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); String data = contentValues.getAsString(Im.DATA); if (data != null) { data = data.trim(); } if (TextUtils.isEmpty(data)) { continue; } if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); } // TODO: add "X-GOOGLE TALK" case... } } } } private void appendWebsites(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Website.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { String website = contentValues.getAsString(Website.URL); if (website != null) { website = website.trim(); } if (!TextUtils.isEmpty(website)) { appendVCardLine(builder, VCARD_PROPERTY_URL, website); } } } } private void appendBirthday(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Event.CONTENT_ITEM_TYPE); if (contentValuesList != null && contentValuesList.size() > 0) { Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { return; } // Theoretically, there must be only one birthday for each vCard data and // we are afraid of some parse error occuring in some devices, so // we emit only one birthday entry for now. String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); if (birthday != null) { birthday = birthday.trim(); } if (!TextUtils.isEmpty(birthday)) { appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); } } } private void appendOrganizations(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Organization.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { String company = contentValues .getAsString(Organization.COMPANY); if (company != null) { company = company.trim(); } String title = contentValues .getAsString(Organization.TITLE); if (title != null) { title = title.trim(); } if (!TextUtils.isEmpty(company)) { appendVCardLine(builder, VCARD_PROPERTY_ORG, company, !VCardUtils.containsOnlyPrintableAscii(company), (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); } if (!TextUtils.isEmpty(title)) { appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, !VCardUtils.containsOnlyPrintableAscii(title), (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); } } } } private void appendPhotos(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Photo.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { byte[] data = contentValues.getAsByteArray(Photo.PHOTO); if (data == null) { continue; } final String photoType; // Use some heuristics for guessing the format of the image. // TODO: there should be some general API for detecting the file format. if (data.length >= 3 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F') { photoType = "GIF"; } else if (data.length >= 4 && data[0] == (byte) 0x89 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { // Note: vCard 2.1 officially does not support PNG, but we // may have it // and using X- word like "X-PNG" may not let importers know // it is // PNG. So we use the String "PNG" as is... photoType = "PNG"; } else if (data.length >= 2 && data[0] == (byte) 0xff && data[1] == (byte) 0xd8) { photoType = "JPEG"; } else { Log.d(LOG_TAG, "Unknown photo type. Ignore."); continue; } final String photoString = VCardUtils.encodeBase64(data); if (photoString.length() > 0) { appendVCardPhotoLine(builder, photoString, photoType); } } } } private void appendNotes(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); if (contentValuesList != null) { if (mOnlyOneNoteFieldIsAvailable) { StringBuilder noteBuilder = new StringBuilder(); boolean first = true; for (ContentValues contentValues : contentValuesList) { String note = contentValues.getAsString(Note.NOTE); if (note == null) { note = ""; } if (note.length() > 0) { if (first) { first = false; } else { noteBuilder.append('\n'); } noteBuilder.append(note); } } final String noteStr = noteBuilder.toString(); // This means we scan noteStr completely twice, which is redundant. // But for now, we assume this is not so time-consuming.. final boolean shouldAppendCharsetInfo = !VCardUtils.containsOnlyPrintableAscii(noteStr); final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } else { for (ContentValues contentValues : contentValuesList) { final String noteStr = contentValues.getAsString(Note.NOTE); if (!TextUtils.isEmpty(noteStr)) { final boolean shouldAppendCharsetInfo = !VCardUtils.containsOnlyPrintableAscii(noteStr); final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } } } } } /** * Append '\' to the characters which should be escaped. The character set is different * not only between vCard 2.1 and vCard 3.0 but also among each device. * * Note that Quoted-Printable string must not be input here. */ @SuppressWarnings("fallthrough") private String escapeCharacters(final String unescaped) { if (TextUtils.isEmpty(unescaped)) { return ""; } final StringBuilder tmpBuilder = new StringBuilder(); final int length = unescaped.length(); for (int i = 0; i < length; i++) { char ch = unescaped.charAt(i); switch (ch) { case ';': { tmpBuilder.append('\\'); tmpBuilder.append(';'); break; } case '\r': { if (i + 1 < length) { char nextChar = unescaped.charAt(i); if (nextChar == '\n') { continue; } else { // fall through } } else { // fall through } } case '\n': { // In vCard 2.1, there's no specification about this, while // vCard 3.0 explicitly requires this should be encoded to "\n". tmpBuilder.append("\\n"); break; } case '\\': { if (mIsV30) { tmpBuilder.append("\\\\"); break; } else { // fall through } } case '<': case '>': { if (mIsDoCoMo) { tmpBuilder.append('\\'); tmpBuilder.append(ch); } else { tmpBuilder.append(ch); } break; } case ',': { if (mIsV30) { tmpBuilder.append("\\,"); } else { tmpBuilder.append(ch); } break; } default: { tmpBuilder.append(ch); break; } } } return tmpBuilder.toString(); } private void appendVCardPhotoLine(final StringBuilder builder, final String encodedData, final String photoType) { StringBuilder tmpBuilder = new StringBuilder(); tmpBuilder.append(VCARD_PROPERTY_PHOTO); tmpBuilder.append(VCARD_ATTR_SEPARATOR); if (mIsV30) { tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); } else { tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); } tmpBuilder.append(VCARD_ATTR_SEPARATOR); appendTypeAttribute(tmpBuilder, photoType); tmpBuilder.append(VCARD_DATA_SEPARATOR); tmpBuilder.append(encodedData); final String tmpStr = tmpBuilder.toString(); tmpBuilder = new StringBuilder(); int lineCount = 0; int length = tmpStr.length(); for (int i = 0; i < length; i++) { tmpBuilder.append(tmpStr.charAt(i)); lineCount++; if (lineCount > 72) { tmpBuilder.append(VCARD_COL_SEPARATOR); tmpBuilder.append(VCARD_WS); lineCount = 0; } } builder.append(tmpBuilder.toString()); builder.append(VCARD_COL_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); } private void appendVCardPostalLine(final StringBuilder builder, final Integer typeAsObject, final String label, final ContentValues contentValues) { builder.append(VCARD_PROPERTY_ADR); builder.append(VCARD_ATTR_SEPARATOR); // Note: Not sure why we need to emit "empty" line even when actual data does not exist. // There may be some reason or may not be any. We keep safer side. // TODO: investigate this. boolean dataExists = false; String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); boolean actuallyUseQuotedPrintable = false; boolean shouldAppendCharset = false; for (String data : dataArray) { if (!TextUtils.isEmpty(data)) { dataExists = true; if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { shouldAppendCharset = true; } if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { actuallyUseQuotedPrintable = true; break; } } } int length = dataArray.length; for (int i = 0; i < length; i++) { String data = dataArray[i]; if (!TextUtils.isEmpty(data)) { if (actuallyUseQuotedPrintable) { dataArray[i] = encodeQuotedPrintable(data); } else { dataArray[i] = escapeCharacters(data); } } } final int typeAsPrimitive; if (typeAsObject == null) { typeAsPrimitive = StructuredPostal.TYPE_OTHER; } else { typeAsPrimitive = typeAsObject; } String typeAsString = null; switch (typeAsPrimitive) { case StructuredPostal.TYPE_HOME: { typeAsString = Constants.ATTR_TYPE_HOME; break; } case StructuredPostal.TYPE_WORK: { typeAsString = Constants.ATTR_TYPE_WORK; break; } case StructuredPostal.TYPE_CUSTOM: { if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { // We're not sure whether the label is valid in the spec // ("IANA-token" in the vCard 3.0 is unclear...) // Just for safety, we add "X-" at the beggining of each label. // Also checks the label obeys with vCard 3.0 spec. builder.append("X-"); builder.append(label); builder.append(VCARD_DATA_SEPARATOR); } break; } case StructuredPostal.TYPE_OTHER: { break; } default: { Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); break; } } // Attribute(s). { boolean shouldAppendAttrSeparator = false; if (typeAsString != null) { appendTypeAttribute(builder, typeAsString); shouldAppendAttrSeparator = true; } if (dataExists) { if (shouldAppendCharset) { // Strictly, vCard 3.0 does not allow exporters to emit charset information, // but we will add it since the information should be useful for importers, // // Assume no parser does not emit error with this attribute in vCard 3.0. if (shouldAppendAttrSeparator) { builder.append(VCARD_ATTR_SEPARATOR); } builder.append(mVCardAttributeCharset); shouldAppendAttrSeparator = true; } if (actuallyUseQuotedPrintable) { if (shouldAppendAttrSeparator) { builder.append(VCARD_ATTR_SEPARATOR); } builder.append(VCARD_ATTR_ENCODING_QP); shouldAppendAttrSeparator = true; } } } // Property values. builder.append(VCARD_DATA_SEPARATOR); if (dataExists) { // The elements in dataArray are already encoded to quoted printable // if needed. // See above. // // TODO: in vCard 3.0, one line may become too huge. Fix this. builder.append(dataArray[0]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[1]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[2]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[3]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[4]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[5]); builder.append(VCARD_ITEM_SEPARATOR); builder.append(dataArray[6]); } builder.append(VCARD_COL_SEPARATOR); } private void appendVCardEmailLine(final StringBuilder builder, final Integer typeAsObject, final String label, final String data) { builder.append(VCARD_PROPERTY_EMAIL); final int typeAsPrimitive; if (typeAsObject == null) { typeAsPrimitive = Email.TYPE_OTHER; } else { typeAsPrimitive = typeAsObject; } final String typeAsString; switch (typeAsPrimitive) { case Email.TYPE_CUSTOM: { // For backward compatibility. // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. // To support mobile type at that time, this custom label had been used. if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME .equals(label)) { typeAsString = Constants.ATTR_TYPE_CELL; } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { typeAsString = "X-" + label; } else { typeAsString = DEFAULT_EMAIL_TYPE; } break; } case Email.TYPE_HOME: { typeAsString = Constants.ATTR_TYPE_HOME; break; } case Email.TYPE_WORK: { typeAsString = Constants.ATTR_TYPE_WORK; break; } case Email.TYPE_OTHER: { typeAsString = DEFAULT_EMAIL_TYPE; break; } case Email.TYPE_MOBILE: { typeAsString = Constants.ATTR_TYPE_CELL; break; } default: { Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); typeAsString = DEFAULT_EMAIL_TYPE; break; } } builder.append(VCARD_ATTR_SEPARATOR); appendTypeAttribute(builder, typeAsString); builder.append(VCARD_DATA_SEPARATOR); builder.append(data); builder.append(VCARD_COL_SEPARATOR); } private void appendVCardTelephoneLine(final StringBuilder builder, final Integer typeAsObject, final String label, String encodedData) { builder.append(VCARD_PROPERTY_TEL); builder.append(VCARD_ATTR_SEPARATOR); final int typeAsPrimitive; if (typeAsObject == null) { typeAsPrimitive = Phone.TYPE_OTHER; } else { typeAsPrimitive = typeAsObject; } switch (typeAsPrimitive) { case Phone.TYPE_HOME: appendTypeAttributes(builder, Arrays.asList( Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); break; case Phone.TYPE_WORK: appendTypeAttributes(builder, Arrays.asList( Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); break; case Phone.TYPE_FAX_HOME: appendTypeAttributes(builder, Arrays.asList( Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); break; case Phone.TYPE_FAX_WORK: appendTypeAttributes(builder, Arrays.asList( Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); break; case Phone.TYPE_MOBILE: builder.append(Constants.ATTR_TYPE_CELL); break; case Phone.TYPE_PAGER: if (mIsDoCoMo) { // Not sure about the reason, but previous implementation had // used "VOICE" instead of "PAGER" // Also, refrain from using appendType() so that "TYPE=" is never be appended. builder.append(Constants.ATTR_TYPE_VOICE); } else { appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); } break; case Phone.TYPE_OTHER: appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); break; case Phone.TYPE_CUSTOM: if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { appendTypeAttribute(builder, "X-" + label); } else { // Just ignore the custom type. appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); } break; default: appendUncommonPhoneType(builder, typeAsPrimitive); break; } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); builder.append(VCARD_COL_SEPARATOR); } /** * Appends phone type string which may not be available in some devices. */ private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { if (mIsDoCoMo) { // The previous implementation for DoCoMo had been conservative // about miscellaneous types. builder.append(Constants.ATTR_TYPE_VOICE); } else { String phoneAttribute = VCardUtils.getPhoneAttributeString(type); if (phoneAttribute != null) { appendTypeAttribute(builder, phoneAttribute); } else { Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); } } } private void appendVCardLine(final StringBuilder builder, final String propertyName, final String rawData) { appendVCardLine(builder, propertyName, rawData, false, false); } private void appendVCardLine(final StringBuilder builder, final String field, final String rawData, final boolean needCharset, boolean needQuotedPrintable) { builder.append(field); if (needCharset) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); } final String encodedData; if (needQuotedPrintable) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(VCARD_ATTR_ENCODING_QP); encodedData = encodeQuotedPrintable(rawData); } else { // TODO: one line may be too huge, which may be invalid in vCard spec, though // several (even well-known) applications do not care this. encodedData = escapeCharacters(rawData); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); builder.append(VCARD_COL_SEPARATOR); } private void appendTypeAttributes(final StringBuilder builder, final List<String> types) { // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. boolean first = true; for (String type : types) { if (first) { first = false; } else { builder.append(VCARD_ATTR_SEPARATOR); } appendTypeAttribute(builder, type); } } private void appendTypeAttribute(final StringBuilder builder, final String type) { // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" if (mIsV30) { builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); } builder.append(type); } /** * Returns true when the property line should contain charset attribute * information. This method may return true even when vCard version is 3.0. * * Strictly, adding charset information is invalid in VCard 3.0. * However we'll add the info only when used charset is not UTF-8 * in vCard 3.0 format, since parser side may be able to use the charset * via this field, though we may encounter another problem by adding it... * * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 * recommends UTF-8. By adding this field, parsers may be able * to know this text is NOT UTF-8 but Shift_Jis. */ private boolean shouldAppendCharsetAttribute(final String propertyValue) { return (!VCardUtils.containsOnlyPrintableAscii(propertyValue) && (!mIsV30 || !mUsesUtf8)); } private boolean shouldAppendCharsetAttribute(final List<String> propertyValueList) { boolean shouldAppendBasically = false; for (String propertyValue : propertyValueList) { if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { shouldAppendBasically = true; break; } } return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); } private String encodeQuotedPrintable(String str) { if (TextUtils.isEmpty(str)) { return ""; } { // Replace "\n" and "\r" with "\r\n". StringBuilder tmpBuilder = new StringBuilder(); int length = str.length(); for (int i = 0; i < length; i++) { char ch = str.charAt(i); if (ch == '\r') { if (i + 1 < length && str.charAt(i + 1) == '\n') { i++; } tmpBuilder.append("\r\n"); } else if (ch == '\n') { tmpBuilder.append("\r\n"); } else { tmpBuilder.append(ch); } } str = tmpBuilder.toString(); } final StringBuilder tmpBuilder = new StringBuilder(); int index = 0; int lineCount = 0; byte[] strArray = null; try { strArray = str.getBytes(mCharsetString); } catch (UnsupportedEncodingException e) { Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + "Try default charset"); strArray = str.getBytes(); } while (index < strArray.length) { tmpBuilder.append(String.format("=%02X", strArray[index])); index += 1; lineCount += 3; if (lineCount >= 67) { // Specification requires CRLF must be inserted before the // length of the line // becomes more than 76. // Assuming that the next character is a multi-byte character, // it will become // 6 bytes. // 76 - 6 - 3 = 67 tmpBuilder.append("=\r\n"); lineCount = 0; } } return tmpBuilder.toString(); } }