| /* |
| * 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(); |
| } |
| } |