| /* |
| * 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.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.nio.charset.UnsupportedCharsetException; |
| 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"; |
| |
| // TODO: Should be configurable? |
| public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; |
| public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; |
| public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; |
| |
| public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = |
| "Failed to get database information"; |
| |
| public static final String FAILURE_REASON_NO_ENTRY = |
| "There's no exportable in the database"; |
| |
| public static final String FAILURE_REASON_NOT_INITIALIZED = |
| "The vCard composer object is not correctly initialized"; |
| |
| /** Should be visible only from developers... (no need to translate, hopefully) */ |
| public static final String FAILURE_REASON_UNSUPPORTED_URI = |
| "The Uri vCard composer received is not supported by the composer."; |
| |
| public static final String NO_ERROR = "No error"; |
| |
| public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; |
| |
| // 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"; |
| |
| private static final String VCARD_DATA_VCARD = "VCARD"; |
| private static final String VCARD_DATA_PUBLIC = "PUBLIC"; |
| |
| private static final String VCARD_PARAM_SEPARATOR = ";"; |
| private static final String VCARD_END_OF_LINE = "\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_PARAM_EQUAL = "="; |
| |
| private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; |
| |
| private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; |
| private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; |
| |
| private static final String SHIFT_JIS = "SHIFT_JIS"; |
| private static final String UTF_8 = "UTF-8"; |
| |
| /** |
| * Special URI for testing. |
| */ |
| public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; |
| public static final Uri VCARD_TEST_AUTHORITY_URI = |
| Uri.parse("content://" + VCARD_TEST_AUTHORITY); |
| public static final Uri CONTACTS_TEST_CONTENT_URI = |
| Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); |
| |
| private static final Uri sDataRequestUri; |
| private static final Map<Integer, String> sImMap; |
| |
| /** |
| * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}. |
| */ |
| private static final Set<String> sPrimaryPropertyNameSet; |
| |
| static { |
| Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); |
| builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); |
| sDataRequestUri = builder.build(); |
| 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. |
| |
| // TODO: incomplete. Implement properly |
| sPrimaryPropertyNameSet = new HashSet<String>(); |
| sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); |
| sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); |
| sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); |
| } |
| |
| 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(); |
| } |
| } |
| } |
| |
| 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 mAppendTypeParamName; |
| private final boolean mRefrainsQPToPrimaryProperties; |
| private final boolean mNeedsToConvertPhoneticString; |
| |
| private Cursor mCursor; |
| private int mIdColumn; |
| |
| private final String mCharsetString; |
| private final String mVCardCharsetParameter; |
| private boolean mTerminateIsCalled; |
| final private List<OneEntryHandler> mHandlerList; |
| |
| private String mErrorReason = NO_ERROR; |
| |
| private boolean mIsCallLogComposer; |
| |
| 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); |
| } |
| |
| public VCardComposer(Context context, int vcardType) { |
| this(context, vcardType, true); |
| } |
| |
| public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { |
| this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); |
| } |
| |
| /** |
| * Construct for supporting call log entry vCard composing. |
| */ |
| public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { |
| mContext = context; |
| mVCardType = vcardType; |
| mCareHandlerErrors = careHandlerErrors; |
| 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); |
| mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType); |
| mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); |
| mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); |
| mHandlerList = new ArrayList<OneEntryHandler>(); |
| |
| if (mIsDoCoMo) { |
| String charset; |
| try { |
| charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); |
| } catch (UnsupportedCharsetException e) { |
| Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); |
| charset = SHIFT_JIS; |
| } |
| mCharsetString = charset; |
| // 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). |
| mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; |
| } else if (mUsesShiftJis) { |
| String charset; |
| try { |
| charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); |
| } catch (UnsupportedCharsetException e) { |
| Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); |
| charset = SHIFT_JIS; |
| } |
| mCharsetString = charset; |
| mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; |
| } else { |
| mCharsetString = UTF_8; |
| mVCardCharsetParameter = "CHARSET=" + UTF_8; |
| } |
| } |
| |
| /** |
| * Must call before {{@link #init()}. |
| */ |
| public void addHandler(OneEntryHandler handler) { |
| if (handler != null) { |
| mHandlerList.add(handler); |
| } |
| } |
| |
| /** |
| * @return Returns true when initialization is successful and all the other |
| * methods are available. Returns false otherwise. |
| */ |
| public boolean init() { |
| return init(null, null); |
| } |
| |
| public boolean init(final String selection, final String[] selectionArgs) { |
| return init(Contacts.CONTENT_URI, selection, selectionArgs, null); |
| } |
| |
| /** |
| * Note that this is unstable interface, may be deleted in the future. |
| */ |
| public boolean init(final Uri contentUri, final String selection, |
| final String[] selectionArgs, final String sortOrder) { |
| if (contentUri == null) { |
| return false; |
| } |
| if (mCareHandlerErrors) { |
| List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( |
| mHandlerList.size()); |
| for (OneEntryHandler handler : mHandlerList) { |
| if (!handler.onInit(mContext)) { |
| for (OneEntryHandler finished : finishedList) { |
| finished.onTerminate(); |
| } |
| return false; |
| } |
| } |
| } else { |
| // Just ignore the false returned from onInit(). |
| for (OneEntryHandler handler : mHandlerList) { |
| handler.onInit(mContext); |
| } |
| } |
| |
| final String[] projection; |
| if (CallLog.Calls.CONTENT_URI.equals(contentUri)) { |
| projection = sCallLogProjection; |
| mIsCallLogComposer = true; |
| } else if (Contacts.CONTENT_URI.equals(contentUri) || |
| CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { |
| projection = sContactsProjection; |
| } else { |
| mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; |
| return false; |
| } |
| mCursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, sortOrder); |
| |
| if (mCursor == null) { |
| mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; |
| return false; |
| } |
| |
| if (getCount() == 0 || !mCursor.moveToFirst()) { |
| try { |
| mCursor.close(); |
| } catch (SQLiteException e) { |
| Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); |
| } finally { |
| mCursor = null; |
| mErrorReason = FAILURE_REASON_NO_ENTRY; |
| } |
| return false; |
| } |
| |
| 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; |
| } |
| |
| 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, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); |
| if (mIsV30) { |
| appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); |
| } else { |
| appendVCardLine(builder, Constants.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); |
| appendPhotos(builder, contentValuesListMap); |
| appendNotes(builder, contentValuesListMap); |
| // TODO: GroupMembership, Relation, Event other than birthday. |
| |
| if (mIsDoCoMo) { |
| appendVCardLine(builder, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); |
| appendVCardLine(builder, Constants.PROPERTY_X_REDUCTION, ""); |
| appendVCardLine(builder, Constants.PROPERTY_X_NO, ""); |
| appendVCardLine(builder, Constants.PROPERTY_X_DCM_HMN_MODE, ""); |
| } |
| |
| appendVCardLine(builder, Constants.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, Constants.PROPERTY_N, ""); |
| } else if (mIsV30) { |
| // vCard 3.0 requires "N" and "FN" properties. |
| appendVCardLine(builder, Constants.PROPERTY_N, ""); |
| appendVCardLine(builder, Constants.PROPERTY_FN, ""); |
| } |
| } |
| |
| 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 phoneticFamilyName = |
| contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); |
| final String phoneticMiddleName = |
| contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); |
| final String phoneticGivenName = |
| contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); |
| 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(phoneticFamilyName) && |
| TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && |
| 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 boolean shouldAppendCharsetParameterToName = |
| !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && |
| shouldAppendCharsetParameters(Arrays.asList( |
| familyName, givenName, middleName, prefix, suffix)); |
| final boolean reallyUseQuotedPrintableToName = |
| (!mRefrainsQPToPrimaryProperties && |
| !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && |
| VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && |
| VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && |
| VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && |
| VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); |
| |
| final String formattedName; |
| if (!TextUtils.isEmpty(displayName)) { |
| formattedName = displayName; |
| } else { |
| formattedName = VCardUtils.constructNameFromElements( |
| VCardConfig.getNameOrderType(mVCardType), |
| familyName, middleName, givenName, prefix, suffix); |
| } |
| final boolean shouldAppendCharsetParameterToFN = |
| !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && |
| shouldAppendCharsetParameter(formattedName); |
| final boolean reallyUseQuotedPrintableToFN = |
| !mRefrainsQPToPrimaryProperties && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); |
| |
| final String encodedFamily; |
| final String encodedGiven; |
| final String encodedMiddle; |
| final String encodedPrefix; |
| final String encodedSuffix; |
| 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); |
| } |
| |
| final String encodedFormattedname = |
| (reallyUseQuotedPrintableToFN ? |
| encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); |
| |
| builder.append(Constants.PROPERTY_N); |
| if (mIsDoCoMo) { |
| if (shouldAppendCharsetParameterToName) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintableToName) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| // DoCoMo phones require that all the elements in the "family name" field. |
| builder.append(formattedName); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| } else { |
| if (shouldAppendCharsetParameterToName) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintableToName) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_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_END_OF_LINE); |
| |
| // FN property |
| builder.append(Constants.PROPERTY_FN); |
| if (shouldAppendCharsetParameterToFN) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintableToFN) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedFormattedname); |
| builder.append(VCARD_END_OF_LINE); |
| } else if (!TextUtils.isEmpty(displayName)) { |
| final boolean reallyUseQuotedPrintableToDisplayName = |
| (!mRefrainsQPToPrimaryProperties && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); |
| final String encodedDisplayName = |
| reallyUseQuotedPrintableToDisplayName ? |
| encodeQuotedPrintable(displayName) : |
| escapeCharacters(displayName); |
| |
| builder.append(Constants.PROPERTY_N); |
| if (shouldAppendCharsetParameter(displayName)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintableToDisplayName) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_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_END_OF_LINE); |
| builder.append(Constants.PROPERTY_FN); |
| |
| // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it |
| // when it would be useful for external importers, assuming no external |
| // importer allows this vioration. |
| if (shouldAppendCharsetParameter(displayName)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedDisplayName); |
| builder.append(VCARD_END_OF_LINE); |
| } else if (mIsV30) { |
| // vCard 3.0 specification requires these fields. |
| appendVCardLine(builder, Constants.PROPERTY_N, ""); |
| appendVCardLine(builder, Constants.PROPERTY_FN, ""); |
| } else if (mIsDoCoMo) { |
| appendVCardLine(builder, Constants.PROPERTY_N, ""); |
| } |
| |
| final String phoneticFamilyName; |
| final String phoneticMiddleName; |
| final String phoneticGivenName; |
| { |
| String tmpPhoneticFamilyName = |
| primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); |
| String tmpPhoneticMiddleName = |
| primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); |
| String tmpPhoneticGivenName = |
| primaryContentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); |
| if (mNeedsToConvertPhoneticString) { |
| phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); |
| phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); |
| phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); |
| } else { |
| phoneticFamilyName = tmpPhoneticFamilyName; |
| phoneticMiddleName = tmpPhoneticMiddleName; |
| phoneticGivenName = tmpPhoneticGivenName; |
| } |
| } |
| if (!(TextUtils.isEmpty(phoneticFamilyName) |
| && TextUtils.isEmpty(phoneticMiddleName) |
| && TextUtils.isEmpty(phoneticGivenName))) { |
| |
| if (mIsV30) { |
| final String sortString = VCardUtils |
| .constructNameFromElements(mVCardType, |
| phoneticFamilyName, phoneticMiddleName, phoneticGivenName); |
| builder.append(Constants.PROPERTY_SORT_STRING); |
| if (shouldAppendCharsetParameter(sortString)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(escapeCharacters(sortString)); |
| builder.append(VCARD_END_OF_LINE); |
| } else if (mIsJapaneseMobilePhone) { |
| // 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 when the device is Japanese one |
| // 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(Constants.PROPERTY_SOUND); |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(Constants.PARAM_TYPE_X_IRMC_N); |
| |
| boolean reallyUseQuotedPrintable = |
| (!mRefrainsQPToPrimaryProperties |
| && !(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 (shouldAppendCharsetParameters(Arrays.asList( |
| encodedPhoneticFamilyName, encodedPhoneticMiddleName, |
| encodedPhoneticGivenName))) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| // DoCoMo's specification requires vCard composer to use just the first |
| // column. |
| { |
| boolean first = true; |
| if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { |
| builder.append(encodedPhoneticFamilyName); |
| first = false; |
| } |
| if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { |
| if (first) { |
| first = false; |
| } else { |
| builder.append(' '); |
| } |
| builder.append(encodedPhoneticMiddleName); |
| } |
| if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { |
| if (!first) { |
| builder.append(' '); |
| } |
| builder.append(encodedPhoneticGivenName); |
| } |
| } |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_ITEM_SEPARATOR); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| } else if (mIsDoCoMo) { |
| builder.append(Constants.PROPERTY_SOUND); |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(Constants.PARAM_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_END_OF_LINE); |
| } |
| |
| if (mUsesDefactProperty) { |
| if (!TextUtils.isEmpty(phoneticGivenName)) { |
| final boolean reallyUseQuotedPrintable = |
| (mUsesQuotedPrintable && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); |
| final String encodedPhoneticGivenName; |
| if (reallyUseQuotedPrintable) { |
| encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); |
| } else { |
| encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); |
| } |
| builder.append(Constants.PROPERTY_X_PHONETIC_FIRST_NAME); |
| if (shouldAppendCharsetParameter(phoneticGivenName)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintable) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedPhoneticGivenName); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| if (!TextUtils.isEmpty(phoneticMiddleName)) { |
| final boolean reallyUseQuotedPrintable = |
| (mUsesQuotedPrintable && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); |
| final String encodedPhoneticMiddleName; |
| if (reallyUseQuotedPrintable) { |
| encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); |
| } else { |
| encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); |
| } |
| builder.append(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME); |
| if (shouldAppendCharsetParameter(phoneticMiddleName)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintable) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedPhoneticMiddleName); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| if (!TextUtils.isEmpty(phoneticFamilyName)) { |
| final boolean reallyUseQuotedPrintable = |
| (mUsesQuotedPrintable && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); |
| final String encodedPhoneticFamilyName; |
| if (reallyUseQuotedPrintable) { |
| encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); |
| } else { |
| encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); |
| } |
| builder.append(Constants.PROPERTY_X_PHONETIC_LAST_NAME); |
| if (shouldAppendCharsetParameter(phoneticFamilyName)) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| if (reallyUseQuotedPrintable) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| } |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedPhoneticFamilyName); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| } |
| } |
| |
| 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) { |
| return; |
| } |
| |
| final boolean useAndroidProperty; |
| if (mIsV30) { |
| useAndroidProperty = false; |
| } else if (mUsesAndroidProperty) { |
| useAndroidProperty = true; |
| } 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; |
| } |
| if (useAndroidProperty) { |
| appendAndroidSpecificProperty(builder, Nickname.CONTENT_ITEM_TYPE, |
| contentValues); |
| } else { |
| appendVCardLineWithCharsetAndQPDetection(builder, |
| Constants.PROPERTY_NICKNAME, nickname); |
| } |
| } |
| } |
| |
| 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); |
| final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); |
| final boolean isPrimary = (isPrimaryAsInteger != null ? |
| (isPrimaryAsInteger > 0) : false); |
| String phoneNumber = contentValues.getAsString(Phone.NUMBER); |
| if (phoneNumber != null) { |
| phoneNumber = phoneNumber.trim(); |
| } |
| if (TextUtils.isEmpty(phoneNumber)) { |
| continue; |
| } |
| int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); |
| if (type == Phone.TYPE_PAGER) { |
| phoneLineExists = true; |
| if (!phoneSet.contains(phoneNumber)) { |
| phoneSet.add(phoneNumber); |
| appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary); |
| } |
| } 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); |
| final String formattedPhoneNumber = |
| PhoneNumberUtils.formatNumber(actualPhoneNumber, format); |
| phoneSet.add(actualPhoneNumber); |
| appendVCardTelephoneLine(builder, type, label, |
| formattedPhoneNumber, isPrimary); |
| } |
| } |
| } |
| } |
| } |
| |
| if (!phoneLineExists && mIsDoCoMo) { |
| appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false); |
| } |
| } |
| |
| 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) { |
| final Set<String> addressSet = new HashSet<String>(); |
| for (ContentValues contentValues : contentValuesList) { |
| String emailAddress = contentValues.getAsString(Email.DATA); |
| if (emailAddress != null) { |
| emailAddress = emailAddress.trim(); |
| } |
| if (TextUtils.isEmpty(emailAddress)) { |
| continue; |
| } |
| Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); |
| final int type = (typeAsObject != null ? |
| typeAsObject : DEFAULT_EMAIL_TYPE); |
| final String label = contentValues.getAsString(Email.LABEL); |
| Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); |
| final boolean isPrimary = (isPrimaryAsInteger != null ? |
| (isPrimaryAsInteger > 0) : false); |
| emailAddressExists = true; |
| if (!addressSet.contains(emailAddress)) { |
| addressSet.add(emailAddress); |
| appendVCardEmailLine(builder, type, label, emailAddress, isPrimary); |
| } |
| } |
| } |
| |
| if (!emailAddressExists && mIsDoCoMo) { |
| appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false); |
| } |
| } |
| |
| 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(Constants.PROPERTY_ADR); |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(Constants.PARAM_TYPE_HOME); |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| } |
| |
| /** |
| * 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) { |
| // 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. |
| // We keep safer side since the previous implementation did so. |
| appendVCardPostalLine(builder, type, label, contentValues, true, true); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void appendPostalsForGeneric(final StringBuilder builder, |
| final List<ContentValues> contentValuesList) { |
| for (ContentValues contentValues : contentValuesList) { |
| if (contentValues == null) { |
| continue; |
| } |
| final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE); |
| final int type = (typeAsObject != null ? |
| typeAsObject : DEFAULT_POSTAL_TYPE); |
| final String label = contentValues.getAsString(StructuredPostal.LABEL); |
| final Integer isPrimaryAsInteger = |
| contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); |
| final boolean isPrimary = (isPrimaryAsInteger != null ? |
| (isPrimaryAsInteger > 0) : false); |
| appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false); |
| } |
| } |
| |
| 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) { |
| return; |
| } |
| for (ContentValues contentValues : contentValuesList) { |
| final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); |
| if (protocolAsObject == null) { |
| continue; |
| } |
| final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); |
| if (propertyName == null) { |
| continue; |
| } |
| String data = contentValues.getAsString(Im.DATA); |
| if (data != null) { |
| data = data.trim(); |
| } |
| if (TextUtils.isEmpty(data)) { |
| continue; |
| } |
| final String typeAsString; |
| { |
| final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); |
| switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { |
| case Im.TYPE_HOME: { |
| typeAsString = Constants.PARAM_TYPE_HOME; |
| break; |
| } |
| case Im.TYPE_WORK: { |
| typeAsString = Constants.PARAM_TYPE_WORK; |
| break; |
| } |
| case Im.TYPE_CUSTOM: { |
| final String label = contentValues.getAsString(Im.LABEL); |
| typeAsString = (label != null ? "X-" + label : null); |
| break; |
| } |
| case Im.TYPE_OTHER: // Ignore |
| default: { |
| typeAsString = null; |
| break; |
| } |
| } |
| } |
| |
| List<String> parameterList = new ArrayList<String>(); |
| if (!TextUtils.isEmpty(typeAsString)) { |
| parameterList.add(typeAsString); |
| } |
| final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); |
| final boolean isPrimary = (isPrimaryAsInteger != null ? |
| (isPrimaryAsInteger > 0) : false); |
| if (isPrimary) { |
| parameterList.add(Constants.PARAM_TYPE_PREF); |
| } |
| |
| appendVCardLineWithCharsetAndQPDetection( |
| builder, propertyName, parameterList, data); |
| } |
| } |
| |
| 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) { |
| return; |
| } |
| for (ContentValues contentValues : contentValuesList) { |
| String website = contentValues.getAsString(Website.URL); |
| if (website != null) { |
| website = website.trim(); |
| } |
| |
| // Note: vCard 3.0 does not allow any parameter addition toward "URL" |
| // property, while there's no document in vCard 2.1. |
| // |
| // TODO: Should we allow adding it when appropriate? |
| // (Actually, we drop some data. Using "group.X-URL-TYPE" or something |
| // may help) |
| if (!TextUtils.isEmpty(website)) { |
| appendVCardLine(builder, Constants.PROPERTY_URL, website); |
| } |
| } |
| } |
| |
| /** |
| * Theoretically, there must be only one birthday for each vCard entry. |
| * Also, we are afraid of some importer's parse error during its import. |
| * We emit only one birthday entry even when there are more than one. |
| */ |
| 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) { |
| return; |
| } |
| String primaryBirthday = null; |
| String secondaryBirthday = null; |
| for (ContentValues contentValues : contentValuesList) { |
| if (contentValues == null) { |
| continue; |
| } |
| final Integer eventType = contentValues.getAsInteger(Event.TYPE); |
| if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { |
| continue; |
| } |
| final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); |
| if (birthdayCandidate == null) { |
| continue; |
| } |
| final Integer isSuperPrimaryAsInteger = |
| contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); |
| final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? |
| (isSuperPrimaryAsInteger > 0) : false); |
| if (isSuperPrimary) { |
| // "super primary" birthday should the prefered one. |
| primaryBirthday = birthdayCandidate; |
| break; |
| } |
| final Integer isPrimaryAsInteger = |
| contentValues.getAsInteger(Event.IS_PRIMARY); |
| final boolean isPrimary = (isPrimaryAsInteger != null ? |
| (isPrimaryAsInteger > 0) : false); |
| if (isPrimary) { |
| // We don't break here since "super primary" birthday may exist later. |
| primaryBirthday = birthdayCandidate; |
| } else if (secondaryBirthday == null) { |
| // First entry is set to the "secondary" candidate. |
| secondaryBirthday = birthdayCandidate; |
| } |
| } |
| |
| final String birthday; |
| if (primaryBirthday != null) { |
| birthday = primaryBirthday.trim(); |
| } else if (secondaryBirthday != null){ |
| birthday = secondaryBirthday.trim(); |
| } else { |
| return; |
| } |
| appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_BDAY, 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 department = contentValues.getAsString(Organization.DEPARTMENT); |
| if (department != null) { |
| department = department.trim(); |
| } |
| String title = contentValues.getAsString(Organization.TITLE); |
| if (title != null) { |
| title = title.trim(); |
| } |
| |
| StringBuilder orgBuilder = new StringBuilder(); |
| if (!TextUtils.isEmpty(company)) { |
| orgBuilder.append(company); |
| } |
| if (!TextUtils.isEmpty(department)) { |
| if (orgBuilder.length() > 0) { |
| orgBuilder.append(';'); |
| } |
| orgBuilder.append(department); |
| } |
| final String orgline = orgBuilder.toString(); |
| appendVCardLine(builder, Constants.PROPERTY_ORG, orgline, |
| !VCardUtils.containsOnlyPrintableAscii(orgline), |
| (mUsesQuotedPrintable && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); |
| |
| if (!TextUtils.isEmpty(title)) { |
| appendVCardLine(builder, Constants.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, Constants.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, Constants.PROPERTY_NOTE, noteStr, |
| shouldAppendCharsetInfo, reallyUseQuotedPrintable); |
| } |
| } |
| } |
| } |
| } |
| |
| private void appendAndroidSpecificProperty(final StringBuilder builder, |
| final String mimeType, ContentValues contentValues) { |
| List<String> rawDataList = new ArrayList<String>(); |
| rawDataList.add(mimeType); |
| final List<String> columnNameList; |
| if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { |
| |
| } else { |
| // If you add the other field, please check all the columns are able to be |
| // converted to String. |
| // |
| // e.g. BLOB is not what we can handle here now. |
| return; |
| } |
| |
| for (int i = 1; i <= Constants.MAX_DATA_COLUMN; i++) { |
| String value = contentValues.getAsString("data" + i); |
| if (value == null) { |
| value = ""; |
| } |
| rawDataList.add(value); |
| } |
| |
| appendVCardLineWithCharsetAndQPDetection(builder, |
| Constants.PROPERTY_X_ANDROID_CUSTOM, rawDataList); |
| } |
| |
| /** |
| * 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++) { |
| final 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') { |
| break; |
| } 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(Constants.PROPERTY_PHOTO); |
| tmpBuilder.append(VCARD_PARAM_SEPARATOR); |
| if (mIsV30) { |
| tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); |
| } else { |
| tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); |
| } |
| tmpBuilder.append(VCARD_PARAM_SEPARATOR); |
| appendTypeParameter(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_END_OF_LINE); |
| tmpBuilder.append(VCARD_WS); |
| lineCount = 0; |
| } |
| } |
| builder.append(tmpBuilder.toString()); |
| builder.append(VCARD_END_OF_LINE); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| |
| private class PostalStruct { |
| final boolean reallyUseQuotedPrintable; |
| final boolean appendCharset; |
| final String addressData; |
| public PostalStruct(final boolean reallyUseQuotedPrintable, |
| final boolean appendCharset, final String addressData) { |
| this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; |
| this.appendCharset = appendCharset; |
| this.addressData = addressData; |
| } |
| } |
| |
| /** |
| * @return null when there's no information available to construct the data. |
| */ |
| private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { |
| boolean reallyUseQuotedPrintable = false; |
| boolean appendCharset = false; |
| |
| boolean dataArrayExists = false; |
| String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); |
| for (String data : dataArray) { |
| if (!TextUtils.isEmpty(data)) { |
| dataArrayExists = true; |
| if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { |
| appendCharset = true; |
| } |
| if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { |
| reallyUseQuotedPrintable = true; |
| break; |
| } |
| } |
| } |
| |
| if (dataArrayExists) { |
| StringBuffer addressBuffer = new StringBuffer(); |
| boolean first = true; |
| for (String data : dataArray) { |
| if (first) { |
| first = false; |
| } else { |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| } |
| if (!TextUtils.isEmpty(data)) { |
| if (reallyUseQuotedPrintable) { |
| addressBuffer.append(encodeQuotedPrintable(data)); |
| } else { |
| addressBuffer.append(escapeCharacters(data)); |
| } |
| } |
| } |
| return new PostalStruct(reallyUseQuotedPrintable, appendCharset, |
| addressBuffer.toString()); |
| } |
| |
| String formattedAddress = |
| contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); |
| if (!TextUtils.isEmpty(formattedAddress)) { |
| reallyUseQuotedPrintable = |
| !VCardUtils.containsOnlyPrintableAscii(formattedAddress); |
| appendCharset = |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedAddress); |
| if (reallyUseQuotedPrintable) { |
| formattedAddress = encodeQuotedPrintable(formattedAddress); |
| } else { |
| formattedAddress = escapeCharacters(formattedAddress); |
| } |
| // We use the second value ("Extended Address"). |
| // |
| // adr-value = 0*6(text-value ";") text-value |
| // ; PO Box, Extended Address, Street, Locality, Region, Postal |
| // ; Code, Country Name |
| StringBuffer addressBuffer = new StringBuffer(); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| addressBuffer.append(formattedAddress); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| addressBuffer.append(VCARD_ITEM_SEPARATOR); |
| return new PostalStruct( |
| reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); |
| } |
| return null; // There's no data available. |
| } |
| |
| private void appendVCardPostalLine(final StringBuilder builder, |
| final int type, final String label, final ContentValues contentValues, |
| final boolean isPrimary, final boolean emitLineEveryTime) { |
| final boolean reallyUseQuotedPrintable; |
| final boolean appendCharset; |
| final String addressData; |
| { |
| PostalStruct postalStruct = tryConstructPostalStruct(contentValues); |
| if (postalStruct == null) { |
| if (emitLineEveryTime) { |
| reallyUseQuotedPrintable = false; |
| appendCharset = false; |
| addressData = ""; |
| } else { |
| return; |
| } |
| } else { |
| reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; |
| appendCharset = postalStruct.appendCharset; |
| addressData = postalStruct.addressData; |
| } |
| } |
| |
| List<String> parameterList = new ArrayList<String>(); |
| if (isPrimary) { |
| parameterList.add(Constants.PARAM_TYPE_PREF); |
| } |
| switch (type) { |
| case StructuredPostal.TYPE_HOME: { |
| parameterList.add(Constants.PARAM_TYPE_HOME); |
| break; |
| } |
| case StructuredPostal.TYPE_WORK: { |
| parameterList.add(Constants.PARAM_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. |
| parameterList.add("X-" + label); |
| } |
| break; |
| } |
| case StructuredPostal.TYPE_OTHER: { |
| break; |
| } |
| default: { |
| Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); |
| break; |
| } |
| } |
| |
| // Actual data construction starts from here. |
| // TODO: add a new version of appendVCardLine() for this purpose. |
| |
| builder.append(Constants.PROPERTY_ADR); |
| builder.append(VCARD_PARAM_SEPARATOR); |
| |
| // Parameters |
| { |
| boolean shouldAppendParamSeparator = false; |
| if (!parameterList.isEmpty()) { |
| appendTypeParameters(builder, parameterList); |
| shouldAppendParamSeparator = true; |
| } |
| |
| if (appendCharset) { |
| // 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 parameter in vCard 3.0. |
| if (shouldAppendParamSeparator) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| } |
| builder.append(mVCardCharsetParameter); |
| shouldAppendParamSeparator = true; |
| } |
| |
| if (reallyUseQuotedPrintable) { |
| if (shouldAppendParamSeparator) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| } |
| builder.append(VCARD_PARAM_ENCODING_QP); |
| shouldAppendParamSeparator = true; |
| } |
| } |
| |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(addressData); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| |
| private void appendVCardEmailLine(final StringBuilder builder, |
| final int type, final String label, |
| final String rawData, final boolean isPrimary) { |
| final String typeAsString; |
| switch (type) { |
| 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.PARAM_TYPE_CELL; |
| } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) |
| && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { |
| typeAsString = "X-" + label; |
| } else { |
| typeAsString = null; |
| } |
| break; |
| } |
| case Email.TYPE_HOME: { |
| typeAsString = Constants.PARAM_TYPE_HOME; |
| break; |
| } |
| case Email.TYPE_WORK: { |
| typeAsString = Constants.PARAM_TYPE_WORK; |
| break; |
| } |
| case Email.TYPE_OTHER: { |
| typeAsString = null; |
| break; |
| } |
| case Email.TYPE_MOBILE: { |
| typeAsString = Constants.PARAM_TYPE_CELL; |
| break; |
| } |
| default: { |
| Log.e(LOG_TAG, "Unknown Email type: " + type); |
| typeAsString = null; |
| break; |
| } |
| } |
| |
| final List<String> parameterList = new ArrayList<String>(); |
| if (isPrimary) { |
| parameterList.add(Constants.PARAM_TYPE_PREF); |
| } |
| if (!TextUtils.isEmpty(typeAsString)) { |
| parameterList.add(typeAsString); |
| } |
| |
| appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL, |
| parameterList, rawData); |
| } |
| |
| private void appendVCardTelephoneLine(final StringBuilder builder, |
| final Integer typeAsObject, final String label, |
| final String encodedData, boolean isPrimary) { |
| builder.append(Constants.PROPERTY_TEL); |
| builder.append(VCARD_PARAM_SEPARATOR); |
| |
| final int typeAsPrimitive; |
| if (typeAsObject == null) { |
| typeAsPrimitive = Phone.TYPE_OTHER; |
| } else { |
| typeAsPrimitive = typeAsObject; |
| } |
| |
| ArrayList<String> parameterList = new ArrayList<String>(); |
| switch (typeAsPrimitive) { |
| case Phone.TYPE_HOME: |
| parameterList.addAll( |
| Arrays.asList(Constants.PARAM_TYPE_HOME)); |
| break; |
| case Phone.TYPE_WORK: |
| parameterList.addAll( |
| Arrays.asList(Constants.PARAM_TYPE_WORK)); |
| break; |
| case Phone.TYPE_FAX_HOME: |
| parameterList.addAll( |
| Arrays.asList(Constants.PARAM_TYPE_HOME, Constants.PARAM_TYPE_FAX)); |
| break; |
| case Phone.TYPE_FAX_WORK: |
| parameterList.addAll( |
| Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_FAX)); |
| break; |
| case Phone.TYPE_MOBILE: |
| parameterList.add(Constants.PARAM_TYPE_CELL); |
| break; |
| case Phone.TYPE_PAGER: |
| if (mIsDoCoMo) { |
| // Not sure about the reason, but previous implementation had |
| // used "VOICE" instead of "PAGER" |
| parameterList.add(Constants.PARAM_TYPE_VOICE); |
| } else { |
| parameterList.add(Constants.PARAM_TYPE_PAGER); |
| } |
| break; |
| case Phone.TYPE_OTHER: |
| parameterList.add(Constants.PARAM_TYPE_VOICE); |
| break; |
| case Phone.TYPE_CAR: |
| parameterList.add(Constants.PARAM_TYPE_CAR); |
| break; |
| case Phone.TYPE_COMPANY_MAIN: |
| // There's no relevant field in vCard (at least 2.1). |
| parameterList.add(Constants.PARAM_TYPE_WORK); |
| isPrimary = true; |
| break; |
| case Phone.TYPE_ISDN: |
| parameterList.add(Constants.PARAM_TYPE_ISDN); |
| break; |
| case Phone.TYPE_MAIN: |
| isPrimary = true; |
| break; |
| case Phone.TYPE_OTHER_FAX: |
| parameterList.add(Constants.PARAM_TYPE_FAX); |
| break; |
| case Phone.TYPE_TELEX: |
| parameterList.add(Constants.PARAM_TYPE_TLX); |
| break; |
| case Phone.TYPE_WORK_MOBILE: |
| parameterList.addAll( |
| Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_CELL)); |
| break; |
| case Phone.TYPE_WORK_PAGER: |
| parameterList.add(Constants.PARAM_TYPE_WORK); |
| // See above. |
| if (mIsDoCoMo) { |
| parameterList.add(Constants.PARAM_TYPE_VOICE); |
| } else { |
| parameterList.add(Constants.PARAM_TYPE_PAGER); |
| } |
| break; |
| case Phone.TYPE_MMS: |
| parameterList.add(Constants.PARAM_TYPE_MSG); |
| break; |
| case Phone.TYPE_CUSTOM: |
| if (mUsesAndroidProperty && !TextUtils.isEmpty(label) |
| && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { |
| // Note: Strictly, vCard 2.1 does not allow "X-" parameter without |
| // "TYPE=" string. |
| parameterList.add("X-" + label); |
| } else { |
| // Just ignore the custom type. |
| parameterList.add(Constants.PARAM_TYPE_VOICE); |
| } |
| break; |
| case Phone.TYPE_RADIO: |
| case Phone.TYPE_TTY_TDD: |
| default: |
| break; |
| } |
| |
| if (isPrimary) { |
| parameterList.add(Constants.PARAM_TYPE_PREF); |
| } |
| |
| if (parameterList.isEmpty()) { |
| appendUncommonPhoneType(builder, typeAsPrimitive); |
| } else { |
| appendTypeParameters(builder, parameterList); |
| } |
| |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(encodedData); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| |
| /** |
| * 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.PARAM_TYPE_VOICE); |
| } else { |
| String phoneType = VCardUtils.getPhoneTypeString(type); |
| if (phoneType != null) { |
| appendTypeParameter(builder, phoneType); |
| } else { |
| Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); |
| } |
| } |
| } |
| |
| // appendVCardLine() variants accepting one String. |
| |
| private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, |
| final String propertyName, final String rawData) { |
| appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData); |
| } |
| |
| private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, |
| final String propertyName, |
| final List<String> parameterList, final String rawData) { |
| final boolean needCharset = |
| (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData)); |
| final boolean reallyUseQuotedPrintable = |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData); |
| appendVCardLine(builder, propertyName, parameterList, |
| rawData, needCharset, reallyUseQuotedPrintable); |
| } |
| |
| 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 propertyName, final String rawData, final boolean needCharset, |
| boolean needQuotedPrintable) { |
| appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable); |
| } |
| |
| private void appendVCardLine(final StringBuilder builder, |
| final String propertyName, |
| final List<String> parameterList, |
| final String rawData, final boolean needCharset, |
| boolean needQuotedPrintable) { |
| builder.append(propertyName); |
| if (parameterList != null && parameterList.size() > 0) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| appendTypeParameters(builder, parameterList); |
| } |
| if (needCharset) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| |
| final String encodedData; |
| if (needQuotedPrintable) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_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_END_OF_LINE); |
| } |
| |
| // appendVCardLine() variants accepting List<String>. |
| |
| private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, |
| final String propertyName, final List<String> rawDataList) { |
| appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList); |
| } |
| |
| private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, |
| final String propertyName, |
| final List<String> parameterList, final List<String> rawDataList) { |
| boolean needCharset = false; |
| boolean reallyUseQuotedPrintable = false; |
| for (String rawData : rawDataList) { |
| if (!needCharset && mUsesQuotedPrintable && |
| !VCardUtils.containsOnlyPrintableAscii(rawData)) { |
| needCharset = true; |
| } |
| if (!reallyUseQuotedPrintable && |
| !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData)) { |
| reallyUseQuotedPrintable = true; |
| } |
| if (needCharset && reallyUseQuotedPrintable) { |
| break; |
| } |
| } |
| |
| appendVCardLine(builder, propertyName, parameterList, |
| rawDataList, needCharset, reallyUseQuotedPrintable); |
| } |
| |
| /* |
| private void appendVCardLine(final StringBuilder builder, |
| final String propertyName, final List<String> rawDataList) { |
| appendVCardLine(builder, propertyName, rawDataList, false, false); |
| } |
| |
| private void appendVCardLine(final StringBuilder builder, |
| final String propertyName, final List<String> rawDataList, |
| final boolean needCharset, boolean needQuotedPrintable) { |
| appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable); |
| }*/ |
| |
| private void appendVCardLine(final StringBuilder builder, |
| final String propertyName, |
| final List<String> parameterList, |
| final List<String> rawDataList, final boolean needCharset, |
| boolean needQuotedPrintable) { |
| builder.append(propertyName); |
| if (parameterList != null && parameterList.size() > 0) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| appendTypeParameters(builder, parameterList); |
| } |
| if (needCharset) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(mVCardCharsetParameter); |
| } |
| |
| builder.append(VCARD_DATA_SEPARATOR); |
| boolean first = true; |
| for (String rawData : rawDataList) { |
| final String encodedData; |
| if (needQuotedPrintable) { |
| builder.append(VCARD_PARAM_SEPARATOR); |
| builder.append(VCARD_PARAM_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); |
| } |
| |
| if (first) { |
| first = false; |
| } else { |
| builder.append(VCARD_ITEM_SEPARATOR); |
| } |
| builder.append(encodedData); |
| } |
| builder.append(VCARD_END_OF_LINE); |
| } |
| |
| /** |
| * VCARD_PARAM_SEPARATOR must be appended before this method being called. |
| */ |
| private void appendTypeParameters(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_PARAM_SEPARATOR); |
| } |
| appendTypeParameter(builder, type); |
| } |
| } |
| |
| /** |
| * VCARD_PARAM_SEPARATOR must be appended before this method being called. |
| */ |
| private void appendTypeParameter(final StringBuilder builder, final String type) { |
| // Refrain from using appendType() so that "TYPE=" is not be appended when the |
| // device is DoCoMo's (just for safety). |
| // |
| // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" |
| if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { |
| builder.append(Constants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); |
| } |
| builder.append(type); |
| } |
| |
| /** |
| * Returns true when the property line should contain charset parameter |
| * 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 charset we use 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 shouldAppendCharsetParameter(final String propertyValue) { |
| return (!(mIsV30 && mUsesUtf8) && !VCardUtils.containsOnlyPrintableAscii(propertyValue)); |
| } |
| |
| private boolean shouldAppendCharsetParameters(final List<String> propertyValueList) { |
| if (mIsV30 && mUsesUtf8) { |
| return false; |
| } |
| for (String propertyValue : propertyValueList) { |
| if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| 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(); |
| } |
| |
| //// The methods bellow are for call log history //// |
| |
| /** |
| * 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, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); |
| if (!vcardVer21) { |
| appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); |
| } else { |
| appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); |
| } |
| |
| boolean needCharset = false; |
| if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { |
| needCharset = true; |
| } |
| appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false); |
| appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false); |
| |
| if (!TextUtils.isEmpty(phoneNumber)) { |
| String label = Integer.toString(phonetype); |
| appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); |
| } |
| |
| appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * 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_PARAM_SEPARATOR); |
| appendTypeParameter(builder, callLogTypeStr); |
| builder.append(VCARD_DATA_SEPARATOR); |
| builder.append(toRfc2455Format(dateAsLong)); |
| builder.append(VCARD_END_OF_LINE); |
| } |
| |
| private String createOneCallLogEntryInternal() { |
| final StringBuilder builder = new StringBuilder(); |
| appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); |
| if (mIsV30) { |
| appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); |
| } else { |
| appendVCardLine(builder, Constants.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)); |
| appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false); |
| appendVCardLine(builder, Constants.PROPERTY_N, 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, false); |
| tryAppendCallHistoryTimeStampField(builder); |
| appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); |
| return builder.toString(); |
| } |
| } |