diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java new file mode 100644 index 0000000000000..eb40a3f48e7fb --- /dev/null +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -0,0 +1,1874 @@ +/* + * 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.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.CharsetUtils; +import android.util.Log; + +import org.apache.commons.codec.binary.Base64; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The class which lets users create their own vCard String. + */ +public class VCardBuilder { + private static final String LOG_TAG = "VCardBuilder"; + + // If you add the other element, please check all the columns are able to be + // converted to String. + // + // e.g. BLOB is not what we can handle here now. + private static final Set sAllowedAndroidPropertySet = + Collections.unmodifiableSet(new HashSet(Arrays.asList( + Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, + Relation.CONTENT_ITEM_TYPE))); + + 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; + + 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"; + + private final int mVCardType; + + 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 final boolean mShouldAppendCharsetParam; + + private final String mCharsetString; + private final String mVCardCharsetParameter; + + private StringBuilder mBuilder; + private boolean mEndAppended; + + public VCardBuilder(final int vcardType) { + mVCardType = vcardType; + + 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); + + mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); + + 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; + } + clear(); + } + + public void clear() { + mBuilder = new StringBuilder(); + mEndAppended = false; + appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); + } else { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); + } + } + + private boolean containsNonEmptyName(final 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 ContentValues getPrimaryContentValue(final List contentValuesList) { + 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. + final 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(); + } + } + + return primaryContentValues; + } + + /** + * 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. + */ + public VCardBuilder appendNameProperties(final List contentValuesList) { + if (contentValuesList == null || contentValuesList.isEmpty()) { + if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } else if (mIsV30) { + // vCard 3.0 requires "N" and "FN" properties. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } + return this; + } + + final ContentValues contentValues = getPrimaryContentValue(contentValuesList); + 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); + + if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final boolean reallyAppendCharsetParameterToName = + shouldAppendCharsetParam(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 reallyAppendCharsetParameterToFN = + shouldAppendCharsetParam(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)); + + mBuilder.append(VCardConstants.PROPERTY_N); + if (mIsDoCoMo) { + if (reallyAppendCharsetParameterToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + // DoCoMo phones require that all the elements in the "family name" field. + mBuilder.append(formattedName); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + } else { + if (reallyAppendCharsetParameterToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedFamily); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedGiven); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedMiddle); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedPrefix); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedSuffix); + } + mBuilder.append(VCARD_END_OF_LINE); + + // FN property + mBuilder.append(VCardConstants.PROPERTY_FN); + if (reallyAppendCharsetParameterToFN) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToFN) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedFormattedname); + mBuilder.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); + + mBuilder.append(VCardConstants.PROPERTY_N); + if (shouldAppendCharsetParam(displayName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToDisplayName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedDisplayName); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + mBuilder.append(VCardConstants.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 (shouldAppendCharsetParam(displayName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedDisplayName); + mBuilder.append(VCARD_END_OF_LINE); + } else if (mIsV30) { + // vCard 3.0 specification requires these fields. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } + + appendPhoneticNameFields(contentValues); + return this; + } + + private void appendPhoneticNameFields(final ContentValues contentValues) { + final String phoneticFamilyName; + final String phoneticMiddleName; + final String phoneticGivenName; + { + final String tmpPhoneticFamilyName = + contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + final String tmpPhoneticMiddleName = + contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + final String tmpPhoneticGivenName = + contentValues.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 (mIsDoCoMo) { + mBuilder.append(VCardConstants.PROPERTY_SOUND); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + } + return; + } + + // Try to emit the field(s) related to phonetic name. + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); + mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); + if (shouldAppendCharsetParam(sortString)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(escapeCharacters(sortString)); + mBuilder.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. + // + // Also, DoCoMo's specification requires vCard composer to use just the first + // column. + // i.e. + // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; + // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; + mBuilder.append(VCardConstants.PROPERTY_SOUND); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.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 (shouldAppendCharsetParam(encodedPhoneticFamilyName, + encodedPhoneticMiddleName, encodedPhoneticGivenName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + { + boolean first = true; + if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { + mBuilder.append(encodedPhoneticFamilyName); + first = false; + } + if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { + if (first) { + first = false; + } else { + mBuilder.append(' '); + } + mBuilder.append(encodedPhoneticMiddleName); + } + if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { + if (!first) { + mBuilder.append(' '); + } + mBuilder.append(encodedPhoneticGivenName); + } + } + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.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); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetParam(phoneticGivenName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticGivenName); + mBuilder.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); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetParam(phoneticMiddleName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticMiddleName); + mBuilder.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); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetParam(phoneticFamilyName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticFamilyName); + mBuilder.append(VCARD_END_OF_LINE); + } + } + } + + public VCardBuilder appendNickNames(final List contentValuesList) { + final boolean useAndroidProperty; + if (mIsV30) { + useAndroidProperty = false; + } else if (mUsesAndroidProperty) { + useAndroidProperty = true; + } else { + // There's no way to add this field. + return this; + } + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + if (useAndroidProperty) { + appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues); + } else { + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname); + } + } + } + return this; + } + + public VCardBuilder appendPhones(final List contentValuesList) { + boolean phoneLineExists = false; + if (contentValuesList != null) { + Set phoneSet = new HashSet(); + 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); + appendTelLine(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 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); + appendTelLine(type, label, formattedPhoneNumber, isPrimary); + } + } + } + } + } + + if (!phoneLineExists && mIsDoCoMo) { + appendTelLine(Phone.TYPE_HOME, "", "", false); + } + + return this; + } + + private List splitIfSeveralPhoneNumbersExist(final String phoneNumber) { + List phoneList = new ArrayList(); + + 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; + } + + public VCardBuilder appendEmails(final List contentValuesList) { + boolean emailAddressExists = false; + if (contentValuesList != null) { + final Set addressSet = new HashSet(); + 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); + appendEmailLine(type, label, emailAddress, isPrimary); + } + } + } + + if (!emailAddressExists && mIsDoCoMo) { + appendEmailLine(Email.TYPE_HOME, "", "", false); + } + + return this; + } + + public VCardBuilder appendPostals(final List contentValuesList) { + if (contentValuesList == null || contentValuesList.isEmpty()) { + if (mIsDoCoMo) { + mBuilder.append(VCardConstants.PROPERTY_ADR); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.PARAM_TYPE_HOME); + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + } + } else { + if (mIsDoCoMo) { + appendPostalsForDoCoMo(contentValuesList); + } else { + appendPostalsForGeneric(contentValuesList); + } + } + + return this; + } + + private static final Map sPostalTypePriorityMap; + + static { + sPostalTypePriorityMap = new HashMap(); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); + } + + /** + * Tries to append just one line. If there's no appropriate address + * information, append an empty line. + */ + private void appendPostalsForDoCoMo(final List contentValuesList) { + int currentPriority = Integer.MAX_VALUE; + int currentType = Integer.MAX_VALUE; + ContentValues currentContentValues = null; + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); + final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); + final int priority = + (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); + if (priority < currentPriority) { + currentPriority = priority; + currentType = typeAsInteger; + currentContentValues = contentValues; + if (priority == 0) { + break; + } + } + } + + if (currentContentValues == null) { + Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); + return; + } + + final String label = currentContentValues.getAsString(StructuredPostal.LABEL); + appendPostalLine(currentType, label, currentContentValues, false, true); + } + + private void appendPostalsForGeneric(final List contentValuesList) { + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); + final int type = (typeAsInteger != null ? + typeAsInteger : 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); + appendPostalLine(type, label, contentValues, isPrimary, false); + } + } + + private static 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. + } + + public VCardBuilder appendIms(final List contentValuesList) { + if (contentValuesList != null) { + 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 = VCardConstants.PARAM_TYPE_HOME; + break; + } + case Im.TYPE_WORK: { + typeAsString = VCardConstants.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; + } + } + } + + final List parameterList = new ArrayList(); + 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(VCardConstants.PARAM_TYPE_PREF); + } + + appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); + } + } + return this; + } + + public VCardBuilder appendWebsites(final List contentValuesList) { + if (contentValuesList != null) { + 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)) { + appendLine(VCardConstants.PROPERTY_URL, website); + } + } + } + return this; + } + + public VCardBuilder appendOrganizations(final List contentValuesList) { + 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(); + appendLine(VCardConstants.PROPERTY_ORG, orgline, + !VCardUtils.containsOnlyPrintableAscii(orgline), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); + + if (!TextUtils.isEmpty(title)) { + appendLine(VCardConstants.PROPERTY_TITLE, title, + !VCardUtils.containsOnlyPrintableAscii(title), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); + } + } + } + return this; + } + + public VCardBuilder appendPhotos(final List contentValuesList) { + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + 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 = new String(Base64.encodeBase64(data)); + if (!TextUtils.isEmpty(photoString)) { + appendPhotoLine(photoString, photoType); + } + } + } + return this; + } + + public VCardBuilder appendNotes(final List contentValuesList) { + if (contentValuesList != null) { + if (mOnlyOneNoteFieldIsAvailable) { + StringBuilder noteBuilder = new StringBuilder(); + boolean first = true; + for (final 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)); + appendLine(VCardConstants.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)); + appendLine(VCardConstants.PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } + } + } + } + return this; + } + + public VCardBuilder appendEvents(final List contentValuesList) { + if (contentValuesList != null) { + String primaryBirthday = null; + String secondaryBirthday = null; + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); + final int eventType; + if (eventTypeAsInteger != null) { + eventType = eventTypeAsInteger; + } else { + eventType = Event.TYPE_OTHER; + } + if (eventType == Event.TYPE_BIRTHDAY) { + 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; + } + } else if (mUsesAndroidProperty) { + // Event types other than Birthday is not supported by vCard. + appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); + } + } + if (primaryBirthday != null) { + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, + primaryBirthday.trim()); + } else if (secondaryBirthday != null){ + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, + secondaryBirthday.trim()); + } + } + return this; + } + + public VCardBuilder appendRelation(final List contentValuesList) { + if (mUsesAndroidProperty && contentValuesList != null) { + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); + } + } + return this; + } + + public void appendPostalLine(final int type, final String label, + final ContentValues contentValues, + final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressValue; + { + PostalStruct postalStruct = tryConstructPostalStruct(contentValues); + if (postalStruct == null) { + if (emitLineEveryTime) { + reallyUseQuotedPrintable = false; + appendCharset = false; + addressValue = ""; + } else { + return; + } + } else { + reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; + appendCharset = postalStruct.appendCharset; + addressValue = postalStruct.addressData; + } + } + + List parameterList = new ArrayList(); + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + switch (type) { + case StructuredPostal.TYPE_HOME: { + parameterList.add(VCardConstants.PARAM_TYPE_HOME); + break; + } + case StructuredPostal.TYPE_WORK: { + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + break; + } + case StructuredPostal.TYPE_CUSTOM: { + if (!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; + } + } + + mBuilder.append(VCardConstants.PROPERTY_ADR); + if (!parameterList.isEmpty()) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + 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. + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(addressValue); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendEmailLine(final int type, final String label, + final String rawValue, final boolean isPrimary) { + final String typeAsString; + switch (type) { + case Email.TYPE_CUSTOM: { + if (VCardUtils.isMobilePhoneLabel(label)) { + typeAsString = VCardConstants.PARAM_TYPE_CELL; + } else if (!TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + typeAsString = "X-" + label; + } else { + typeAsString = null; + } + break; + } + case Email.TYPE_HOME: { + typeAsString = VCardConstants.PARAM_TYPE_HOME; + break; + } + case Email.TYPE_WORK: { + typeAsString = VCardConstants.PARAM_TYPE_WORK; + break; + } + case Email.TYPE_OTHER: { + typeAsString = null; + break; + } + case Email.TYPE_MOBILE: { + typeAsString = VCardConstants.PARAM_TYPE_CELL; + break; + } + default: { + Log.e(LOG_TAG, "Unknown Email type: " + type); + typeAsString = null; + break; + } + } + + final List parameterList = new ArrayList(); + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, + rawValue); + } + + public void appendTelLine(final Integer typeAsInteger, final String label, + final String encodedValue, boolean isPrimary) { + mBuilder.append(VCardConstants.PROPERTY_TEL); + mBuilder.append(VCARD_PARAM_SEPARATOR); + + final int type; + if (typeAsInteger == null) { + type = Phone.TYPE_OTHER; + } else { + type = typeAsInteger; + } + + ArrayList parameterList = new ArrayList(); + switch (type) { + case Phone.TYPE_HOME: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); + break; + } + case Phone.TYPE_WORK: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); + break; + } + case Phone.TYPE_FAX_HOME: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); + break; + } + case Phone.TYPE_FAX_WORK: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); + break; + } + case Phone.TYPE_MOBILE: { + parameterList.add(VCardConstants.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(VCardConstants.PARAM_TYPE_VOICE); + } else { + parameterList.add(VCardConstants.PARAM_TYPE_PAGER); + } + break; + } + case Phone.TYPE_OTHER: { + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + break; + } + case Phone.TYPE_CAR: { + parameterList.add(VCardConstants.PARAM_TYPE_CAR); + break; + } + case Phone.TYPE_COMPANY_MAIN: { + // There's no relevant field in vCard (at least 2.1). + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + isPrimary = true; + break; + } + case Phone.TYPE_ISDN: { + parameterList.add(VCardConstants.PARAM_TYPE_ISDN); + break; + } + case Phone.TYPE_MAIN: { + isPrimary = true; + break; + } + case Phone.TYPE_OTHER_FAX: { + parameterList.add(VCardConstants.PARAM_TYPE_FAX); + break; + } + case Phone.TYPE_TELEX: { + parameterList.add(VCardConstants.PARAM_TYPE_TLX); + break; + } + case Phone.TYPE_WORK_MOBILE: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); + break; + } + case Phone.TYPE_WORK_PAGER: { + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + // See above. + if (mIsDoCoMo) { + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + } else { + parameterList.add(VCardConstants.PARAM_TYPE_PAGER); + } + break; + } + case Phone.TYPE_MMS: { + parameterList.add(VCardConstants.PARAM_TYPE_MSG); + break; + } + case Phone.TYPE_CUSTOM: { + if (TextUtils.isEmpty(label)) { + // Just ignore the custom type. + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + } else if (VCardUtils.isMobilePhoneLabel(label)) { + parameterList.add(VCardConstants.PARAM_TYPE_CELL); + } else { + final String upperLabel = label.toUpperCase(); + if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { + parameterList.add(upperLabel); + } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + // Note: Strictly, vCard 2.1 does not allow "X-" parameter without + // "TYPE=" string. + parameterList.add("X-" + label); + } + } + break; + } + case Phone.TYPE_RADIO: + case Phone.TYPE_TTY_TDD: + default: { + break; + } + } + + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + + if (parameterList.isEmpty()) { + appendUncommonPhoneType(mBuilder, type); + } else { + appendTypeParameters(parameterList); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedValue); + mBuilder.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(VCardConstants.PARAM_TYPE_VOICE); + } else { + String phoneType = VCardUtils.getPhoneTypeString(type); + if (phoneType != null) { + appendTypeParameter(phoneType); + } else { + Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); + } + } + } + + /** + * @param encodedValue Must be encoded by BASE64 + * @param photoType + */ + public void appendPhotoLine(final String encodedValue, final String photoType) { + StringBuilder tmpBuilder = new StringBuilder(); + tmpBuilder.append(VCardConstants.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(encodedValue); + + final String tmpStr = tmpBuilder.toString(); + tmpBuilder = new StringBuilder(); + int lineCount = 0; + final int length = tmpStr.length(); + final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 + - VCARD_END_OF_LINE.length(); + final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); + int maxNum = maxNumForFirstLine; + for (int i = 0; i < length; i++) { + tmpBuilder.append(tmpStr.charAt(i)); + lineCount++; + if (lineCount > maxNum) { + tmpBuilder.append(VCARD_END_OF_LINE); + tmpBuilder.append(VCARD_WS); + maxNum = maxNumInGeneral; + lineCount = 0; + } + } + mBuilder.append(tmpBuilder.toString()); + mBuilder.append(VCARD_END_OF_LINE); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { + List rawValueList = new ArrayList(); + rawValueList.add(mimeType); + final List columnNameList; + if (!sAllowedAndroidPropertySet.contains(mimeType)) { + return; + } + + for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { + String value = contentValues.getAsString("data" + i); + if (value == null) { + value = ""; + } + rawValueList.add(value); + } + + appendLineWithCharsetAndQPDetection( + VCardConstants.PROPERTY_X_ANDROID_CUSTOM, rawValueList); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final String rawValue) { + appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); + } + + private void appendLineWithCharsetAndQPDetection( + final String propertyName, final List rawValueList) { + appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final List parameterList, final String rawValue) { + final boolean needCharset = + (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawValue)); + final boolean reallyUseQuotedPrintable = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue); + appendLine(propertyName, parameterList, + rawValue, needCharset, reallyUseQuotedPrintable); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final List parameterList, final List rawValueList) { + boolean needCharset = false; + boolean reallyUseQuotedPrintable = false; + for (String rawValue : rawValueList) { + if (!needCharset && mUsesQuotedPrintable && + !VCardUtils.containsOnlyPrintableAscii(rawValue)) { + needCharset = true; + } + if (!reallyUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)) { + reallyUseQuotedPrintable = true; + } + if (needCharset && reallyUseQuotedPrintable) { + break; + } + } + + appendLine(propertyName, parameterList, rawValueList, + needCharset, reallyUseQuotedPrintable); + } + + /** + * Appends one line with a given property name and value. + */ + public void appendLine(final String propertyName, final String rawValue) { + appendLine(propertyName, rawValue, false, false); + } + + public void appendLine(final String propertyName, final List rawValueList) { + appendLine(propertyName, rawValueList, false, false); + } + + public void appendLine(final String propertyName, + final String rawValue, final boolean needCharset, boolean needQuotedPrintable) { + appendLine(propertyName, null, rawValue, needCharset, needQuotedPrintable); + } + + public void appendLine(final String propertyName, final List parameterList, + final String rawValue) { + appendLine(propertyName, parameterList, rawValue, false, false); + } + + public void appendLine(final String propertyName, final List parameterList, + final String rawValue, final boolean needCharset, + boolean needQuotedPrintable) { + mBuilder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + if (needCharset) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + + final String encodedValue; + if (needQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + encodedValue = encodeQuotedPrintable(rawValue); + } 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. + encodedValue = escapeCharacters(rawValue); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedValue); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendLine(final String propertyName, final List rawValueList, + final boolean needCharset, boolean needQuotedPrintable) { + appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); + } + + public void appendLine(final String propertyName, final List parameterList, + final List rawValueList, final boolean needCharset, + final boolean needQuotedPrintable) { + mBuilder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + if (needCharset) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + boolean first = true; + for (String rawValue : rawValueList) { + final String encodedValue; + if (needQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + encodedValue = encodeQuotedPrintable(rawValue); + } else { + // TODO: one line may be too huge, which may be invalid in vCard 3.0 + // (which says "When generating a content line, lines longer than + // 75 characters SHOULD be folded"), though several + // (even well-known) applications do not care this. + encodedValue = escapeCharacters(rawValue); + } + + if (first) { + first = false; + } else { + mBuilder.append(VCARD_ITEM_SEPARATOR); + } + mBuilder.append(encodedValue); + } + mBuilder.append(VCARD_END_OF_LINE); + } + + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameters(final List 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 { + mBuilder.append(VCARD_PARAM_SEPARATOR); + } + appendTypeParameter(type); + } + } + + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameter(final String type) { + appendTypeParameter(mBuilder, type); + } + + 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(VCardConstants.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 shouldAppendCharsetParam(String...propertyValueList) { + if (!mShouldAppendCharsetParam) { + 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". + final 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(); + } + + + /** + * 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(); + } + + @Override + public String toString() { + if (!mEndAppended) { + if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); + appendLine(VCardConstants.PROPERTY_X_NO, ""); + appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); + } + appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); + mEndAppended = true; + } + return mBuilder.toString(); + } +} diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index d9f38bbf04c62..c2d628feb6114 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -38,17 +38,15 @@ import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; -import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.text.format.Time; import android.util.CharsetUtils; import android.util.Log; -import org.apache.commons.codec.binary.Base64; - import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; @@ -131,21 +129,6 @@ public class VCardComposer { 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"; @@ -171,19 +154,19 @@ public class VCardComposer { builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); sDataRequestUri = builder.build(); sImMap = new HashMap(); - 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); + sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); // Google talk is a special case. // TODO: incomplete. Implement properly sPrimaryPropertyNameSet = new HashSet(); - sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); - sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); - sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); + sPrimaryPropertyNameSet.add(VCardConstants.PROPERTY_N); + sPrimaryPropertyNameSet.add(VCardConstants.PROPERTY_FN); + sPrimaryPropertyNameSet.add(VCardConstants.PROPERTY_SOUND); } public static interface OneEntryHandler { @@ -294,26 +277,12 @@ public class VCardComposer { 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 mHandlerList; @@ -361,18 +330,8 @@ public class VCardComposer { 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(); if (mIsDoCoMo) { @@ -384,10 +343,6 @@ public class VCardComposer { 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 { @@ -397,10 +352,8 @@ public class VCardComposer { charset = SHIFT_JIS; } mCharsetString = charset; - mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else { mCharsetString = UTF_8; - mVCardCharsetParameter = "CHARSET=" + UTF_8; } } @@ -583,36 +536,19 @@ public class VCardComposer { 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); - + final VCardBuilder builder = new VCardBuilder(mVCardType); + builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); return builder.toString(); } @@ -625,8 +561,7 @@ public class VCardComposer { try { mCursor.close(); } catch (SQLiteException e) { - Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " - + e.getMessage()); + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); } mCursor = null; } @@ -662,1752 +597,27 @@ public class VCardComposer { return mErrorReason; } - private void appendStructuredNames(final StringBuilder builder, - final Map> contentValuesListMap) { - final List 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(final 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 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. - final 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; - { - final String tmpPhoneticFamilyName = - primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); - final String tmpPhoneticMiddleName = - primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - final 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))) { - // Try to emit the field(s) related to phonetic name. - 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. - // - // Also, DoCoMo's specification requires vCard composer to use just the first - // column. - // i.e. - // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; - // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; - 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); - { - 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 phonetic name fields are all empty - 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> contentValuesListMap) { - final List 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> contentValuesListMap) { - final List contentValuesList = contentValuesListMap - .get(Phone.CONTENT_ITEM_TYPE); - boolean phoneLineExists = false; - if (contentValuesList != null) { - Set phoneSet = new HashSet(); - 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 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 splitIfSeveralPhoneNumbersExist(final String phoneNumber) { - List phoneList = new ArrayList(); - - 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> contentValuesListMap) { - final List contentValuesList = contentValuesListMap - .get(Email.CONTENT_ITEM_TYPE); - - boolean emailAddressExists = false; - if (contentValuesList != null) { - final Set addressSet = new HashSet(); - 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> contentValuesListMap) { - final List 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); - } - } - - private static final Map sPostalTypePriorityMap; - - static { - sPostalTypePriorityMap = new HashMap(); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); - sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); - } - - /** - * 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 contentValuesList) { - int currentPriority = Integer.MAX_VALUE; - int currentType = Integer.MAX_VALUE; - ContentValues currentContentValues = null; - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); - final int priority = - (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); - if (priority < currentPriority) { - currentPriority = priority; - currentType = typeAsInteger; - currentContentValues = contentValues; - if (priority == 0) { - break; - } - } - } - - if (currentContentValues == null) { - Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); - return; - } - - final String label = currentContentValues.getAsString(StructuredPostal.LABEL); - appendVCardPostalLine(builder, currentType, label, currentContentValues, false, true); - } - - private void appendPostalsForGeneric(final StringBuilder builder, - final List contentValuesList) { - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null) { - continue; - } - final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); - final int type = (typeAsInteger != null ? - typeAsInteger : 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> contentValuesListMap) { - final List 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 parameterList = new ArrayList(); - 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> contentValuesListMap) { - final List 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> contentValuesListMap) { - final List 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> contentValuesListMap) { - final List 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> contentValuesListMap) { - final List 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 = new String(Base64.encodeBase64(data)); - if (!TextUtils.isEmpty(photoString)) { - appendVCardPhotoLine(builder, photoString, photoType); - } - } - } - } - - private void appendNotes(final StringBuilder builder, - final Map> contentValuesListMap) { - final List 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 rawDataList = new ArrayList(); - rawDataList.add(mimeType); - final List 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 parameterList = new ArrayList(); - 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 (!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. - - builder.append(Constants.PROPERTY_ADR); - - // Parameters - { - if (!parameterList.isEmpty()) { - builder.append(VCARD_PARAM_SEPARATOR); - appendTypeParameters(builder, parameterList); - } - - 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. - 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(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: { - if (VCardUtils.isMobilePhoneLabel(label)) { - typeAsString = Constants.PARAM_TYPE_CELL; - } else if (!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 parameterList = new ArrayList(); - 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 typeAsInteger, final String label, - final String encodedData, boolean isPrimary) { - builder.append(Constants.PROPERTY_TEL); - builder.append(VCARD_PARAM_SEPARATOR); - - final int type; - if (typeAsInteger == null) { - type = Phone.TYPE_OTHER; - } else { - type = typeAsInteger; - } - - ArrayList parameterList = new ArrayList(); - switch (type) { - 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 (TextUtils.isEmpty(label)) { - // Just ignore the custom type. - parameterList.add(Constants.PARAM_TYPE_VOICE); - } else if (VCardUtils.isMobilePhoneLabel(label)) { - parameterList.add(Constants.PARAM_TYPE_CELL); - } else { - final String upperLabel = label.toUpperCase(); - if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { - parameterList.add(upperLabel); - } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - // Note: Strictly, vCard 2.1 does not allow "X-" parameter without - // "TYPE=" string. - parameterList.add("X-" + label); - } - } - 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, type); - } 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 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 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. - - private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, - final String propertyName, final List rawDataList) { - appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList); - } - - private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, - final String propertyName, - final List parameterList, final List 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 rawDataList) { - appendVCardLine(builder, propertyName, rawDataList, false, false); - } - - private void appendVCardLine(final StringBuilder builder, - final String propertyName, final List rawDataList, - final boolean needCharset, boolean needQuotedPrintable) { - appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable); - }*/ - - private void appendVCardLine(final StringBuilder builder, - final String propertyName, - final List parameterList, - final List rawDataList, final boolean needCharset, - final 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 3.0 - // (which says "When generating a content line, lines longer than - // 75 characters SHOULD be folded"), 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 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 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". - final 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); - } - + final int vcardType = (vcardVer21 ? + VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 : + VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); + final VCardBuilder builder = new VCardBuilder(vcardType); 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); + builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false); + builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false); if (!TextUtils.isEmpty(phoneNumber)) { String label = Integer.toString(phonetype); - appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); + builder.appendTelLine(phonetype, label, phoneNumber, false); } - appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); - return builder.toString(); } @@ -2426,7 +636,7 @@ public class VCardComposer { * 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) { + private void tryAppendCallHistoryTimeStampField(final VCardBuilder 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 @@ -2457,39 +667,28 @@ public class VCardComposer { } 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); + builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP, + Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong)); } 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); - } + final VCardBuilder builder = new VCardBuilder(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); 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); + builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false); + builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false); - String number = mCursor.getString(NUMBER_COLUMN_INDEX); - int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + final String number = mCursor.getString(NUMBER_COLUMN_INDEX); + final 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); + builder.appendTelLine(type, label, number, false); tryAppendCallHistoryTimeStampField(builder); - appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); return builder.toString(); } } diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/VCardConstants.java similarity index 99% rename from core/java/android/pim/vcard/Constants.java rename to core/java/android/pim/vcard/VCardConstants.java index 9e4b13a848567..8c07126d56bd2 100644 --- a/core/java/android/pim/vcard/Constants.java +++ b/core/java/android/pim/vcard/VCardConstants.java @@ -18,8 +18,7 @@ package android.pim.vcard; /** * Constants used in both exporter and importer code. */ -/* package */ class Constants { - +public class VCardConstants { public static final String VERSION_V21 = "2.1"; public static final String VERSION_V30 = "3.0"; @@ -148,6 +147,6 @@ package android.pim.vcard; /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75; - private Constants() { + private VCardConstants() { } } \ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java index c1b4c03da6860..a6e432d734693 100644 --- a/core/java/android/pim/vcard/VCardEntry.java +++ b/core/java/android/pim/vcard/VCardEntry.java @@ -65,14 +65,14 @@ public class VCardEntry { private static final Map sImMap = new HashMap(); static { - sImMap.put(Constants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); - sImMap.put(Constants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); - sImMap.put(Constants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); - sImMap.put(Constants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); - sImMap.put(Constants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); - sImMap.put(Constants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); - sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); - sImMap.put(Constants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, + sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); + sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); + sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); + sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); + sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); + sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); + sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); } @@ -749,24 +749,25 @@ public class VCardEntry { } final String propValue = listToString(propValueList).trim(); - if (propName.equals(Constants.PROPERTY_VERSION)) { + if (propName.equals(VCardConstants.PROPERTY_VERSION)) { // vCard version. Ignore this. - } else if (propName.equals(Constants.PROPERTY_FN)) { + } else if (propName.equals(VCardConstants.PROPERTY_FN)) { mFullName = propValue; - } else if (propName.equals(Constants.PROPERTY_NAME) && mFullName == null) { + } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) { // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not // actually exist in the real vCard data, does not exist. mFullName = propValue; - } else if (propName.equals(Constants.PROPERTY_N)) { + } else if (propName.equals(VCardConstants.PROPERTY_N)) { handleNProperty(propValueList); - } else if (propName.equals(Constants.PROPERTY_SORT_STRING)) { + } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { mPhoneticFullName = propValue; - } else if (propName.equals(Constants.PROPERTY_NICKNAME) || - propName.equals(Constants.ImportOnly.PROPERTY_X_NICKNAME)) { + } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || + propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { addNickName(propValue); - } else if (propName.equals(Constants.PROPERTY_SOUND)) { - Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); - if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_X_IRMC_N)) { + } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) { + Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); + if (typeCollection != null + && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { // As of 2009-10-08, Parser side does not split a property value into separated // values using ';' (in other words, propValueList.size() == 1), // which is correct behavior from the view of vCard 2.1. @@ -778,7 +779,7 @@ public class VCardEntry { } else { // Ignore this field since Android cannot understand what it is. } - } else if (propName.equals(Constants.PROPERTY_ADR)) { + } else if (propName.equals(VCardConstants.PROPERTY_ADR)) { boolean valuesAreAllEmpty = true; for (String value : propValueList) { if (value.length() > 0) { @@ -793,25 +794,25 @@ public class VCardEntry { int type = -1; String label = ""; boolean isPrimary = false; - Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.PARAM_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { type = StructuredPostal.TYPE_HOME; label = ""; - } else if (typeString.equals(Constants.PARAM_TYPE_WORK) || - typeString.equalsIgnoreCase(Constants.PARAM_EXTRA_TYPE_COMPANY)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) || + typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { // "COMPANY" seems emitted by Windows Mobile, which is not // specifically supported by vCard 2.1. We assume this is same // as "WORK". type = StructuredPostal.TYPE_WORK; label = ""; - } else if (typeString.equals(Constants.PARAM_ADR_TYPE_PARCEL) || - typeString.equals(Constants.PARAM_ADR_TYPE_DOM) || - typeString.equals(Constants.PARAM_ADR_TYPE_INTL)) { + } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) || + typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) || + typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { // We do not have any appropriate way to store this information. } else { if (typeString.startsWith("X-") && type < 0) { @@ -830,21 +831,21 @@ public class VCardEntry { } addPostal(type, propValueList, label, isPrimary); - } else if (propName.equals(Constants.PROPERTY_EMAIL)) { + } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) { int type = -1; String label = null; boolean isPrimary = false; - Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.PARAM_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { type = Email.TYPE_HOME; - } else if (typeString.equals(Constants.PARAM_TYPE_WORK)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) { type = Email.TYPE_WORK; - } else if (typeString.equals(Constants.PARAM_TYPE_CELL)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) { type = Email.TYPE_MOBILE; } else { if (typeString.startsWith("X-") && type < 0) { @@ -862,26 +863,26 @@ public class VCardEntry { type = Email.TYPE_OTHER; } addEmail(type, propValue, label, isPrimary); - } else if (propName.equals(Constants.PROPERTY_ORG)) { + } else if (propName.equals(VCardConstants.PROPERTY_ORG)) { // vCard specification does not specify other types. final int type = Organization.TYPE_WORK; boolean isPrimary = false; - Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.PARAM_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } } } handleOrgValue(type, propValueList, isPrimary); - } else if (propName.equals(Constants.PROPERTY_TITLE)) { + } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { handleTitleValue(propValue); - } else if (propName.equals(Constants.PROPERTY_ROLE)) { + } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { // This conflicts with TITLE. Ignore for now... // handleTitleValue(propValue); - } else if (propName.equals(Constants.PROPERTY_PHOTO) || - propName.equals(Constants.PROPERTY_LOGO)) { + } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) || + propName.equals(VCardConstants.PROPERTY_LOGO)) { Collection paramMapValue = paramMap.get("VALUE"); if (paramMapValue != null && paramMapValue.contains("URL")) { // Currently we do not have appropriate example for testing this case. @@ -891,7 +892,7 @@ public class VCardEntry { boolean isPrimary = false; if (typeCollection != null) { for (String typeValue : typeCollection) { - if (Constants.PARAM_TYPE_PREF.equals(typeValue)) { + if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { isPrimary = true; } else if (formatName == null){ formatName = typeValue; @@ -900,8 +901,8 @@ public class VCardEntry { } addPhotoBytes(formatName, propBytes, isPrimary); } - } else if (propName.equals(Constants.PROPERTY_TEL)) { - final Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + } else if (propName.equals(VCardConstants.PROPERTY_TEL)) { + final Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); final int type; @@ -915,18 +916,18 @@ public class VCardEntry { } final boolean isPrimary; - if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_PREF)) { + if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; } addPhone(type, propValue, label, isPrimary); - } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { // The phone number available via Skype. - Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); final int type = Phone.TYPE_OTHER; final boolean isPrimary; - if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_PREF)) { + if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; @@ -936,15 +937,15 @@ public class VCardEntry { final int protocol = sImMap.get(propName); boolean isPrimary = false; int type = -1; - final Collection typeCollection = paramMap.get(Constants.PARAM_TYPE); + final Collection typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.PARAM_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } else if (type < 0) { - if (typeString.equalsIgnoreCase(Constants.PARAM_TYPE_HOME)) { + if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { type = Im.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(Constants.PARAM_TYPE_WORK)) { + } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { type = Im.TYPE_WORK; } } @@ -954,22 +955,22 @@ public class VCardEntry { type = Phone.TYPE_HOME; } addIm(protocol, null, type, propValue, isPrimary); - } else if (propName.equals(Constants.PROPERTY_NOTE)) { + } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) { addNote(propValue); - } else if (propName.equals(Constants.PROPERTY_URL)) { + } else if (propName.equals(VCardConstants.PROPERTY_URL)) { if (mWebsiteList == null) { mWebsiteList = new ArrayList(1); } mWebsiteList.add(propValue); - } else if (propName.equals(Constants.PROPERTY_BDAY)) { + } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { mBirthday = propValue; - } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_FIRST_NAME)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { mPhoneticGivenName = propValue; - } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { mPhoneticMiddleName = propValue; - } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_LAST_NAME)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { mPhoneticFamilyName = propValue; - } else if (propName.equals(Constants.PROPERTY_X_ANDROID_CUSTOM)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { final List customPropertyList = VCardUtils.constructListFromValue(propValue, VCardConfig.isV30(mVCardType)); @@ -1247,10 +1248,10 @@ public class VCardEntry { int size = customPropertyList.size(); if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { continue; - } else if (size > Constants.MAX_DATA_COLUMN + 1) { - size = Constants.MAX_DATA_COLUMN + 1; + } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) { + size = VCardConstants.MAX_DATA_COLUMN + 1; customPropertyList = - customPropertyList.subList(0, Constants.MAX_DATA_COLUMN + 2); + customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2); } int i = 0; diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index 3bbf698f26359..0dea972db0490 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -178,7 +178,7 @@ public class VCardParser_V21 extends VCardParser { } protected String getVersionString() { - return Constants.VERSION_V21; + return VCardConstants.VERSION_V21; } /** diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 3dd467caee54d..7314b87d8f857 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -84,7 +84,7 @@ public class VCardParser_V30 extends VCardParser_V21 { @Override protected String getVersionString() { - return Constants.VERSION_V30; + return VCardConstants.VERSION_V30; } @Override diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index 12a8e424abbe1..45c0172288f04 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -50,41 +50,44 @@ public class VCardUtils { sKnownPhoneTypesMap_ItoS = new HashMap(); sKnownPhoneTypeMap_StoI = new HashMap(); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.PARAM_TYPE_CAR); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_CAR, Phone.TYPE_CAR); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.PARAM_TYPE_PAGER); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.PARAM_TYPE_ISDN); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, + Phone.TYPE_CALLBACK); sKnownPhoneTypeMap_StoI.put( - Constants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD); - sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, + VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, + Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, Phone.TYPE_ASSISTANT); sPhoneTypesUnknownToContactsSet = new HashSet(); - sPhoneTypesUnknownToContactsSet.add(Constants.PARAM_TYPE_MODEM); - sPhoneTypesUnknownToContactsSet.add(Constants.PARAM_TYPE_BBS); - sPhoneTypesUnknownToContactsSet.add(Constants.PARAM_TYPE_VIDEO); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); sKnownImPropNameMap_ItoS = new HashMap(); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, Constants.PROPERTY_X_GOOGLE_TALK); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, Constants.PROPERTY_X_QQ); - sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, Constants.PROPERTY_X_NETMEETING); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, + VCardConstants.PROPERTY_X_GOOGLE_TALK); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) @@ -119,9 +122,9 @@ public class VCardUtils { continue; } typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.PARAM_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { hasPref = true; - } else if (typeString.equals(Constants.PARAM_TYPE_FAX)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) { isFax = true; } else { if (typeString.startsWith("X-") && type < 0) { diff --git a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java index c1727ded7c8e1..dcdc167dc9dcd 100644 --- a/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java +++ b/tests/AndroidTests/src/com/android/unit_tests/vcard/VCardExporterTests.java @@ -26,6 +26,7 @@ import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; @@ -52,12 +53,13 @@ public class VCardExporterTests extends VCardTestsBase { VCardVerifier verifier = new VCardVerifier(resolver, V21); verifier.addPropertyNodesVerifierElem() .addNodeWithoutOrder("FN", "Roid Ando") - .addNodeWithoutOrder("N", "Ando;Roid;;;", Arrays.asList("Ando", "Roid", "", "", "")); + .addNodeWithoutOrder("N", "Ando;Roid;;;", + Arrays.asList("Ando", "Roid", "", "", "")); verifier.verify(); } - private void testStructuredNameBasic(int version) { - final boolean isV30 = VCardConfig.isV30(version); + private void testStructuredNameBasic(int vcardType) { + final boolean isV30 = VCardConfig.isV30(vcardType); ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) @@ -70,7 +72,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredName.PHONETIC_GIVEN_NAME, "AppropriatePhoneticGiven") .put(StructuredName.PHONETIC_MIDDLE_NAME, "AppropriatePhoneticMiddle"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() .addNodeWithOrder("N", "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" @@ -107,8 +109,8 @@ public class VCardExporterTests extends VCardTestsBase { * which presume that there's only one property toward each of "N", "FN", etc. * Note that more than one "N", "FN", etc. properties are acceptable in vCard spec. */ - private void testStructuredNameUsePrimaryCommon(int version) { - final boolean isV30 = (version == V30); + private void testStructuredNameUsePrimaryCommon(int vcardType) { + final boolean isV30 = (vcardType == V30); ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -146,7 +148,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle2") .put(StructuredName.IS_PRIMARY, 1); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() .addNodeWithOrder("N", "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" @@ -181,8 +183,8 @@ public class VCardExporterTests extends VCardTestsBase { * Tests that only "super primary" StructuredName is emitted. * See also the comment in {@link #testStructuredNameUsePrimaryCommon(int)}. */ - private void testStructuredNameUseSuperPrimaryCommon(int version) { - final boolean isV30 = (version == V30); + private void testStructuredNameUseSuperPrimaryCommon(int vcardType) { + final boolean isV30 = (vcardType == V30); ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -231,7 +233,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredName.PHONETIC_MIDDLE_NAME, "DoNotEmitPhoneticMiddle3") .put(StructuredName.IS_PRIMARY, 1); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); PropertyNodesVerifierElem elem = verifier.addPropertyNodesVerifierElem() .addNodeWithOrder("N", "AppropriateFamilyName;AppropriateGivenName;AppropriateMiddleName;" @@ -274,14 +276,14 @@ public class VCardExporterTests extends VCardTestsBase { verifier.verify(); } - private void testPhoneBasicCommon(int version) { + private void testPhoneBasicCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(Phone.CONTENT_ITEM_TYPE) .put(Phone.NUMBER, "1") .put(Phone.TYPE, Phone.TYPE_HOME); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("TEL", "1", new TypeSet("HOME")); @@ -299,7 +301,7 @@ public class VCardExporterTests extends VCardTestsBase { /** * Tests that vCard composer emits corresponding type param which we expect. */ - private void testPhoneVariousTypeSupport(int version) { + private void testPhoneVariousTypeSupport(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -352,7 +354,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Phone.NUMBER, "160") .put(Phone.TYPE, Phone.TYPE_MMS); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("TEL", "10", new TypeSet("HOME")) .addNodeWithoutOrder("TEL", "20", new TypeSet("WORK")) @@ -384,7 +386,7 @@ public class VCardExporterTests extends VCardTestsBase { /** * Tests that "PREF"s are emitted appropriately. */ - private void testPhonePrefHandlingCommon(int version) { + private void testPhonePrefHandlingCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); entry.buildData(Phone.CONTENT_ITEM_TYPE) @@ -402,7 +404,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Phone.NUMBER, "4") .put(Phone.TYPE, Phone.TYPE_FAX_WORK); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("TEL", "4", new TypeSet("WORK", "FAX")) .addNodeWithoutOrder("TEL", "3", new TypeSet("HOME", "FAX", "PREF")) @@ -475,12 +477,12 @@ public class VCardExporterTests extends VCardTestsBase { testMiscPhoneTypeHandling(V30); } - private void testEmailBasicCommon(int version) { + private void testEmailBasicCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(Email.CONTENT_ITEM_TYPE) .put(Email.DATA, "sample@example.com"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("EMAIL", "sample@example.com"); @@ -496,7 +498,7 @@ public class VCardExporterTests extends VCardTestsBase { testEmailBasicCommon(V30); } - private void testEmailVariousTypeSupportCommon(int version) { + private void testEmailVariousTypeSupportCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -513,7 +515,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Email.DATA, "type_other@example.com") .put(Email.TYPE, Email.TYPE_OTHER); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("EMAIL", "type_home@example.com", new TypeSet("HOME")) @@ -532,7 +534,7 @@ public class VCardExporterTests extends VCardTestsBase { testEmailVariousTypeSupportCommon(V30); } - private void testEmailPrefHandlingCommon(int version) { + private void testEmailPrefHandlingCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -544,7 +546,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Email.DATA, "type_notype@example.com") .put(Email.IS_PRIMARY, 1); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("EMAIL", "type_notype@example.com", new TypeSet("PREF")) @@ -561,7 +563,7 @@ public class VCardExporterTests extends VCardTestsBase { testEmailPrefHandlingCommon(V30); } - private void testPostalOnlyWithStructuredDataCommon(int version) { + private void testPostalOnlyWithStructuredDataCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); // adr-value = 0*6(text-value ";") text-value @@ -575,7 +577,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredPostal.REGION, "Region") .put(StructuredPostal.POSTCODE, "100") .put(StructuredPostal.COUNTRY, "Country"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("ADR", "Pobox;Neighborhood;Street;City;Region;100;Country", Arrays.asList("Pobox", "Neighborhood", "Street", "City", @@ -592,14 +594,14 @@ public class VCardExporterTests extends VCardTestsBase { testPostalOnlyWithStructuredDataCommon(V30); } - private void testPostalOnlyWithFormattedAddressCommon(int version) { + private void testPostalOnlyWithFormattedAddressCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(StructuredPostal.CONTENT_ITEM_TYPE) .put(StructuredPostal.FORMATTED_ADDRESS, "Formatted address CA 123-334 United Statue"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithOrder("ADR", ";Formatted address CA 123-334 United Statue;;;;;", Arrays.asList("", "Formatted address CA 123-334 United Statue", @@ -620,7 +622,7 @@ public class VCardExporterTests extends VCardTestsBase { * Tests that the vCard composer honors formatted data when it is available * even when it is partial. */ - private void testPostalWithBothStructuredAndFormattedCommon(int version) { + private void testPostalWithBothStructuredAndFormattedCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(StructuredPostal.CONTENT_ITEM_TYPE) @@ -629,7 +631,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredPostal.FORMATTED_ADDRESS, "Formatted address CA 123-334 United Statue"); // Should be ignored - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("ADR", "Pobox;;;;;;Country", Arrays.asList("Pobox", "", "", "", "", "", "Country"), @@ -646,7 +648,7 @@ public class VCardExporterTests extends VCardTestsBase { testPostalWithBothStructuredAndFormattedCommon(V30); } - private void testOrganizationCommon(int version) { + private void testOrganizationCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); entry.buildData(Organization.CONTENT_ITEM_TYPE) @@ -668,7 +670,7 @@ public class VCardExporterTests extends VCardTestsBase { .putNull(Organization.DEPARTMENT) .put(Organization.TITLE, "TitleXYZYX"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); // Currently we do not use group but depend on the order. verifier.addPropertyNodesVerifierElemWithEmptyName() @@ -690,7 +692,7 @@ public class VCardExporterTests extends VCardTestsBase { testOrganizationCommon(V30); } - private void testImVariousTypeSupportCommon(int version) { + private void testImVariousTypeSupportCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -732,7 +734,7 @@ public class VCardExporterTests extends VCardTestsBase { // No determined way to express unknown type... - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("X-JABBER", "jabber") .addNodeWithoutOrder("X-ICQ", "icq") @@ -755,7 +757,7 @@ public class VCardExporterTests extends VCardTestsBase { testImVariousTypeSupportCommon(V30); } - private void testImPrefHandlingCommon(int version) { + private void testImPrefHandlingCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -769,7 +771,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Im.TYPE, Im.TYPE_HOME) .put(Im.IS_PRIMARY, 1); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("X-AIM", "aim1") .addNodeWithoutOrder("X-AIM", "aim2", new TypeSet("HOME", "PREF")); @@ -785,7 +787,7 @@ public class VCardExporterTests extends VCardTestsBase { testImPrefHandlingCommon(V30); } - private void testWebsiteCommon(int version) { + private void testWebsiteCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -798,7 +800,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Website.TYPE, Website.TYPE_FTP); // We drop TYPE information since vCard (especially 3.0) does not allow us to emit it. - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithoutOrder("URL", "ftp://ftp.example.android.com/index.html") .addNodeWithoutOrder("URL", "http://website.example.android.com/index.html"); @@ -813,7 +815,19 @@ public class VCardExporterTests extends VCardTestsBase { testWebsiteCommon(V30); } - private void testEventCommon(int version) { + private String getAndroidPropValue(final String mimeType, String value, + Integer type) { + return getAndroidPropValue(mimeType, value, type, null); + } + + private String getAndroidPropValue(final String mimeType, String value, + Integer type, String label) { + return (mimeType + ";" + value + ";" + + (type != null ? type : "") + ";" + + (label != null ? label : "") + ";;;;;;;;;;;;"); + } + + private void testEventCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -832,12 +846,22 @@ public class VCardExporterTests extends VCardTestsBase { .put(Event.START_DATE, "When the Tower of Hanoi with 64 rings is completed."); entry.buildData(Event.CONTENT_ITEM_TYPE) .put(Event.TYPE, Event.TYPE_BIRTHDAY) - .put(Event.START_DATE, "2009-05-19"); + .put(Event.START_DATE, "2009-05-19"); // Should be ignored. - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() - .addNodeWithoutOrder("BDAY", "2008-10-22"); - + .addNodeWithoutOrder("BDAY", "2008-10-22") + .addNodeWithoutOrder("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, "1982-06-16", Event.TYPE_ANNIVERSARY)) + .addNodeWithoutOrder("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, "2018-03-12", Event.TYPE_OTHER)) + .addNodeWithoutOrder("X-ANDROID-CUSTOM", + getAndroidPropValue( + Event.CONTENT_ITEM_TYPE, + "When the Tower of Hanoi with 64 rings is completed.", + Event.TYPE_CUSTOM, "The last day")); verifier.verify(); } @@ -849,7 +873,7 @@ public class VCardExporterTests extends VCardTestsBase { testEventCommon(V30); } - private void testNoteCommon(int version) { + private void testNoteCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); @@ -859,7 +883,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(Note.NOTE, "note2") .put(Note.IS_PRIMARY, 1); // Just ignored. - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElemWithEmptyName() .addNodeWithOrder("NOTE", "note1") .addNodeWithOrder("NOTE", "note2"); @@ -875,8 +899,8 @@ public class VCardExporterTests extends VCardTestsBase { testNoteCommon(V30); } - private void testPhotoCommon(int version) { - final boolean isV30 = version == V30; + private void testPhotoCommon(int vcardType) { + final boolean isV30 = vcardType == V30; ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); entry.buildData(StructuredName.CONTENT_ITEM_TYPE) @@ -886,7 +910,7 @@ public class VCardExporterTests extends VCardTestsBase { ContentValues contentValuesForPhoto = new ContentValues(); contentValuesForPhoto.put("ENCODING", (isV30 ? "b" : "BASE64")); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElem() .addNodeWithoutOrder("FN", "PhotoTest") .addNodeWithoutOrder("N", "PhotoTest;;;;", @@ -905,8 +929,31 @@ public class VCardExporterTests extends VCardTestsBase { testPhotoCommon(V30); } + private void testRelationCommon(int vcardType) { + ExportTestResolver resolver = new ExportTestResolver(); + ContactEntry entry = resolver.buildContactEntry(); + entry.buildData(Relation.CONTENT_ITEM_TYPE) + .put(Relation.TYPE, Relation.TYPE_MOTHER) + .put(Relation.NAME, "Ms. Mother"); + + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); + ImportVerifierElem elem = verifier.addImportVerifier(); + elem.addExpected(Relation.CONTENT_ITEM_TYPE) + .put(Relation.TYPE, Relation.TYPE_MOTHER) + .put(Relation.NAME, "Ms. Mother"); + verifier.verify(); + } + + public void testRelationV21() { + testRelationCommon(V21); + } + + public void testRelationV30() { + testRelationCommon(V30); + } + public void testV30HandleEscape() { - final int version = V30; + final int vcardType = V30; ExportTestResolver resolver = new ExportTestResolver(); resolver.buildContactEntry().buildData(StructuredName.CONTENT_ITEM_TYPE) .put(StructuredName.FAMILY_NAME, "\\") @@ -914,7 +961,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredName.MIDDLE_NAME, ",") .put(StructuredName.PREFIX, "\n") .put(StructuredName.DISPLAY_NAME, "[<{Unescaped:Asciis}>]"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); // Verifies the vCard String correctly escapes each character which must be escaped. verifier.addLineVerifier() .addExpected("N:\\\\;\\;;\\,;\\n;") @@ -960,7 +1007,7 @@ public class VCardExporterTests extends VCardTestsBase { verifier.verify(); } - private void testPickUpNonEmptyContentValuesCommon(int version) { + private void testPickUpNonEmptyContentValuesCommon(int vcardType) { ExportTestResolver resolver = new ExportTestResolver(); ContactEntry entry = resolver.buildContactEntry(); entry.buildData(StructuredName.CONTENT_ITEM_TYPE) @@ -975,7 +1022,7 @@ public class VCardExporterTests extends VCardTestsBase { .put(StructuredName.FAMILY_NAME, "family3"); entry.buildData(StructuredName.CONTENT_ITEM_TYPE) .put(StructuredName.FAMILY_NAME, "family4"); - VCardVerifier verifier = new VCardVerifier(resolver, version); + VCardVerifier verifier = new VCardVerifier(resolver, vcardType); verifier.addPropertyNodesVerifierElem() .addNodeWithoutOrder("N", Arrays.asList("family2", "", "", "", "")) .addNodeWithoutOrder("FN", "family2");