diff options
| author | Daisuke Miyakawa <dmiyakawa@google.com> | 2009-10-13 17:45:25 -0700 |
|---|---|---|
| committer | Daisuke Miyakawa <dmiyakawa@google.com> | 2009-10-14 16:07:40 -0700 |
| commit | 99a0a2cd73503513891565a4aaf99e209bd262d2 (patch) | |
| tree | 98de27744ba5983de9abeb734edb8189a1d4de54 /core/java/android/pim | |
| parent | c4989b1b75848acbeaf53850fbcfbf2f8812e325 (diff) | |
| download | frameworks_base-99a0a2cd73503513891565a4aaf99e209bd262d2.zip frameworks_base-99a0a2cd73503513891565a4aaf99e209bd262d2.tar.gz frameworks_base-99a0a2cd73503513891565a4aaf99e209bd262d2.tar.bz2 | |
Add tests and fix vCard code.
Now, basic tests are almost ready.
TODO:
- importer test toward multiple vCard input (though it was tested with real usage)
- exporter tests for multiple composition
- tests with non-Ascii
- tests with special types like TYPE_DOCOMO
ISSUE:
In order to fully check the validity of exporter, we may have to develop
some vCard importer which rejects vCard which is valid but a kind of dubious.
Internal Issue Number: 2160039
Diffstat (limited to 'core/java/android/pim')
| -rw-r--r-- | core/java/android/pim/vcard/Constants.java | 46 | ||||
| -rw-r--r-- | core/java/android/pim/vcard/ContactStruct.java | 57 | ||||
| -rw-r--r-- | core/java/android/pim/vcard/VCardComposer.java | 839 | ||||
| -rw-r--r-- | core/java/android/pim/vcard/VCardConfig.java | 292 | ||||
| -rw-r--r-- | core/java/android/pim/vcard/VCardDataBuilder.java | 2 | ||||
| -rw-r--r-- | core/java/android/pim/vcard/VCardUtils.java | 67 |
6 files changed, 871 insertions, 432 deletions
diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java index aaa7215..a1c7e10 100644 --- a/core/java/android/pim/vcard/Constants.java +++ b/core/java/android/pim/vcard/Constants.java @@ -56,7 +56,7 @@ package android.pim.vcard; public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions + // Properties both ContactsStruct in Eclair and de-fact vCard extensions // shown in http://en.wikipedia.org/wiki/VCard support are defined here. public static final String PROPERTY_X_AIM = "X-AIM"; public static final String PROPERTY_X_MSN = "X-MSN"; @@ -65,15 +65,12 @@ package android.pim.vcard; public static final String PROPERTY_X_JABBER = "X-JABBER"; public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; + // Properties only ContactsStruct has. We alse use this. + public static final String PROPERTY_X_QQ = "X-QQ"; + public static final String PROPERTY_X_NETMEETING = "X-NETMEETING"; + // Phone number for Skype, available as usual phone. public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; - // Some device emits this "X-" attribute, which is specifically invalid but should be - // always properly accepted, and emitted in some special case (for that device/application). - public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; - - // Android specific properties - // Use only in vCard paser code. - public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; // Properties for DoCoMo vCard. public static final String PROPERTY_X_CLASS = "X-CLASS"; @@ -81,6 +78,11 @@ package android.pim.vcard; public static final String PROPERTY_X_NO = "X-NO"; public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + // For some historical reason, we often use the term "ATTR"/"attribute" especially toward + // what is called "param" in both vCard specs, while vCard, while vCard importer side uses + // "param". + // + // TODO: Confusing. Fix it. public static final String ATTR_TYPE = "TYPE"; // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 @@ -102,13 +104,19 @@ package android.pim.vcard; public static final String ATTR_TYPE_VOICE = "VOICE"; public static final String ATTR_TYPE_INTERNET = "INTERNET"; - // Abbreviation of "preferable"? We interpret this value as "primary" property. + // Abbreviation of "prefered" according to vCard 2.1 specification. + // We interpret this value as "primary" property during import/export. + // + // Note: Both vCard specs does anything about the requirement about this attribute, + // but there may be some vCard importer which will get confused with more than + // one "PREF"s in one property name, while Android accepts them. public static final String ATTR_TYPE_PREF = "PREF"; // Phone types valid in vCard and known to ContactsContract, but not so common. public static final String ATTR_TYPE_CAR = "CAR"; public static final String ATTR_TYPE_ISDN = "ISDN"; public static final String ATTR_TYPE_PAGER = "PAGER"; + public static final String ATTR_TYPE_TLX = "TLX"; // Telex // Phone types existing in vCard 2.1 but not known to ContactsContract. // TODO: should make parser make these TYPE_CUSTOM. @@ -118,16 +126,16 @@ package android.pim.vcard; public static final String ATTR_TYPE_VIDEO = "VIDEO"; // Attribute for Phones, which are not formally valid in vCard (at least 2.1). - // These types are encoded to "X-" attributes when composing vCard for now. - // Parser passes these even if "X-" is added to the attribute. - public static final String ATTR_PHONE_EXTRA_TYPE_OTHER = "OTHER"; + // These types are basically encoded to "X-" attributes when composing vCard. + // Parser passes these when "X-" is added to the attribute or not. public static final String ATTR_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK"; - // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". - public static final String ATTR_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; public static final String ATTR_PHONE_EXTRA_TYPE_RADIO = "RADIO"; - public static final String ATTR_PHONE_EXTRA_TYPE_TELEX = "TELEX"; public static final String ATTR_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD"; public static final String ATTR_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT"; + // vCard composer translates this type to "WORK" + "PREF". Just for parsing. + public static final String ATTR_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; + // vCard composer translates this type to "VOICE" Just for parsing. + public static final String ATTR_PHONE_EXTRA_TYPE_OTHER = "OTHER"; // Attribute for addresses. public static final String ATTR_ADR_TYPE_PARCEL = "PARCEL"; @@ -142,6 +150,14 @@ package android.pim.vcard; // vCard 3.0. public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + public interface ImportOnly { + public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; + // Some device emits this "X-" attribute for expressing Google Talk, + // which is specifically invalid but should be always properly accepted, and emitted + // in some special case (for that device/application). + public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; + } + private Constants() { } }
\ No newline at end of file diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java index 046fb02..eb9c48a 100644 --- a/core/java/android/pim/vcard/ContactStruct.java +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -68,7 +68,8 @@ public class ContactStruct { 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.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(Constants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, + Im.PROTOCOL_GOOGLE_TALK); } static public class PhoneData { @@ -292,16 +293,18 @@ public class ContactStruct { } static public class ImData { + public final int protocol; + public final String customProtocol; public final int type; public final String data; - public final String label; public final boolean isPrimary; - // TODO: ContactsConstant#PROTOCOL, ContactsConstant#CUSTOM_PROTOCOL should be used? - public ImData(int type, String data, String label, boolean isPrimary) { + public ImData(int protocol, String customProtocol, int type, + String data, boolean isPrimary) { + this.protocol = protocol; + this.customProtocol = customProtocol; this.type = type; this.data = data; - this.label = label; this.isPrimary = isPrimary; } @@ -311,14 +314,18 @@ public class ContactStruct { return false; } ImData imData = (ImData)obj; - return (type == imData.type && data.equals(imData.data) && - label.equals(imData.label) && isPrimary == imData.isPrimary); + return (type == imData.type && protocol == imData.protocol + && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : + (imData.customProtocol == null)) + && (data != null ? data.equals(imData.data) : (imData.data == null)) + && isPrimary == imData.isPrimary); } @Override public String toString() { - return String.format("type: %d, data: %s, label: %s, isPrimary: %s", - type, data, label, isPrimary); + return String.format( + "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", + type, protocol, customProtocol, data, isPrimary); } } @@ -440,7 +447,7 @@ public class ContactStruct { private final Account mAccount; public ContactStruct() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC); + this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); } public ContactStruct(int vcardType) { @@ -619,11 +626,12 @@ public class ContactStruct { addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); } - private void addIm(int type, String data, String label, boolean isPrimary) { + private void addIm(int protocol, String customProtocol, int type, + String propValue, boolean isPrimary) { if (mImList == null) { mImList = new ArrayList<ImData>(); } - mImList.add(new ImData(type, data, label, isPrimary)); + mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); } private void addNote(final String note) { @@ -720,7 +728,7 @@ public class ContactStruct { } else if (propName.equals(Constants.PROPERTY_NICKNAME)) { mPhoneticFullName = propValue; } else if (propName.equals(Constants.PROPERTY_NICKNAME) || - propName.equals(Constants.PROPERTY_X_NICKNAME)) { + propName.equals(Constants.ImportOnly.PROPERTY_X_NICKNAME)) { addNickName(propValue); } else if (propName.equals(Constants.PROPERTY_SOUND)) { Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); @@ -891,25 +899,28 @@ public class ContactStruct { isPrimary = false; } addPhone(type, propValue, label, isPrimary); - } else if (sImMap.containsKey(propName)){ - int type = sImMap.get(propName); + } else if (sImMap.containsKey(propName)) { + final int protocol = sImMap.get(propName); boolean isPrimary = false; + int type = -1; final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { if (typeString.equals(Constants.ATTR_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { - type = Phone.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { - type = Phone.TYPE_WORK; + } else if (type < 0) { + if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { + type = Im.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { + type = Im.TYPE_WORK; + } } } } if (type < 0) { type = Phone.TYPE_HOME; } - addIm(type, propValue, null, isPrimary); + addIm(protocol, null, type, propValue, isPrimary); } else if (propName.equals(Constants.PROPERTY_NOTE)) { addNote(propValue); } else if (propName.equals(Constants.PROPERTY_URL)) { @@ -1158,10 +1169,10 @@ public class ContactStruct { builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); builder.withValue(Im.TYPE, imData.type); - if (imData.type == Im.TYPE_CUSTOM) { - builder.withValue(Im.LABEL, imData.label); + builder.withValue(Im.PROTOCOL, imData.protocol); + if (imData.protocol == Im.PROTOCOL_CUSTOM) { + builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); } - builder.withValue(Im.DATA, imData.data); if (imData.isPrimary) { builder.withValue(Data.IS_PRIMARY, 1); } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 9cb9c90..c5afb73 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -100,7 +100,10 @@ import java.util.Set; public class VCardComposer { private static final String LOG_TAG = "vcard.VCardComposer"; - private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + // TODO: Should be configurable? + public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; + public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; + public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; @@ -154,6 +157,11 @@ public class VCardComposer { private static final Uri sDataRequestUri; private static final Map<Integer, String> sImMap; + /** + * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}. + */ + private static final Set<String> sPrimaryPropertyNameSet; + static { Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); @@ -166,6 +174,12 @@ public class VCardComposer { sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); // Google talk is a special case. + + // TODO: incomplete. Implement properly + sPrimaryPropertyNameSet = new HashSet<String>(); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); } public static interface OneEntryHandler { @@ -287,8 +301,9 @@ public class VCardComposer { private final boolean mUsesDefactProperty; private final boolean mUsesUtf8; private final boolean mUsesShiftJis; - private final boolean mUsesQPToPrimaryProperties; private final boolean mAppendTypeParamName; + private final boolean mRefrainsQPToPrimaryProperties; + private final boolean mNeedsToConvertPhoneticString; private Cursor mCursor; private int mIdColumn; @@ -353,8 +368,9 @@ public class VCardComposer { mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); mUsesUtf8 = VCardConfig.usesUtf8(vcardType); mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); - mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); + mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType); mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); + mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { @@ -569,7 +585,7 @@ public class VCardComposer { appendOrganizations(builder, contentValuesListMap); appendPhotos(builder, contentValuesListMap); appendNotes(builder, contentValuesListMap); - // TODO: GroupMembership + // TODO: GroupMembership, Relation, Event other than birthday. if (mIsDoCoMo) { appendVCardLine(builder, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); @@ -650,21 +666,38 @@ public class VCardComposer { // 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) { + if (isSuperPrimary != null && isSuperPrimary > 0) { // We choose "super primary" ContentValues. primaryContentValues = contentValues; break; - } else if (primaryContentValues == null && contentValues != null) { - // We choose the first ContentValues if "super primary" ContentValues does not exist. - primaryContentValues = contentValues; + } else if (primaryContentValues == null) { + // We choose the first "primary" ContentValues + // if "super primary" ContentValues does not exist. + Integer primary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); + if (primary != null && primary > 0) { + primaryContentValues = contentValues; + // Do not break, since there may be ContentValues with "super primary" + // afterword. + } else if (subprimaryContentValues == null) { + subprimaryContentValues = contentValues; + } } } if (primaryContentValues == null) { - Log.e(LOG_TAG, "All ContentValues given from database is empty."); - primaryContentValues = new ContentValues(); + 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 @@ -688,7 +721,7 @@ public class VCardComposer { final String encodedSuffix; final boolean reallyUseQuotedPrintableToName = - (mUsesQPToPrimaryProperties && + (!mRefrainsQPToPrimaryProperties && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && @@ -733,17 +766,17 @@ public class VCardComposer { builder.append(encodedSuffix); builder.append(VCARD_COL_SEPARATOR); - final String fullname = VCardUtils.constructNameFromElements( + final String formattedName = VCardUtils.constructNameFromElements( VCardConfig.getNameOrderType(mVCardType), encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); final boolean reallyUseQuotedPrintableToFullname = - mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); + !mRefrainsQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); final String encodedFullname = reallyUseQuotedPrintableToFullname ? - encodeQuotedPrintable(fullname) : - escapeCharacters(fullname); + encodeQuotedPrintable(formattedName) : + escapeCharacters(formattedName); // FN property builder.append(Constants.PROPERTY_FN); @@ -760,7 +793,7 @@ public class VCardComposer { builder.append(VCARD_COL_SEPARATOR); } else if (!TextUtils.isEmpty(displayName)) { final boolean reallyUseQuotedPrintableToDisplayName = - (mUsesQPToPrimaryProperties && + (!mRefrainsQPToPrimaryProperties && !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); final String encodedDisplayName = reallyUseQuotedPrintableToDisplayName ? @@ -783,37 +816,53 @@ public class VCardComposer { builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_ITEM_SEPARATOR); builder.append(VCARD_COL_SEPARATOR); - } else if (mIsDoCoMo) { - appendVCardLine(builder, Constants.PROPERTY_N, ""); + if (mIsV30) { + builder.append(Constants.PROPERTY_FN); + // TODO: Not allowed formally... + if (shouldAppendCharsetAttribute(encodedDisplayName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedDisplayName); + builder.append(VCARD_COL_SEPARATOR); + } } 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, ""); } - String phoneticFamilyName = primaryContentValues - .getAsString(StructuredName.PHONETIC_FAMILY_NAME); - String phoneticMiddleName = primaryContentValues - .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - String phoneticGivenName = primaryContentValues - .getAsString(StructuredName.PHONETIC_GIVEN_NAME); - if (!(TextUtils.isEmpty(phoneticFamilyName) - && TextUtils.isEmpty(phoneticMiddleName) && - TextUtils.isEmpty(phoneticGivenName))) { // if not empty - if (mIsJapaneseMobilePhone) { - phoneticFamilyName = VCardUtils - .toHalfWidthString(phoneticFamilyName); - phoneticMiddleName = VCardUtils - .toHalfWidthString(phoneticMiddleName); - phoneticGivenName = VCardUtils - .toHalfWidthString(phoneticGivenName); + final String phoneticFamilyName; + final String phoneticMiddleName; + final String phoneticGivenName; + { + String tmpPhoneticFamilyName = + primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String tmpPhoneticMiddleName = + primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String tmpPhoneticGivenName = + primaryContentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (mNeedsToConvertPhoneticString) { + phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); + phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); + phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); + } else { + phoneticFamilyName = tmpPhoneticFamilyName; + phoneticMiddleName = tmpPhoneticMiddleName; + phoneticGivenName = tmpPhoneticGivenName; } + } + if (!(TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) + && TextUtils.isEmpty(phoneticGivenName))) { if (mIsV30) { final String sortString = VCardUtils .constructNameFromElements(mVCardType, - phoneticFamilyName, - phoneticMiddleName, - phoneticGivenName); + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); builder.append(Constants.PROPERTY_SORT_STRING); // Do not need to care about QP, since vCard 3.0 does not allow it. @@ -825,11 +874,12 @@ public class VCardComposer { builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedSortString); builder.append(VCARD_COL_SEPARATOR); - } else { + } 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 since it is supported by + // We chose to use DoCoMo's way when the device is Japanese one + // since it is supported by // a lot of Japanese mobile phones. This is "X-" property, so // any parser hopefully would not get confused with this. builder.append(Constants.PROPERTY_SOUND); @@ -837,13 +887,13 @@ public class VCardComposer { builder.append(Constants.ATTR_TYPE_X_IRMC_N); boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticFamilyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticMiddleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticGivenName))); + (!mRefrainsQPToPrimaryProperties + && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); final String encodedPhoneticFamilyName; final String encodedPhoneticMiddleName; @@ -889,8 +939,7 @@ public class VCardComposer { if (mUsesDefactProperty) { if (!TextUtils.isEmpty(phoneticGivenName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName); final String encodedPhoneticGivenName; if (reallyUseQuotedPrintable) { encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); @@ -912,8 +961,7 @@ public class VCardComposer { } if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName); final String encodedPhoneticMiddleName; if (reallyUseQuotedPrintable) { encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); @@ -935,8 +983,7 @@ public class VCardComposer { } if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName); final String encodedPhoneticFamilyName; if (reallyUseQuotedPrintable) { encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); @@ -963,46 +1010,48 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Nickname.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - final String propertyNickname; - if (mIsV30) { - propertyNickname = Constants.PROPERTY_NICKNAME; - /*} else if (mUsesAndroidProperty) { - propertyNickname = VCARD_PROPERTY_X_NICKNAME;*/ - } else { - // There's no way to add this field. - return; - } + if (contentValuesList == null) { + return; + } - for (ContentValues contentValues : contentValuesList) { - final String nickname = contentValues.getAsString(Nickname.NAME); - if (TextUtils.isEmpty(nickname)) { - continue; - } + final String propertyNickname; + if (mIsV30) { + propertyNickname = Constants.PROPERTY_NICKNAME; + /*} else if (mUsesAndroidProperty) { + propertyNickname = VCARD_PROPERTY_X_NICKNAME;*/ + } else { + // There's no way to add this field. + return; + } - final String encodedNickname; - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); - if (reallyUseQuotedPrintable) { - encodedNickname = encodeQuotedPrintable(nickname); - } else { - encodedNickname = escapeCharacters(nickname); - } + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } - builder.append(propertyNickname); - if (shouldAppendCharsetAttribute(propertyNickname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedNickname); - builder.append(VCARD_COL_SEPARATOR); + final String encodedNickname; + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); + if (reallyUseQuotedPrintable) { + encodedNickname = encodeQuotedPrintable(nickname); + } else { + encodedNickname = escapeCharacters(nickname); } + + builder.append(propertyNickname); + if (shouldAppendCharsetAttribute(propertyNickname)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedNickname); + builder.append(VCARD_COL_SEPARATOR); } } @@ -1016,6 +1065,9 @@ public class VCardComposer { 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(); @@ -1024,18 +1076,18 @@ public class VCardComposer { continue; } phoneLineExists = true; - int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); + int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); // TODO: Premature, since this allows two phone numbers which are // same from the view of phone number format (e.g. "100" v.s. "1-0-0") if (!phoneSet.contains(phoneNumber)) { phoneSet.add(phoneNumber); - appendVCardTelephoneLine(builder, type, label, phoneNumber); + appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary); } } } if (!phoneLineExists && mIsDoCoMo) { - appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false); } } @@ -1043,14 +1095,11 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Email.CONTENT_ITEM_TYPE); + boolean emailAddressExists = false; if (contentValuesList != null) { - Set<String> addressSet = new HashSet<String>(); + final Set<String> addressSet = new HashSet<String>(); for (ContentValues contentValues : contentValuesList) { - Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); - final int type = (typeAsObject != null ? - typeAsObject : Email.TYPE_OTHER); - final String label = contentValues.getAsString(Email.LABEL); String emailAddress = contentValues.getAsString(Email.DATA); if (emailAddress != null) { emailAddress = emailAddress.trim(); @@ -1058,16 +1107,23 @@ public class VCardComposer { 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); + appendVCardEmailLine(builder, type, label, emailAddress, isPrimary); } } } if (!emailAddressExists && mIsDoCoMo) { - appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false); } } @@ -1124,7 +1180,10 @@ public class VCardComposer { final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); final String label = contentValues.getAsString(StructuredPostal.LABEL); if (type == preferedType) { - appendVCardPostalLine(builder, type, label, contentValues); + // Note: Not sure why we need to emit "empty" line even when actual + // data does not exist. There may be some reason or may not. + // We keep safer side since the previous implementation did so. + appendVCardPostalLine(builder, type, label, contentValues, true, true); return true; } } @@ -1134,11 +1193,18 @@ public class VCardComposer { private void appendPostalsForGeneric(final StringBuilder builder, final List<ContentValues> contentValuesList) { for (ContentValues contentValues : contentValuesList) { - final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - if (type != null) { - appendVCardPostalLine(builder, type, label, contentValues); + if (contentValues == null) { + continue; } + final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : DEFAULT_POSTAL_TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + final Integer isPrimaryAsInteger = + contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false); } } @@ -1146,24 +1212,63 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Im.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); - String data = contentValues.getAsString(Im.DATA); - if (data != null) { - data = data.trim(); - } - if (TextUtils.isEmpty(data)) { - continue; - } - - if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { - if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { - appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); + 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.ATTR_TYPE_HOME; + break; + } + case Im.TYPE_WORK: { + typeAsString = Constants.ATTR_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; } - // TODO: add "X-GOOGLE TALK" case... } } + + List<String> attributeList = new ArrayList<String>(); + if (!TextUtils.isEmpty(typeAsString)) { + attributeList.add(typeAsString); + } + final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + if (isPrimary) { + attributeList.add(Constants.ATTR_TYPE_PREF); + } + + appendVCardLineWithCharsetAndQPDetection( + builder, propertyName, attributeList, data); } } @@ -1171,39 +1276,83 @@ public class VCardComposer { final Map<String, List<ContentValues>> contentValuesListMap) { final List<ContentValues> contentValuesList = contentValuesListMap .get(Website.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String website = contentValues.getAsString(Website.URL); - if (website != null) { - website = website.trim(); - } - if (!TextUtils.isEmpty(website)) { - appendVCardLine(builder, Constants.PROPERTY_URL, website); - } + 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 attribute addition toward "URL" + // property, while there's no document in vCard 2.1. + // + // TODO: Should we allow adding it when appropriate? + // (Actually, we drop some data. Using "group.X-URL-TYPE" or something + // may help) + if (!TextUtils.isEmpty(website)) { + appendVCardLine(builder, Constants.PROPERTY_URL, website); } } } + /** + * Theoretically, there must be only one birthday for each vCard entry. + * Also, we are afraid of some importer's parse error during its import. + * We emit only one birthday entry even when there are more than one. + */ private void appendBirthday(final StringBuilder builder, final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Event.CONTENT_ITEM_TYPE); - if (contentValuesList != null && contentValuesList.size() > 0) { - Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); - if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { - return; + final List<ContentValues> contentValuesList = + contentValuesListMap.get(Event.CONTENT_ITEM_TYPE); + if (contentValuesList == null) { + return; + } + String primaryBirthday = null; + String secondaryBirthday = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; } - // Theoretically, there must be only one birthday for each vCard data and - // we are afraid of some parse error occuring in some devices, so - // we emit only one birthday entry for now. - String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); - if (birthday != null) { - birthday = birthday.trim(); + 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; } - if (!TextUtils.isEmpty(birthday)) { - appendVCardLine(builder, Constants.PROPERTY_BDAY, birthday); + 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, @@ -1212,23 +1361,35 @@ public class VCardComposer { .get(Organization.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { - String company = contentValues - .getAsString(Organization.COMPANY); + String company = contentValues.getAsString(Organization.COMPANY); if (company != null) { company = company.trim(); } - String title = contentValues - .getAsString(Organization.TITLE); + 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)) { - appendVCardLine(builder, Constants.PROPERTY_ORG, company, - !VCardUtils.containsOnlyPrintableAscii(company), - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(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), @@ -1257,11 +1418,9 @@ public class VCardComposer { 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... + // 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) { @@ -1435,59 +1594,126 @@ public class VCardComposer { builder.append(VCARD_COL_SEPARATOR); } - private void appendVCardPostalLine(final StringBuilder builder, - final Integer typeAsObject, final String label, - final ContentValues contentValues) { - builder.append(Constants.PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); + 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; + } + } - // Note: Not sure why we need to emit "empty" line even when actual data does not exist. - // There may be some reason or may not be any. We keep safer side. - // TODO: investigate this. - boolean dataExists = false; + /** + * @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); - boolean actuallyUseQuotedPrintable = false; - boolean shouldAppendCharset = false; for (String data : dataArray) { if (!TextUtils.isEmpty(data)) { - dataExists = true; - if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { - shouldAppendCharset = true; + dataArrayExists = true; + if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { + appendCharset = true; } if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { - actuallyUseQuotedPrintable = true; + reallyUseQuotedPrintable = true; break; } } } - int length = dataArray.length; - for (int i = 0; i < length; i++) { - String data = dataArray[i]; - if (!TextUtils.isEmpty(data)) { - if (actuallyUseQuotedPrintable) { - dataArray[i] = encodeQuotedPrintable(data); + if (dataArrayExists) { + StringBuffer addressBuffer = new StringBuffer(); + boolean first = true; + for (String data : dataArray) { + if (first) { + first = false; } else { - dataArray[i] = escapeCharacters(data); + 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()); } - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = StructuredPostal.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; + 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; + } } - String typeAsString = null; - switch (typeAsPrimitive) { + List<String> attributeList = new ArrayList<String>(); + if (isPrimary) { + attributeList.add(Constants.ATTR_TYPE_PREF); + } + switch (type) { case StructuredPostal.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; + attributeList.add(Constants.ATTR_TYPE_HOME); break; } case StructuredPostal.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + attributeList.add(Constants.ATTR_TYPE_WORK); break; } case StructuredPostal.TYPE_CUSTOM: { @@ -1497,9 +1723,7 @@ public class VCardComposer { // ("IANA-token" in the vCard 3.0 is unclear...) // Just for safety, we add "X-" at the beggining of each label. // Also checks the label obeys with vCard 3.0 spec. - builder.append("X-"); - builder.append(label); - builder.append(VCARD_DATA_SEPARATOR); + attributeList.add("X-" + label); } break; } @@ -1507,82 +1731,56 @@ public class VCardComposer { break; } default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); break; } } - // Attribute(s). + // Actual data construction starts from here. + // TODO: add a new version of appendVCardLine() for this purpose. + builder.append(Constants.PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + + // Attributes { boolean shouldAppendAttrSeparator = false; - if (typeAsString != null) { - appendTypeAttribute(builder, typeAsString); + if (!attributeList.isEmpty()) { + appendTypeAttributes(builder, attributeList); shouldAppendAttrSeparator = true; } - if (dataExists) { - if (shouldAppendCharset) { - // Strictly, vCard 3.0 does not allow exporters to emit charset information, - // but we will add it since the information should be useful for importers, - // - // Assume no parser does not emit error with this attribute in vCard 3.0. - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(mVCardAttributeCharset); - shouldAppendAttrSeparator = true; + if (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 attribute in vCard 3.0. + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); } + builder.append(mVCardAttributeCharset); + shouldAppendAttrSeparator = true; + } - if (actuallyUseQuotedPrintable) { - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(VCARD_ATTR_ENCODING_QP); - shouldAppendAttrSeparator = true; + if (reallyUseQuotedPrintable) { + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); } + builder.append(VCARD_ATTR_ENCODING_QP); + shouldAppendAttrSeparator = true; } } - // Property values. - builder.append(VCARD_DATA_SEPARATOR); - if (dataExists) { - // The elements in dataArray are already encoded to quoted printable - // if needed. - // See above. - // - // TODO: in vCard 3.0, one line may become too huge. Fix this. - builder.append(dataArray[0]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[1]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[2]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[3]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[4]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[5]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[6]); - } + builder.append(addressData); builder.append(VCARD_COL_SEPARATOR); } private void appendVCardEmailLine(final StringBuilder builder, - final Integer typeAsObject, final String label, final String data) { - builder.append(Constants.PROPERTY_EMAIL); - - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = Email.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; - } - + final int type, final String label, + final String rawData, final boolean isPrimary) { final String typeAsString; - switch (typeAsPrimitive) { + switch (type) { case Email.TYPE_CUSTOM: { // For backward compatibility. // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. @@ -1594,7 +1792,7 @@ public class VCardComposer { && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { typeAsString = "X-" + label; } else { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; } break; } @@ -1607,7 +1805,7 @@ public class VCardComposer { break; } case Email.TYPE_OTHER: { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; break; } case Email.TYPE_MOBILE: { @@ -1615,22 +1813,27 @@ public class VCardComposer { break; } default: { - Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); - typeAsString = DEFAULT_EMAIL_TYPE; + Log.e(LOG_TAG, "Unknown Email type: " + type); + typeAsString = null; break; } } - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, typeAsString); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(data); - builder.append(VCARD_COL_SEPARATOR); + final List<String> attributeList = new ArrayList<String>(); + if (isPrimary) { + attributeList.add(Constants.ATTR_TYPE_PREF); + } + if (!TextUtils.isEmpty(typeAsString)) { + attributeList.add(typeAsString); + } + + appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL, + attributeList, rawData); } private void appendVCardTelephoneLine(final StringBuilder builder, final Integer typeAsObject, final String label, - String encodedData) { + final String encodedData, boolean isPrimary) { builder.append(Constants.PROPERTY_TEL); builder.append(VCARD_ATTR_SEPARATOR); @@ -1641,53 +1844,102 @@ public class VCardComposer { typeAsPrimitive = typeAsObject; } + ArrayList<String> attributeList = new ArrayList<String>(); switch (typeAsPrimitive) { case Phone.TYPE_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + attributeList.addAll( + Arrays.asList(Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); break; case Phone.TYPE_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + attributeList.addAll( + Arrays.asList(Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); break; case Phone.TYPE_FAX_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + attributeList.addAll( + Arrays.asList(Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); break; case Phone.TYPE_FAX_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + attributeList.addAll( + Arrays.asList(Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); break; case Phone.TYPE_MOBILE: - builder.append(Constants.ATTR_TYPE_CELL); + attributeList.add(Constants.ATTR_TYPE_CELL); break; case Phone.TYPE_PAGER: if (mIsDoCoMo) { // Not sure about the reason, but previous implementation had // used "VOICE" instead of "PAGER" - // Also, refrain from using appendType() so that "TYPE=" is never be appended. - builder.append(Constants.ATTR_TYPE_VOICE); + attributeList.add(Constants.ATTR_TYPE_VOICE); } else { - appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); + attributeList.add(Constants.ATTR_TYPE_PAGER); } break; case Phone.TYPE_OTHER: - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + attributeList.add(Constants.ATTR_TYPE_VOICE); + break; + case Phone.TYPE_CAR: + attributeList.add(Constants.ATTR_TYPE_CAR); + break; + case Phone.TYPE_COMPANY_MAIN: + // There's no relevant field in vCard (at least 2.1). + attributeList.add(Constants.ATTR_TYPE_WORK); + isPrimary = true; + break; + case Phone.TYPE_ISDN: + attributeList.add(Constants.ATTR_TYPE_ISDN); + break; + case Phone.TYPE_MAIN: + isPrimary = true; + break; + case Phone.TYPE_OTHER_FAX: + attributeList.add(Constants.ATTR_TYPE_FAX); + break; + case Phone.TYPE_TELEX: + attributeList.add(Constants.ATTR_TYPE_TLX); + break; + case Phone.TYPE_WORK_MOBILE: + attributeList.addAll( + Arrays.asList(Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_CELL)); + break; + case Phone.TYPE_WORK_PAGER: + attributeList.add(Constants.ATTR_TYPE_WORK); + // See above. + if (mIsDoCoMo) { + attributeList.add(Constants.ATTR_TYPE_VOICE); + } else { + attributeList.add(Constants.ATTR_TYPE_PAGER); + } + break; + case Phone.TYPE_MMS: + attributeList.add(Constants.ATTR_TYPE_MSG); break; case Phone.TYPE_CUSTOM: if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - appendTypeAttribute(builder, "X-" + label); + // Note: Strictly, vCard 2.1 does not allow "X-" attribute without + // "TYPE=" string. + attributeList.add("X-" + label); } else { // Just ignore the custom type. - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + attributeList.add(Constants.ATTR_TYPE_VOICE); } break; + case Phone.TYPE_RADIO: + case Phone.TYPE_TTY_TDD: default: - appendUncommonPhoneType(builder, typeAsPrimitive); break; } + if (isPrimary) { + attributeList.add(Constants.ATTR_TYPE_PREF); + } + + if (attributeList.isEmpty()) { + appendUncommonPhoneType(builder, typeAsPrimitive); + } else { + appendTypeAttributes(builder, attributeList); + } + builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); builder.append(VCARD_COL_SEPARATOR); @@ -1711,15 +1963,43 @@ public class VCardComposer { } } + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData); + } + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, + final List<String> attributeList, final String rawData) { + final boolean needCharset = + (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData)); + final boolean reallyUseQuotedPrintable = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData); + appendVCardLine(builder, propertyName, attributeList, + 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 field, final String rawData, final boolean needCharset, + final String propertyName, final String rawData, final boolean needCharset, + boolean needQuotedPrintable) { + appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable); + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, + final List<String> attributeList, + final String rawData, final boolean needCharset, boolean needQuotedPrintable) { - builder.append(field); + builder.append(propertyName); + if (attributeList != null && attributeList.size() > 0) { + builder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttributes(builder, attributeList); + } if (needCharset) { builder.append(VCARD_ATTR_SEPARATOR); builder.append(mVCardAttributeCharset); @@ -1741,6 +2021,9 @@ public class VCardComposer { builder.append(VCARD_COL_SEPARATOR); } + /** + * VCARD_ATTR_SEPARATOR must be appended before this method being called. + */ private void appendTypeAttributes(final StringBuilder builder, final List<String> types) { // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, @@ -1756,9 +2039,15 @@ public class VCardComposer { } } + /** + * VCARD_ATTR_SEPARATOR must be appended before this method being called. + */ private void appendTypeAttribute(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) { + if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); } builder.append(type); @@ -1869,12 +2158,11 @@ public class VCardComposer { if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { needCharset = true; } - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false); appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false); String label = Integer.toString(phonetype); - appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); + appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); @@ -1948,7 +2236,6 @@ public class VCardComposer { name = mCursor.getString(NUMBER_COLUMN_INDEX); } final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false); appendVCardLine(builder, Constants.PROPERTY_N, name, needCharset, false); @@ -1958,7 +2245,7 @@ public class VCardComposer { if (TextUtils.isEmpty(label)) { label = Integer.toString(type); } - appendVCardTelephoneLine(builder, type, label, number); + appendVCardTelephoneLine(builder, 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/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 03ed329..9581c74 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -15,14 +15,20 @@ */ package android.pim.vcard; +import android.util.Log; + import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * The class representing VCard related configurations. Useful static methods are not in this class * but in VCardUtils. */ public class VCardConfig { + private static final String LOG_TAG = "vcard.VCardConfig"; + // TODO: may be better to make the instance of this available and stop using static methods and // one integer. @@ -95,18 +101,63 @@ public class VCardConfig { private static final int FLAG_DOCOMO = 0x20000000; /** - * The flag indicating the vCard composer use Quoted-Printable toward even "primary" types. - * In this context, "primary" types means "N", "FN", etc. which are usually "not" encoded - * into Quoted-Printable format in external exporters. - * This flag is useful when some target importer does not accept "primary" property values - * without Quoted-Printable encoding. - * - * @hide Temporaly made public. We don't strictly define "primary", so we may change the - * behavior around this flag in the future. Do not use this flag without any reason. + * <P> + * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" + * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). + * </P> + * <P> + * We actually cannot define what is the "primary" property. Note that this is NOT defined + * in vCard specification either. Also be aware that it is NOT related to "primary" notion + * used in {@link android.provider.ContactsContract}. + * This notion is just for vCard composition in Android. + * </P> + * <P> + * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 + * do NOT use Quoted-Printable encoding toward some properties like "N", "FN", etc. even when + * their values contain non-ascii or/and CR/LF, while they use the encoding in the other + * properties like "ADR", "ORG", etc. + * <P> + * We are afraid of the case where some vCard importer also forget handling QP presuming QP is + * not used in such fields. + * </P> + * <P> + * This flag is useful when some target importer you are going to focus on does not accept + * such "primary" property values with Quoted-Printable encoding. + * </P> + * <P> + * Again, we should not use this flag at all for complying vCard 2.1 spec. + * </P> + * <P> + * We will change the behavior around this flag in the future, after understanding the other + * real vCard cases around this problem. Please use this flag with extreme caution even when + * needed. + * </P> + * <P> + * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this + * kind of problem (hopefully). + * </P> */ - public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; + public static final int FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES = 0x10000000; /** + * <P> + * The flag indicating that phonetic name related fields must be converted to + * appropriate form. Note that "appropriate" is not defined in any vCard specification. + * This is Android-specific. + * </P> + * <P> + * One typical (and currently sole) example where we need this flag is the time when + * we need to emit Japanese phonetic names into vCard entries. The property values + * should be encoded into half-width katakana when the target importer is Japanese mobile + * phones', which are probably not able to parse full-width hiragana/katakana for + * historical reasons, while the vCard importers embedded to softwares for PC should be + * able to parse them as we expect. + * </P> + */ + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; + + /** + * <P> * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string every time * possible. The default behavior does not emit it and is valid, while adding "TYPE=" * is also valid. In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in @@ -119,87 +170,118 @@ public class VCardConfig { * * e.g. int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); */ - public static final int FLAG_APPEND_TYPE_PARAM = 0x08000000; + public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; //// The followings are VCard types available from importer/exporter. //// /** + * <P> * General vCard format with the version 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used. - * + * When composing a vCard entry, the US convension will be used toward formatting + * some values + * </P> + * <P> * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", - * while in Japan, it should be "Prefix Family Middle Given Suffix". + * while it should be "Prefix Family Middle Given Suffix" in Japan. + * </P> */ - public static final int VCARD_TYPE_V21_GENERIC = + public static final int VCARD_TYPE_V21_GENERIC_UTF8 = (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; + /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; /** + * <P> * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * - * Note that this type is not fully implemented, so probably some bugs remain both in - * parsing and composing. - * - * TODO: implement this type correctly. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_GENERIC = + public static final int VCARD_TYPE_V30_GENERIC_UTF8 = (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; /** - * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * <P> + * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + * </P> */ - public static final int VCARD_TYPE_V21_EUROPE = + public static final int VCARD_TYPE_V21_EUROPE_UTF8 = (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; + /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; /** + * <P> * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_EUROPE = + public static final int VCARD_TYPE_V30_EUROPE_UTF8 = (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; - - /** - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - */ - public static final int VCARD_TYPE_V21_JAPANESE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese"; - /** - * vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + + /** + * <P> + * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for + * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_SJIS = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; /** + * <P> * vCard format for miscellaneous Japanese devices, using Shift_Jis for * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_JAPANESE = + public static final int VCARD_TYPE_V30_JAPANESE_SJIS = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese"; + /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; /** - * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | @@ -208,38 +290,72 @@ public class VCardConfig { /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; /** - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. - * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. - * No Android-specific property nor defact property is included. + * <P> + * The vCard 2.1 based format which (partially) considers the convention in Japanese + * mobile phones, where phonetic names are translated to half-width katakana if + * possible, etc. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_CONVERT_PHONETIC_NAME_STRINGS | + FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES); + + public static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; + + /** + * <P> + * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * </p> + * <P> + * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. + * No Android-specific property nor defact property is included. The "Primary" properties + * are NOT encoded to Quoted-Printable. + * </P> */ public static final int VCARD_TYPE_DOCOMO = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; - private static final Map<String, Integer> VCARD_TYPES_MAP; + private static final Map<String, Integer> sVCardTypeMap; + private static final Set<Integer> sJapaneseMobileTypeSet; static { - VCARD_TYPES_MAP = new HashMap<String, Integer>(); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + sVCardTypeMap = new HashMap<String, Integer>(); + sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); + sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + + sJapaneseMobileTypeSet = new HashSet<Integer>(); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); + sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); } public static int getVCardTypeFromString(String vcardTypeString) { String loweredKey = vcardTypeString.toLowerCase(); - if (VCARD_TYPES_MAP.containsKey(loweredKey)) { - return VCARD_TYPES_MAP.get(loweredKey); + if (sVCardTypeMap.containsKey(loweredKey)) { + return sVCardTypeMap.get(loweredKey); } else { // XXX: should return the value indicating the input is invalid? + Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\""); return VCARD_TYPE_DEFAULT; } } @@ -252,22 +368,6 @@ public class VCardConfig { return !isV30(vcardType); } - public static boolean isDoCoMo(int vcardType) { - return ((vcardType & FLAG_DOCOMO) != 0); - } - - /** - * @return true if the device is Japanese and some Japanese convension is - * applied to creating "formatted" something like FORMATTED_ADDRESS. - */ - public static boolean isJapaneseDevice(int vcardType) { - return ((vcardType == VCARD_TYPE_V21_JAPANESE) || - (vcardType == VCARD_TYPE_V21_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_V30_JAPANESE) || - (vcardType == VCARD_TYPE_V30_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_DOCOMO)); - } - public static boolean usesUtf8(int vcardType) { return ((vcardType & FLAG_CHARSET_UTF8) != 0); } @@ -276,17 +376,6 @@ public class VCardConfig { return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); } - /** - * @return true when Japanese phonetic string must be converted to a string - * containing only half-width katakana. This method exists since Japanese mobile - * phones usually use only half-width katakana for expressing phonetic names and - * some devices are not ready for parsing other phonetic strings like hiragana and - * full-width katakana. - */ - public static boolean needsToConvertPhoneticString(int vcardType) { - return (vcardType == VCARD_TYPE_DOCOMO); - } - public static int getNameOrderType(int vcardType) { return vcardType & NAME_ORDER_MASK; } @@ -299,24 +388,37 @@ public class VCardConfig { return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); } - public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { - return vcardType == VCARD_TYPE_DOCOMO; - } - public static boolean showPerformanceLog() { return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; } + public static boolean refrainsQPToPrimaryProperties(int vcardType) { + return (!usesQuotedPrintable(vcardType) || + ((vcardType & FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES) != 0)); + } + + public static boolean appendTypeParamName(int vcardType) { + return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); + } + /** - * @hide + * @return true if the device is Japanese and some Japanese convension is + * applied to creating "formatted" something like FORMATTED_ADDRESS. */ - public static boolean usesQPToPrimaryProperties(int vcardType) { - return (usesQuotedPrintable(vcardType) && - ((vcardType & FLAG_USE_QP_TO_PRIMARY_PROPERTIES) != 0)); + public static boolean isJapaneseDevice(int vcardType) { + return sJapaneseMobileTypeSet.contains(vcardType); } - public static boolean appendTypeParamName(int vcardType) { - return (vcardType & FLAG_APPEND_TYPE_PARAM) != 0; + public static boolean needsToConvertPhoneticString(int vcardType) { + return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + + public static boolean isDoCoMo(int vcardType) { + return ((vcardType & FLAG_DOCOMO) != 0); } private VCardConfig() { diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java index d00f616..76ad482 100644 --- a/core/java/android/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -69,7 +69,7 @@ public class VCardDataBuilder implements VCardBuilder { private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); public VCardDataBuilder() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); } /** diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index b3bf426..376327c 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -18,6 +18,7 @@ package android.pim.vcard; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.text.TextUtils; @@ -44,38 +45,49 @@ public class VCardUtils { private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; private static final Set<String> sPhoneTypesSetUnknownToContacts; - private static final Map<String, Integer> sKnownPhoneTypesMap_StoI; + private static final Map<String, Integer> sKnownPhoneTypeMap_StoI; + + private static final Map<Integer, String> sKnownImPropNameMap_ItoS; static { sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); - sKnownPhoneTypesMap_StoI = new HashMap<String, Integer>(); + sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>(); sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.ATTR_TYPE_CAR); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.ATTR_TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK); - sKnownPhoneTypesMap_StoI.put( + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put( Constants.ATTR_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_TELEX, Phone.TYPE_TELEX); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_ASSISTANT, + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(Constants.ATTR_PHONE_EXTRA_TYPE_ASSISTANT, Phone.TYPE_ASSISTANT); sPhoneTypesSetUnknownToContacts = new HashSet<String>(); sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MODEM); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MSG); sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_BBS); sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_VIDEO); + + sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); + 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); } public static String getPhoneAttributeString(Integer type) { @@ -103,7 +115,7 @@ public class VCardUtils { if (typeString.startsWith("X-") && type < 0) { typeString = typeString.substring(2); } - Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); if (tmp != null) { type = tmp; } else if (type < 0) { @@ -136,7 +148,11 @@ public class VCardUtils { return type; } } - + + public static String getPropertyNameForIm(int protocol) { + return sKnownImPropNameMap_ItoS.get(protocol); + } + public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { // TODO: check the following. // - it may violate vCard spec @@ -206,12 +222,12 @@ public class VCardUtils { builder.withValue(Data.IS_PRIMARY, 1); } } - + /** * Returns String[] containing address information based on vCard spec * (PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name). * All String objects are non-null ("" is used when the relevant data is empty). - * + * * Note that the data structure of ContactsContract is different from that defined in vCard. * So some conversion may be performed in this method. See also * {{@link #insertStructuredPostalDataUsingContactsStruct(int, @@ -219,13 +235,20 @@ public class VCardUtils { * android.pim.vcard.ContactStruct.PostalData)} */ public static String[] getVCardPostalElements(ContentValues contentValues) { + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name String[] dataArray = new String[7]; dataArray[0] = contentValues.getAsString(StructuredPostal.POBOX); if (dataArray[0] == null) { dataArray[0] = ""; } - // Extended addr. There's no relevant data in ContactsContract. - dataArray[1] = ""; + // We keep all the data in StructuredPostal, presuming NEIGHBORHOOD is + // similar to "Extended Address". + dataArray[1] = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); + if (dataArray[1] == null) { + dataArray[1] = ""; + } dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); if (dataArray[2] == null) { dataArray[2] = ""; |
