diff options
author | Android (Google) Code Review <android-gerrit@google.com> | 2009-08-25 22:08:38 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2009-08-25 22:08:38 -0700 |
commit | 97f870956a37d441f82e21135a5e68d3ecdd0bf5 (patch) | |
tree | 0803c76ea83196fbbd9651639ee10cad0683004a /core/java/android | |
parent | ba176d6c8ad581e65b46bd6835c0737e74ef453d (diff) | |
parent | f4ddea769098e24a7316b9ee895d323005433c2c (diff) | |
download | frameworks_base-97f870956a37d441f82e21135a5e68d3ecdd0bf5.zip frameworks_base-97f870956a37d441f82e21135a5e68d3ecdd0bf5.tar.gz frameworks_base-97f870956a37d441f82e21135a5e68d3ecdd0bf5.tar.bz2 |
Merge change 22399 into eclair
* changes:
Refactor VCard handling code, phase 2, 3, 4, 5
Diffstat (limited to 'core/java/android')
-rw-r--r-- | core/java/android/pim/vcard/Constants.java | 94 | ||||
-rw-r--r-- | core/java/android/pim/vcard/ContactStruct.java | 1438 | ||||
-rw-r--r-- | core/java/android/pim/vcard/EntryCommitter.java | 54 | ||||
-rw-r--r-- | core/java/android/pim/vcard/EntryHandler.java | 13 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardComposer.java | 1433 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardConfig.java | 268 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardDataBuilder.java | 33 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardParser_V21.java | 142 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardParser_V30.java | 10 | ||||
-rw-r--r-- | core/java/android/pim/vcard/VCardUtils.java | 764 | ||||
-rw-r--r-- | core/java/android/syncml/pim/vcard/VCardDataBuilder.java | 6 | ||||
-rw-r--r-- | core/java/android/syncml/pim/vcard/VCardParser.java | 1 |
12 files changed, 3349 insertions, 907 deletions
diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java new file mode 100644 index 0000000..ca41ce5 --- /dev/null +++ b/core/java/android/pim/vcard/Constants.java @@ -0,0 +1,94 @@ +/* + * 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; + +/** + * Constants used in both composer and parser. + */ +/* package */ class Constants { + + public static final String ATTR_TYPE = "TYPE"; + + public static final String VERSION_V21 = "2.1"; + public static final String VERSION_V30 = "3.0"; + + // Properties both the current (as of 2009-08-17) ContactsStruct 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"; + public static final String PROPERTY_X_YAHOO = "X-YAHOO"; + public static final String PROPERTY_X_ICQ = "X-ICQ"; + 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"; + // 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"; + + // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 + // + // e.g. + // 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..." + // 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..." + // 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..." + // + // 2) has been the default of VCard exporter/importer in Android, but we can see the other + // formats in vCard data emitted by the other softwares/devices. + // + // So we are currently not sure which type is the best; probably we will have to change which + // type should be emitted depending on the device. + public static final String ATTR_TYPE_HOME = "HOME"; + public static final String ATTR_TYPE_WORK = "WORK"; + public static final String ATTR_TYPE_FAX = "FAX"; + public static final String ATTR_TYPE_CELL = "CELL"; + public static final String ATTR_TYPE_VOICE = "VOICE"; + public static final String ATTR_TYPE_INTERNET = "INTERNET"; + + 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"; + + // Phone types existing in vCard 2.1 but not known to ContactsContract. + // TODO: should make parser make these TYPE_CUSTOM. + public static final String ATTR_TYPE_MODEM = "MODEM"; + public static final String ATTR_TYPE_MSG = "MSG"; + public static final String ATTR_TYPE_BBS = "BBS"; + public static final String ATTR_TYPE_VIDEO = "VIDEO"; + + // Phone types existing in the current Contacts structure but not 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_TYPE_PHONE_EXTRA_OTHER = "OTHER"; + public static final String ATTR_TYPE_PHONE_EXTRA_CALLBACK = "CALLBACK"; + // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". + public static final String ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN = "COMPANY-MAIN"; + public static final String ATTR_TYPE_PHONE_EXTRA_RADIO = "RADIO"; + public static final String ATTR_TYPE_PHONE_EXTRA_TELEX = "TELEX"; + public static final String ATTR_TYPE_PHONE_EXTRA_TTY_TDD = "TTY-TDD"; + public static final String ATTR_TYPE_PHONE_EXTRA_ASSISTANT = "ASSISTANT"; + + // DoCoMo specific attribute. Used with "SOUND" property, which is alternate of SORT-STRING in + // vCard 3.0. + public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + + 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 46725d3..8e8d46a 100644 --- a/core/java/android/pim/vcard/ContactStruct.java +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -15,38 +15,58 @@ */ package android.pim.vcard; -import android.content.AbstractSyncableContentProvider; +import android.content.ContentProviderOperation; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.ContentValues; -import android.net.Uri; -import android.provider.Contacts; -import android.provider.Contacts.ContactMethods; -import android.provider.Contacts.Extensions; -import android.provider.Contacts.GroupMembership; -import android.provider.Contacts.Organizations; -import android.provider.Contacts.People; -import android.provider.Contacts.Phones; -import android.provider.Contacts.Photos; +import android.content.OperationApplicationException; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Miscellaneous; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; /** * This class bridges between data structure of Contact app and VCard data. */ public class ContactStruct { - private static final String LOG_TAG = "ContactStruct"; + private static final String LOG_TAG = "vcard.ContactStruct"; + + // Key: the name shown in VCard. e.g. "X-AIM", "X-ICQ" + // Value: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} + private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); + + 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.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + } /** * @hide only for testing @@ -85,11 +105,7 @@ public class ContactStruct { /** * @hide only for testing */ - static public class ContactMethod { - // Contacts.KIND_EMAIL, Contacts.KIND_POSTAL - public final int kind; - // e.g. Contacts.ContactMethods.TYPE_HOME, Contacts.PhoneColumns.TYPE_HOME - // If type == Contacts.PhoneColumns.TYPE_CUSTOM, label is used. + static public class EmailData { public final int type; public final String data; // Used only when TYPE is TYPE_CUSTOM. @@ -97,35 +113,143 @@ public class ContactStruct { // isPrimary is changable only when there's no appropriate one existing in // the original VCard. public boolean isPrimary; - public ContactMethod(int kind, int type, String data, String label, - boolean isPrimary) { - this.kind = kind; + public EmailData(int type, String data, String label, boolean isPrimary) { this.type = type; this.data = data; - this.label = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EmailData) { + return false; + } + EmailData emailData = (EmailData)obj; + return (type == emailData.type && data.equals(emailData.data) && + label.equals(emailData.label) && isPrimary == emailData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + static public class PostalData { + // Determined by vCard spec. + // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name + public static final int ADDR_MAX_DATA_SIZE = 7; + private final String[] dataArray; + public final String pobox; + public final String extendedAddress; + public final String street; + public final String localty; + public final String region; + public final String postalCode; + public final String country; + + public final int type; + + // Used only when type variable is TYPE_CUSTOM. + public final String label; + + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PostalData(int type, List<String> propValueList, + String label, boolean isPrimary) { + this.type = type; + dataArray = new String[ADDR_MAX_DATA_SIZE]; + + int size = propValueList.size(); + if (size > ADDR_MAX_DATA_SIZE) { + size = ADDR_MAX_DATA_SIZE; + } + + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + // + // Use Iterator assuming List may be LinkedList, though actually it is + // always ArrayList in the current implementation. + int i = 0; + for (String addressElement : propValueList) { + dataArray[i] = addressElement; + if (++i >= size) { + break; + } + } + while (i < ADDR_MAX_DATA_SIZE) { + dataArray[i++] = null; + } + + this.pobox = dataArray[0]; + this.extendedAddress = dataArray[1]; + this.street = dataArray[2]; + this.localty = dataArray[3]; + this.region = dataArray[4]; + this.postalCode = dataArray[5]; + this.country = dataArray[6]; + + this.label = label; this.isPrimary = isPrimary; } @Override public boolean equals(Object obj) { - if (obj instanceof ContactMethod) { + if (obj instanceof PostalData) { return false; } - ContactMethod contactMethod = (ContactMethod)obj; - return (kind == contactMethod.kind && type == contactMethod.type && - data.equals(contactMethod.data) && label.equals(contactMethod.label) && - isPrimary == contactMethod.isPrimary); + PostalData postalData = (PostalData)obj; + return (Arrays.equals(dataArray, postalData.dataArray) && + (type == postalData.type && + (type == StructuredPostal.TYPE_CUSTOM ? + (label == postalData.label) : true)) && + (isPrimary == postalData.isPrimary)); + } + + public String getFormattedAddress(int vcardType) { + StringBuilder builder = new StringBuilder(); + boolean empty = true; + if (VCardConfig.isJapaneseDevice(vcardType)) { + // In Japan, the order is reversed. + for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } else { + for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } + + return builder.toString().trim(); } @Override public String toString() { - return String.format("kind: %d, type: %d, data: %s, label: %s, isPrimary: %s", - kind, type, data, label, isPrimary); + return String.format("type: %d, label: %s, isPrimary: %s", + type, label, isPrimary); } } /** - * @hide only for testing + * @hide only for testing. */ static public class OrganizationData { public final int type; @@ -161,7 +285,54 @@ public class ContactStruct { } } - static class Property { + static public class ImData { + 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) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ImData) { + return false; + } + ImData imData = (ImData)obj; + return (type == imData.type && data.equals(imData.data) && + label.equals(imData.label) && isPrimary == imData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + /** + * @hide only for testing. + */ + static public class PhotoData { + public static final String FORMAT_FLASH = "SWF"; + public final int type; + public final String formatName; // used when type is not defined in ContactsContract. + public final byte[] photoBytes; + + public PhotoData(int type, String formatName, byte[] photoBytes) { + this.type = type; + this.formatName = formatName; + this.photoBytes = photoBytes; + } + } + + static /* package */ class Property { private String mPropertyName; private Map<String, Collection<String>> mParameterMap = new HashMap<String, Collection<String>>(); @@ -178,7 +349,7 @@ public class ContactStruct { public void addParameter(final String paramName, final String paramValue) { Collection<String> values; - if (mParameterMap.containsKey(paramName)) { + if (!mParameterMap.containsKey(paramName)) { if (paramName.equals("TYPE")) { values = new HashSet<String>(); } else { @@ -188,6 +359,7 @@ public class ContactStruct { } else { values = mParameterMap.get(paramName); } + values.add(paramValue); } public void addToPropertyValueList(final String propertyValue) { @@ -213,123 +385,129 @@ public class ContactStruct { } } - private String mName; - private String mPhoneticName; - // private String mPhotoType; - private byte[] mPhotoBytes; - private List<String> mNotes; - private List<PhoneData> mPhoneList; - private List<ContactMethod> mContactMethodList; - private List<OrganizationData> mOrganizationList; - private Map<String, List<String>> mExtensionMap; + private String mFamilyName; + private String mGivenName; + private String mMiddleName; + private String mPrefix; + private String mSuffix; - private int mNameOrderType; + // Used only when no family nor given name is found. + private String mFullName; - /* private variables bellow is for temporary use. */ + private String mPhoneticFamilyName; + private String mPhoneticGivenName; + private String mPhoneticMiddleName; - // For name, there are three fields in vCard: FN, N, NAME. - // We prefer FN, which is a required field in vCard 3.0 , but not in vCard 2.1. - // Next, we prefer NAME, which is defined only in vCard 3.0. - // Finally, we use N, which is a little difficult to parse. - private String mTmpFullName; - private String mTmpNameFromNProperty; + private String mPhoneticFullName; + + private List<String> mNickNameList; - // Some vCard has "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", and - // "X-PHONETIC-LAST-NAME" - private String mTmpXPhoneticFirstName; - private String mTmpXPhoneticMiddleName; - private String mTmpXPhoneticLastName; + private String mDisplayName; + + private String mBirthday; + + private List<String> mNoteList; + private List<PhoneData> mPhoneList; + private List<EmailData> mEmailList; + private List<PostalData> mPostalList; + private List<OrganizationData> mOrganizationList; + private List<ImData> mImList; + private List<PhotoData> mPhotoList; + private List<String> mWebsiteList; + + private final int mVCardType; // Each Column of four properties has ISPRIMARY field // (See android.provider.Contacts) - // If false even after the following loop, we choose the first - // entry as a "primary" entry. + // If false even after the parsing loop, we choose the first entry as a "primary" + // entry. private boolean mPrefIsSet_Address; private boolean mPrefIsSet_Phone; private boolean mPrefIsSet_Email; private boolean mPrefIsSet_Organization; public ContactStruct() { - mNameOrderType = VCardConfig.NAME_ORDER_TYPE_DEFAULT; + this(VCardConfig.VCARD_TYPE_V21_GENERIC); } - public ContactStruct(int nameOrderType) { - mNameOrderType = nameOrderType; + public ContactStruct(int vcardType) { + mVCardType = vcardType; } - + /** - * @hide only for test + * @hide only for testing. */ - public ContactStruct(String name, - String phoneticName, - byte[] photoBytes, + public ContactStruct(String givenName, + String familyName, + String middleName, + String prefix, + String suffix, + String phoneticGivenName, + String pheneticFamilyName, + String phoneticMiddleName, + List<byte[]> photoBytesList, List<String> notes, List<PhoneData> phoneList, - List<ContactMethod> contactMethodList, + List<EmailData> emailList, + List<PostalData> postalList, List<OrganizationData> organizationList, - Map<String, List<String>> extensionMap) { - mName = name; - mPhoneticName = phoneticName; - mPhotoBytes = photoBytes; - mContactMethodList = contactMethodList; + List<PhotoData> photoList, + List<String> websiteList) { + this(VCardConfig.VCARD_TYPE_DEFAULT); + mGivenName = givenName; + mFamilyName = familyName; + mPrefix = prefix; + mSuffix = suffix; + mPhoneticGivenName = givenName; + mPhoneticFamilyName = familyName; + mPhoneticMiddleName = middleName; + mEmailList = emailList; + mPostalList = postalList; mOrganizationList = organizationList; - mExtensionMap = extensionMap; + mPhotoList = photoList; + mWebsiteList = websiteList; } /** - * @hide only for test + * @hide only for testing. */ - public String getName() { - return mName; + public final List<PhotoData> getPhotoList() { + return mPhotoList; } /** - * @hide only for test - */ - public String getPhoneticName() { - return mPhoneticName; - } - - /** - * @hide only for test - */ - public final byte[] getPhotoBytes() { - return mPhotoBytes; - } - - /** - * @hide only for test + * @hide only for testing. */ public final List<String> getNotes() { - return mNotes; + return mNoteList; } /** - * @hide only for test + * @hide only for testing. */ public final List<PhoneData> getPhoneList() { return mPhoneList; } /** - * @hide only for test + * @hide only for testing. */ - public final List<ContactMethod> getContactMethodList() { - return mContactMethodList; + public final List<EmailData> getEmailList() { + return mEmailList; } /** - * @hide only for test + * @hide only for testing. */ - public final List<OrganizationData> getOrganizationList() { - return mOrganizationList; + public final List<PostalData> getPostalList() { + return mPostalList; } - + /** - * @hide only for test + * @hide only for testing. */ - public final Map<String, List<String>> getExtensionMap() { - return mExtensionMap; + public final List<OrganizationData> getOrganizationList() { + return mOrganizationList; } /** @@ -359,32 +537,55 @@ public class ContactStruct { mPhoneList.add(phoneData); } - /** - * Add a contactmethod info to contactmethodList. - * @param kind integer value defined in Contacts.java - * (e.g. Contacts.KIND_EMAIL) - * @param type type col of content://contacts/contact_methods - * @param data contact data - * @param label extra string used only when kind is Contacts.KIND_CUSTOM. - */ - private void addContactmethod(int kind, int type, String data, - String label, boolean isPrimary){ - if (mContactMethodList == null) { - mContactMethodList = new ArrayList<ContactMethod>(); + private void addNickName(final String nickName) { + if (mNickNameList == null) { + mNickNameList = new ArrayList<String>(); } - mContactMethodList.add(new ContactMethod(kind, type, data, label, isPrimary)); + mNickNameList.add(nickName); } - /** - * Add a Organization info to organizationList. - */ - private void addOrganization(int type, String companyName, String positionName, - boolean isPrimary) { + private void addEmail(int type, String data, String label, boolean isPrimary){ + if (mEmailList == null) { + mEmailList = new ArrayList<EmailData>(); + } + mEmailList.add(new EmailData(type, data, label, isPrimary)); + } + + private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ + if (mPostalList == null) { + mPostalList = new ArrayList<PostalData>(); + } + mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); + } + + private void addOrganization(int type, final String companyName, + final String positionName, boolean isPrimary) { if (mOrganizationList == null) { mOrganizationList = new ArrayList<OrganizationData>(); } mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); } + + private void addIm(int type, String data, String label, boolean isPrimary) { + if (mImList == null) { + mImList = new ArrayList<ImData>(); + } + mImList.add(new ImData(type, data, label, isPrimary)); + } + + private void addNote(final String note) { + if (mNoteList == null) { + mNoteList = new ArrayList<String>(1); + } + mNoteList.add(note); + } + + private void addPhotoBytes(String formatName, byte[] photoBytes) { + if (mPhotoList == null) { + mPhotoList = new ArrayList<PhotoData>(1); + } + final PhotoData photoData = new PhotoData(0, null, photoBytes); + } /** * Set "position" value to the appropriate data. If there's more than one @@ -407,148 +608,66 @@ public class ContactStruct { } int size = mOrganizationList.size(); if (size == 0) { - addOrganization(Contacts.OrganizationColumns.TYPE_OTHER, "", null, false); + addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER, + "", null, false); size = 1; } OrganizationData lastData = mOrganizationList.get(size - 1); lastData.positionName = positionValue; } - private void addExtension(String propName, Map<String, Collection<String>> paramMap, - List<String> propValueList) { - if (propValueList.size() == 0) { + @SuppressWarnings("fallthrough") + private void handleNProperty(List<String> elems) { + // Family, Given, Middle, Prefix, Suffix. (1 - 5) + int size; + if (elems == null || (size = elems.size()) < 1) { return; } - // Now store the string into extensionMap. - List<String> list; - if (mExtensionMap == null) { - mExtensionMap = new HashMap<String, List<String>>(); + if (size > 5) { + size = 5; } - if (!mExtensionMap.containsKey(propName)){ - list = new ArrayList<String>(); - mExtensionMap.put(propName, list); - } else { - list = mExtensionMap.get(propName); - } - - list.add(encodeProperty(propName, paramMap, propValueList)); - } - private String encodeProperty(String propName, Map<String, Collection<String>> paramMap, - List<String> propValueList) { - // PropertyNode#toString() is for reading, not for parsing in the future. - // We construct appropriate String here. - StringBuilder builder = new StringBuilder(); - if (propName.length() > 0) { - builder.append("propName:["); - builder.append(propName); - builder.append("],"); + switch (size) { + // fallthrough + case 5: + mSuffix = elems.get(4); + case 4: + mPrefix = elems.get(3); + case 3: + mMiddleName = elems.get(2); + case 2: + mGivenName = elems.get(1); + default: + mFamilyName = elems.get(0); } - - if (paramMap.size() > 0) { - builder.append("paramMap:["); - int size = paramMap.size(); - int i = 0; - for (Map.Entry<String, Collection<String>> entry : paramMap.entrySet()) { - String key = entry.getKey(); - for (String value : entry.getValue()) { - // Assuming param-key does not contain NON-ASCII nor symbols. - // TODO: check it. - // - // According to vCard 3.0: - // param-name = iana-token / x-name - builder.append(key); - - // param-value may contain any value including NON-ASCIIs. - // We use the following replacing rule. - // \ -> \\ - // , -> \, - // In String#replaceAll(), "\\\\" means a single backslash. - builder.append("="); - - // TODO: fix this. - builder.append(value.replaceAll("\\\\", "\\\\\\\\").replaceAll(",", "\\\\,")); - if (i < size -1) { - builder.append(","); - } - i++; - } - } - - builder.append("],"); + } + + /** + * Some Japanese mobile phones use this field for phonetic name, + * since vCard 2.1 does not have "SORT-STRING" type. + * Also, in some cases, the field has some ';'s in it. + * Assume the ';' means the same meaning in N property + */ + @SuppressWarnings("fallthrough") + private void handlePhoneticNameFromSound(List<String> elems) { + // Family, Given, Middle. (1-3) + // This is not from specification but mere assumption. Some Japanese phones use this order. + int size; + if (elems == null || (size = elems.size()) < 1) { + return; } - - int size = propValueList.size(); - if (size > 0) { - builder.append("propValue:["); - List<String> list = propValueList; - for (int i = 0; i < size; i++) { - // TODO: fix this. - builder.append(list.get(i).replaceAll("\\\\", "\\\\\\\\").replaceAll(",", "\\\\,")); - if (i < size -1) { - builder.append(","); - } - } - builder.append("],"); + if (size > 3) { + size = 3; } - return builder.toString(); - } - - private static String getNameFromNProperty(List<String> elems, int nameOrderType) { - // Family, Given, Middle, Prefix, Suffix. (1 - 5) - int size = elems.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - boolean builderIsEmpty = true; - // Prefix - if (size > 3 && elems.get(3).length() > 0) { - builder.append(elems.get(3)); - builderIsEmpty = false; - } - String first, second; - if (nameOrderType == VCardConfig.NAME_ORDER_TYPE_JAPANESE) { - first = elems.get(0); - second = elems.get(1); - } else { - first = elems.get(1); - second = elems.get(0); - } - if (first.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(first); - builderIsEmpty = false; - } - // Middle name - if (size > 2 && elems.get(2).length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(elems.get(2)); - builderIsEmpty = false; - } - if (second.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(second); - builderIsEmpty = false; - } - // Suffix - if (size > 4 && elems.get(4).length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(elems.get(4)); - builderIsEmpty = false; - } - return builder.toString(); - } else if (size == 1) { - return elems.get(0); - } else { - return ""; + switch (size) { + // fallthrough + case 3: + mPhoneticMiddleName = elems.get(2); + case 2: + mPhoneticGivenName = elems.get(1); + default: + mPhoneticFamilyName = elems.get(0); } } @@ -561,41 +680,27 @@ public class ContactStruct { if (propValueList.size() == 0) { return; } - - String propValue = listToString(propValueList); - + final String propValue = listToString(propValueList).trim(); + if (propName.equals("VERSION")) { // vCard version. Ignore this. } else if (propName.equals("FN")) { - mTmpFullName = propValue; - } else if (propName.equals("NAME") && mTmpFullName == null) { - // Only in vCard 3.0. Use this if FN does not exist. - // Though, note that vCard 3.0 requires FN. - mTmpFullName = propValue; + mFullName = propValue; + } else if (propName.equals("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("N")) { - mTmpNameFromNProperty = getNameFromNProperty(propValueList, mNameOrderType); + handleNProperty(propValueList); } else if (propName.equals("SORT-STRING")) { - mPhoneticName = propValue; + mPhoneticFullName = propValue; + } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + addNickName(propValue); } else if (propName.equals("SOUND")) { - if ("X-IRMC-N".equals(paramMap.get("TYPE")) && mPhoneticName == null) { - // Some Japanese mobile phones use this field for phonetic name, - // since vCard 2.1 does not have "SORT-STRING" type. - // Also, in some cases, the field has some ';'s in it. - // We remove them. - StringBuilder builder = new StringBuilder(); - String value = propValue; - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch != ';') { - builder.append(ch); - } - } - if (builder.length() > 0) { - mPhoneticName = builder.toString(); - } + if (Constants.ATTR_TYPE_X_IRMC_N.equals(paramMap.get(Constants.ATTR_TYPE))) { + handlePhoneticNameFromSound(propValueList); } else { - addExtension(propName, paramMap, propValueList); + // Ignore this field since Android cannot understand what it is. } } else if (propName.equals("ADR")) { boolean valuesAreAllEmpty = true; @@ -609,108 +714,103 @@ public class ContactStruct { return; } - int kind = Contacts.KIND_POSTAL; int type = -1; String label = ""; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get("TYPE"); + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals("PREF") && !mPrefIsSet_Address) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { // Only first "PREF" is considered. mPrefIsSet_Address = true; isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = StructuredPostal.TYPE_HOME; label = ""; - } else if (typeString.equalsIgnoreCase("WORK") || + } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || typeString.equalsIgnoreCase("COMPANY")) { // "COMPANY" seems emitted by Windows Mobile, which is not // specifically supported by vCard 2.1. We assume this is same // as "WORK". - type = Contacts.ContactMethodsColumns.TYPE_WORK; + type = StructuredPostal.TYPE_WORK; label = ""; - } else if (typeString.equalsIgnoreCase("POSTAL")) { - kind = Contacts.KIND_POSTAL; - } else if (typeString.equalsIgnoreCase("PARCEL") || - typeString.equalsIgnoreCase("DOM") || - typeString.equalsIgnoreCase("INTL")) { - // We do not have a kind or type matching these. - // TODO: fix this. We may need to split entries into two. - // (e.g. entries for KIND_POSTAL and KIND_PERCEL) - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0) { + } else if (typeString.equals("PARCEL") || + typeString.equals("DOM") || + typeString.equals("INTL")) { + // We do not have any appropriate way to store this information. + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters // emit non-standard types. We do not handle their values now. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + type = StructuredPostal.TYPE_CUSTOM; label = typeString; } } } // We use "HOME" as default if (type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; + type = StructuredPostal.TYPE_HOME; } - - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name - String address; - int size = propValueList.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - boolean builderIsEmpty = true; - if (Locale.getDefault().getCountry().equals(Locale.JAPAN.getCountry())) { - // In Japan, the order is reversed. - for (int i = size - 1; i >= 0; i--) { - String addressPart = propValueList.get(i); - if (addressPart.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(addressPart); - builderIsEmpty = false; - } - } - } else { - for (int i = 0; i < size; i++) { - String addressPart = propValueList.get(i); - if (addressPart.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(addressPart); - builderIsEmpty = false; + + addPostal(type, propValueList, label, isPrimary); + } else if (propName.equals("EMAIL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { + // Only first "PREF" is considered. + mPrefIsSet_Email = true; + isPrimary = true; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = Email.TYPE_HOME; + } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + type = Email.TYPE_WORK; + } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + // We do not have TYPE_MOBILE yet. + // TODO: modify this code when TYPE_MOBILE is supported. + type = Email.TYPE_CUSTOM; + label = + android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); } + // vCard 3.0 allows iana-token. + // We may have INTERNET (specified in vCard spec), + // SCHOOL, etc. + type = Email.TYPE_CUSTOM; + label = typeString; } } - address = builder.toString().trim(); - } else { - address = propValue; } - addContactmethod(kind, type, address, label, isPrimary); + if (type < 0) { + type = Email.TYPE_OTHER; + } + addEmail(type, propValue, label, isPrimary); } else if (propName.equals("ORG")) { // vCard specification does not specify other types. - int type = Contacts.OrganizationColumns.TYPE_WORK; + int type = Organization.TYPE_WORK; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get("TYPE"); + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals("PREF") && !mPrefIsSet_Organization) { + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { // vCard specification officially does not have PREF in ORG. // This is just for safety. mPrefIsSet_Organization = true; isPrimary = true; } - // XXX: Should we cope with X- words? } } - int size = propValueList.size(); StringBuilder builder = new StringBuilder(); for (Iterator<String> iter = propValueList.iterator(); iter.hasNext();) { builder.append(iter.next()); @@ -718,250 +818,190 @@ public class ContactStruct { builder.append(' '); } } - addOrganization(type, builder.toString(), "", isPrimary); } else if (propName.equals("TITLE")) { setPosition(propValue); } else if (propName.equals("ROLE")) { setPosition(propValue); - } else if ((propName.equals("PHOTO") || (propName.equals("LOGO")) && mPhotoBytes == null)) { - // We prefer PHOTO to LOGO. + } else if (propName.equals("PHOTO") || propName.equals("LOGO")) { + String formatName = null; + Collection<String> typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + formatName = typeCollection.iterator().next(); + } Collection<String> paramMapValue = paramMap.get("VALUE"); if (paramMapValue != null && paramMapValue.contains("URL")) { - // TODO: do something. + // Currently we do not have appropriate example for testing this case. } else { - // Assume PHOTO is stored in BASE64. In that case, - // data is already stored in propValue_bytes in binary form. - // It should be automatically done by VBuilder (VDataBuilder/VCardDatabuilder) - mPhotoBytes = propBytes; - /* - Collection<String> typeCollection = paramMap.get("TYPE"); - if (typeCollection != null) { - if (typeCollection.size() > 1) { - StringBuilder builder = new StringBuilder(); - int size = typeCollection.size(); - int i = 0; - for (String type : typeCollection) { - builder.append(type); - if (i < size - 1) { - builder.append(','); - } - i++; - } - Log.w(LOG_TAG, "There is more than TYPE: " + builder.toString()); - } - mPhotoType = typeCollection.iterator().next(); - }*/ + addPhotoBytes(formatName, propBytes); } - } else if (propName.equals("EMAIL")) { - int type = -1; - String label = null; - boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get("TYPE"); - if (typeCollection != null) { - for (String typeString : typeCollection) { - if (typeString.equals("PREF") && !mPrefIsSet_Email) { - // Only first "PREF" is considered. - mPrefIsSet_Email = true; - isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; - } else if (typeString.equalsIgnoreCase("WORK")) { - type = Contacts.ContactMethodsColumns.TYPE_WORK; - } else if (typeString.equalsIgnoreCase("CELL")) { - // We do not have Contacts.ContactMethodsColumns.TYPE_MOBILE yet. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME; - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0) { - // vCard 3.0 allows iana-token. - // We may have INTERNET (specified in vCard spec), - // SCHOOL, etc. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString; - } - } + } else if (propName.equals("TEL")) { + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); + final int type; + final String label; + if (typeObject instanceof Integer) { + type = (Integer)typeObject; + label = null; + } else { + type = Phone.TYPE_CUSTOM; + label = typeObject.toString(); } - if (type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_OTHER; + + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; } - addContactmethod(Contacts.KIND_EMAIL, type, propValue,label, isPrimary); - } else if (propName.equals("TEL")) { - int type = -1; - String label = null; + addPhone(type, propValue, label, isPrimary); + } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { + // The phone number available via Skype. + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) + int type = Phone.TYPE_OTHER; + final String label = null; + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; + } + addPhone(type, propValue, label, isPrimary); + } else if (sImMap.containsKey(propName)){ + int type = sImMap.get(propName); boolean isPrimary = false; - boolean isFax = false; - Collection<String> typeCollection = paramMap.get("TYPE"); + final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals("PREF") && !mPrefIsSet_Phone) { - // Only first "PREF" is considered. - mPrefIsSet_Phone = true; + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.PhonesColumns.TYPE_HOME; - } else if (typeString.equalsIgnoreCase("WORK")) { - type = Contacts.PhonesColumns.TYPE_WORK; - } else if (typeString.equalsIgnoreCase("CELL")) { - type = Contacts.PhonesColumns.TYPE_MOBILE; - } else if (typeString.equalsIgnoreCase("PAGER")) { - type = Contacts.PhonesColumns.TYPE_PAGER; - } else if (typeString.equalsIgnoreCase("FAX")) { - isFax = true; - } else if (typeString.equalsIgnoreCase("VOICE") || - typeString.equalsIgnoreCase("MSG")) { - // Defined in vCard 3.0. Ignore these because they - // conflict with "HOME", "WORK", etc. - // XXX: do something? - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.PhonesColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0){ - // We may have MODEM, CAR, ISDN, etc... - type = Contacts.PhonesColumns.TYPE_CUSTOM; - label = typeString; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { + type = Phone.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { + type = Phone.TYPE_WORK; } } } if (type < 0) { - type = Contacts.PhonesColumns.TYPE_HOME; + type = Phone.TYPE_HOME; } - if (isFax) { - if (type == Contacts.PhonesColumns.TYPE_HOME) { - type = Contacts.PhonesColumns.TYPE_FAX_HOME; - } else if (type == Contacts.PhonesColumns.TYPE_WORK) { - type = Contacts.PhonesColumns.TYPE_FAX_WORK; - } - } - - addPhone(type, propValue, label, isPrimary); + addIm(type, propValue, null, isPrimary); } else if (propName.equals("NOTE")) { - if (mNotes == null) { - mNotes = new ArrayList<String>(1); + addNote(propValue); + } else if (propName.equals("URL")) { + if (mWebsiteList == null) { + mWebsiteList = new ArrayList<String>(1); } - mNotes.add(propValue); + mWebsiteList.add(propValue); + } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { + mPhoneticGivenName = propValue; + } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + mPhoneticMiddleName = propValue; + } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + mPhoneticFamilyName = propValue; } else if (propName.equals("BDAY")) { - addExtension(propName, paramMap, propValueList); - } else if (propName.equals("URL")) { - addExtension(propName, paramMap, propValueList); - } else if (propName.equals("REV")) { + mBirthday = propValue; + /*} else if (propName.equals("REV")) { // Revision of this VCard entry. I think we can ignore this. - addExtension(propName, paramMap, propValueList); } else if (propName.equals("UID")) { - addExtension(propName, paramMap, propValueList); } else if (propName.equals("KEY")) { // Type is X509 or PGP? I don't know how to handle this... - addExtension(propName, paramMap, propValueList); } else if (propName.equals("MAILER")) { - addExtension(propName, paramMap, propValueList); } else if (propName.equals("TZ")) { - addExtension(propName, paramMap, propValueList); } else if (propName.equals("GEO")) { - addExtension(propName, paramMap, propValueList); - } else if (propName.equals("NICKNAME")) { - // vCard 3.0 only. - addExtension(propName, paramMap, propValueList); } else if (propName.equals("CLASS")) { // vCard 3.0 only. // e.g. CLASS:CONFIDENTIAL - addExtension(propName, paramMap, propValueList); } else if (propName.equals("PROFILE")) { // VCard 3.0 only. Must be "VCARD". I think we can ignore this. - addExtension(propName, paramMap, propValueList); } else if (propName.equals("CATEGORIES")) { // VCard 3.0 only. // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY - addExtension(propName, paramMap, propValueList); } else if (propName.equals("SOURCE")) { // VCard 3.0 only. - addExtension(propName, paramMap, propValueList); } else if (propName.equals("PRODID")) { // VCard 3.0 only. // To specify the identifier for the product that created - // the vCard object. - addExtension(propName, paramMap, propValueList); - } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { - mTmpXPhoneticFirstName = propValue; - } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { - mTmpXPhoneticMiddleName = propValue; - } else if (propName.equals("X-PHONETIC-LAST-NAME")) { - mTmpXPhoneticLastName = propValue; + // the vCard object.*/ } else { // Unknown X- words and IANA token. - addExtension(propName, paramMap, propValueList); } } - public String displayString() { - if (mName.length() > 0) { - return mName; - } - if (mContactMethodList != null && mContactMethodList.size() > 0) { - for (ContactMethod contactMethod : mContactMethodList) { - if (contactMethod.kind == Contacts.KIND_EMAIL && contactMethod.isPrimary) { - return contactMethod.data; + public String getDisplayName() { + if (mDisplayName == null) { + constructDisplayName(); + } + return mDisplayName; + } + + /** + * Construct the display name. The constructed data must not be null. + */ + private void constructDisplayName() { + if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { + StringBuilder builder = new StringBuilder(); + List<String> nameList; + switch (VCardConfig.getNameOrderType(mVCardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + if (VCardUtils.containsOnlyAscii(mFamilyName) && + VCardUtils.containsOnlyAscii(mGivenName)) { + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + } else { + nameList = Arrays.asList(mPrefix, mFamilyName, mMiddleName, mGivenName, mSuffix); } + break; + case VCardConfig.NAME_ORDER_EUROPE: + nameList = Arrays.asList(mPrefix, mMiddleName, mGivenName, mFamilyName, mSuffix); + break; + default: + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + break; } - } - if (mPhoneList != null && mPhoneList.size() > 0) { - for (PhoneData phoneData : mPhoneList) { - if (phoneData.isPrimary) { - return phoneData.data; + boolean first = true; + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); } } + mDisplayName = builder.toString(); + } else if (!TextUtils.isEmpty(mFullName)) { + mDisplayName = mFullName; + } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && + TextUtils.isEmpty(mPhoneticGivenName))) { + mDisplayName = VCardUtils.constructNameFromElements(mVCardType, + mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName); + } else if (mEmailList != null && mEmailList.size() > 0) { + mDisplayName = mEmailList.get(0).data; + } else if (mPhoneList != null && mPhoneList.size() > 0) { + mDisplayName = mPhoneList.get(0).data; + } else if (mPostalList != null && mPostalList.size() > 0) { + mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); } - return ""; - } + if (mDisplayName == null) { + mDisplayName = ""; + } + } + /** * Consolidate several fielsds (like mName) using name candidates, */ public void consolidateFields() { - if (mTmpFullName != null) { - mName = mTmpFullName; - } else if(mTmpNameFromNProperty != null) { - mName = mTmpNameFromNProperty; - } else { - mName = ""; - } - - if (mPhoneticName == null && - (mTmpXPhoneticFirstName != null || mTmpXPhoneticMiddleName != null || - mTmpXPhoneticLastName != null)) { - // Note: In Europe, this order should be "LAST FIRST MIDDLE". See the comment around - // NAME_ORDER_TYPE_* for more detail. - String first; - String second; - if (mNameOrderType == VCardConfig.NAME_ORDER_TYPE_JAPANESE) { - first = mTmpXPhoneticLastName; - second = mTmpXPhoneticFirstName; - } else { - first = mTmpXPhoneticFirstName; - second = mTmpXPhoneticLastName; - } - StringBuilder builder = new StringBuilder(); - if (first != null) { - builder.append(first); - } - if (mTmpXPhoneticMiddleName != null) { - builder.append(mTmpXPhoneticMiddleName); - } - if (second != null) { - builder.append(second); - } - mPhoneticName = builder.toString(); - } + constructDisplayName(); - // Remove unnecessary white spaces. - // It is found that some mobile phone emits phonetic name with just one white space - // when a user does not specify one. - // This logic is effective toward such kind of weird data. - if (mPhoneticName != null) { - mPhoneticName = mPhoneticName.trim(); + if (mPhoneticFullName != null) { + mPhoneticFullName = mPhoneticFullName.trim(); } // If there is no "PREF", we choose the first entries as primary. @@ -969,258 +1009,196 @@ public class ContactStruct { mPhoneList.get(0).isPrimary = true; } - if (!mPrefIsSet_Address && mContactMethodList != null) { - for (ContactMethod contactMethod : mContactMethodList) { - if (contactMethod.kind == Contacts.KIND_POSTAL) { - contactMethod.isPrimary = true; - break; - } - } + if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) { + mPostalList.get(0).isPrimary = true; } - if (!mPrefIsSet_Email && mContactMethodList != null) { - for (ContactMethod contactMethod : mContactMethodList) { - if (contactMethod.kind == Contacts.KIND_EMAIL) { - contactMethod.isPrimary = true; - break; - } - } + if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) { + mEmailList.get(0).isPrimary = true; } if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) { mOrganizationList.get(0).isPrimary = true; } - } - private void pushIntoContentProviderOrResolver(Object contentSomething, - long myContactsGroupId) { - ContentResolver resolver = null; - AbstractSyncableContentProvider provider = null; - if (contentSomething instanceof ContentResolver) { - resolver = (ContentResolver)contentSomething; - } else if (contentSomething instanceof AbstractSyncableContentProvider) { - provider = (AbstractSyncableContentProvider)contentSomething; - } else { - Log.e(LOG_TAG, "Unsupported object came."); - return; - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(People.NAME, mName); - contentValues.put(People.PHONETIC_NAME, mPhoneticName); - - if (mNotes != null && mNotes.size() > 0) { - if (mNotes.size() > 1) { - StringBuilder builder = new StringBuilder(); - for (String note : mNotes) { - builder.append(note); - builder.append("\n"); - } - contentValues.put(People.NOTES, builder.toString()); - } else { - contentValues.put(People.NOTES, mNotes.get(0)); - } - } + public void pushIntoContentResolver(ContentResolver resolver) { + ArrayList<ContentProviderOperation> operationList = + new ArrayList<ContentProviderOperation>(); + ContentProviderOperation.Builder builder = + ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); + builder.withValues(new ContentValues()); + operationList.add(builder.build()); - Uri personUri; - long personId = 0; - if (resolver != null) { - personUri = Contacts.People.createPersonInMyContactsGroup(resolver, contentValues); - if (personUri != null) { - personId = ContentUris.parseId(personUri); - } - } else { - personUri = provider.insert(People.CONTENT_URI, contentValues); - if (personUri != null) { - personId = ContentUris.parseId(personUri); - ContentValues values = new ContentValues(); - values.put(GroupMembership.PERSON_ID, personId); - values.put(GroupMembership.GROUP_ID, myContactsGroupId); - Uri resultUri = provider.insert(GroupMembership.CONTENT_URI, values); - if (resultUri == null) { - Log.e(LOG_TAG, "Faild to insert the person to MyContact."); - provider.delete(personUri, null, null); - personUri = null; - } - } - } + { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); - if (personUri == null) { - Log.e(LOG_TAG, "Failed to create the contact."); - return; + builder.withValue(StructuredName.GIVEN_NAME, mGivenName); + builder.withValue(StructuredName.FAMILY_NAME, mFamilyName); + builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName); + builder.withValue(StructuredName.PREFIX, mPrefix); + builder.withValue(StructuredName.SUFFIX, mSuffix); + + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); + builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); + builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + + builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); + operationList.add(builder.build()); } - - if (mPhotoBytes != null) { - if (resolver != null) { - People.setPhotoData(resolver, personUri, mPhotoBytes); - } else { - Uri photoUri = Uri.withAppendedPath(personUri, Contacts.Photos.CONTENT_DIRECTORY); - ContentValues values = new ContentValues(); - values.put(Photos.DATA, mPhotoBytes); - provider.update(photoUri, values, null, null); + + if (mNickNameList != null && mNickNameList.size() > 0) { + boolean first = true; + for (String nickName : mNickNameList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); + + builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); + builder.withValue(Nickname.NAME, nickName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); } } - long primaryPhoneId = -1; - if (mPhoneList != null && mPhoneList.size() > 0) { + if (mPhoneList != null) { for (PhoneData phoneData : mPhoneList) { - ContentValues values = new ContentValues(); - values.put(Contacts.PhonesColumns.TYPE, phoneData.type); - if (phoneData.type == Contacts.PhonesColumns.TYPE_CUSTOM) { - values.put(Contacts.PhonesColumns.LABEL, phoneData.label); - } - // Already formatted. - values.put(Contacts.PhonesColumns.NUMBER, phoneData.data); - - // Not sure about Contacts.PhonesColumns.NUMBER_KEY ... - values.put(Contacts.PhonesColumns.ISPRIMARY, 1); - values.put(Contacts.Phones.PERSON_ID, personId); - Uri phoneUri; - if (resolver != null) { - phoneUri = resolver.insert(Phones.CONTENT_URI, values); - } else { - phoneUri = provider.insert(Phones.CONTENT_URI, values); + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); + + builder.withValue(Phone.TYPE, phoneData.type); + if (phoneData.type == Phone.TYPE_CUSTOM) { + builder.withValue(Phone.LABEL, phoneData.label); } + builder.withValue(Phone.NUMBER, phoneData.data); if (phoneData.isPrimary) { - primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment()); + builder.withValue(Data.IS_PRIMARY, 1); } + operationList.add(builder.build()); } } - long primaryOrganizationId = -1; - if (mOrganizationList != null && mOrganizationList.size() > 0) { + if (mOrganizationList != null) { + boolean first = true; for (OrganizationData organizationData : mOrganizationList) { - ContentValues values = new ContentValues(); + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); + // Currently, we do not use TYPE_CUSTOM. - values.put(Contacts.OrganizationColumns.TYPE, - organizationData.type); - values.put(Contacts.OrganizationColumns.COMPANY, - organizationData.companyName); - values.put(Contacts.OrganizationColumns.TITLE, - organizationData.positionName); - values.put(Contacts.OrganizationColumns.ISPRIMARY, 1); - values.put(Contacts.OrganizationColumns.PERSON_ID, personId); - - Uri organizationUri; - if (resolver != null) { - organizationUri = resolver.insert(Organizations.CONTENT_URI, values); - } else { - organizationUri = provider.insert(Organizations.CONTENT_URI, values); - } - if (organizationData.isPrimary) { - primaryOrganizationId = Long.parseLong(organizationUri.getLastPathSegment()); + builder.withValue(Organization.TYPE, organizationData.type); + builder.withValue(Organization.COMPANY, organizationData.companyName); + builder.withValue(Organization.TITLE, organizationData.positionName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); } + operationList.add(builder.build()); } } - long primaryEmailId = -1; - if (mContactMethodList != null && mContactMethodList.size() > 0) { - for (ContactMethod contactMethod : mContactMethodList) { - ContentValues values = new ContentValues(); - values.put(Contacts.ContactMethodsColumns.KIND, contactMethod.kind); - values.put(Contacts.ContactMethodsColumns.TYPE, contactMethod.type); - if (contactMethod.type == Contacts.ContactMethodsColumns.TYPE_CUSTOM) { - values.put(Contacts.ContactMethodsColumns.LABEL, contactMethod.label); + if (mEmailList != null) { + for (EmailData emailData : mEmailList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Email.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); + + builder.withValue(Email.TYPE, emailData.type); + if (emailData.type == Email.TYPE_CUSTOM) { + builder.withValue(Email.LABEL, emailData.label); } - values.put(Contacts.ContactMethodsColumns.DATA, contactMethod.data); - values.put(Contacts.ContactMethodsColumns.ISPRIMARY, 1); - values.put(Contacts.ContactMethods.PERSON_ID, personId); - - if (contactMethod.kind == Contacts.KIND_EMAIL) { - Uri emailUri; - if (resolver != null) { - emailUri = resolver.insert(ContactMethods.CONTENT_URI, values); - } else { - emailUri = provider.insert(ContactMethods.CONTENT_URI, values); - } - if (contactMethod.isPrimary) { - primaryEmailId = Long.parseLong(emailUri.getLastPathSegment()); - } - } else { // probably KIND_POSTAL - if (resolver != null) { - resolver.insert(ContactMethods.CONTENT_URI, values); - } else { - provider.insert(ContactMethods.CONTENT_URI, values); - } + builder.withValue(Email.DATA, emailData.data); + if (emailData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); } + operationList.add(builder.build()); } } - if (mExtensionMap != null && mExtensionMap.size() > 0) { - ArrayList<ContentValues> contentValuesArray; - if (resolver != null) { - contentValuesArray = new ArrayList<ContentValues>(); - } else { - contentValuesArray = null; + if (mPostalList != null) { + for (PostalData postalData : mPostalList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + VCardUtils.insertStructuredPostalDataUsingContactsStruct( + mVCardType, builder, postalData); + operationList.add(builder.build()); } - for (Entry<String, List<String>> entry : mExtensionMap.entrySet()) { - String key = entry.getKey(); - List<String> list = entry.getValue(); - for (String value : list) { - ContentValues values = new ContentValues(); - values.put(Extensions.NAME, key); - values.put(Extensions.VALUE, value); - values.put(Extensions.PERSON_ID, personId); - if (resolver != null) { - contentValuesArray.add(values); - } else { - provider.insert(Extensions.CONTENT_URI, values); - } + } + + if (mImList != null) { + for (ImData imData : mImList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + 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.DATA, imData.data); + if (imData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); } - } - if (resolver != null) { - resolver.bulkInsert(Extensions.CONTENT_URI, - contentValuesArray.toArray(new ContentValues[0])); } } - if (primaryPhoneId >= 0 || primaryOrganizationId >= 0 || primaryEmailId >= 0) { - ContentValues values = new ContentValues(); - if (primaryPhoneId >= 0) { - values.put(People.PRIMARY_PHONE_ID, primaryPhoneId); - } - if (primaryOrganizationId >= 0) { - values.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId); + if (mNoteList != null) { + for (String note : mNoteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); + + builder.withValue(Note.NOTE, note); + operationList.add(builder.build()); } - if (primaryEmailId >= 0) { - values.put(People.PRIMARY_EMAIL_ID, primaryEmailId); + } + + if (mPhotoList != null) { + boolean first = true; + for (PhotoData photoData : mPhotoList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); + builder.withValue(Photo.PHOTO, photoData.photoBytes); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); } - if (resolver != null) { - resolver.update(personUri, values, null, null); - } else { - provider.update(personUri, values, null, null); + } + + if (mWebsiteList != null) { + for (String website : mWebsiteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Website.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); + builder.withValue(Website.URL, website); + operationList.add(builder.build()); } } - } - - /** - * Push this object into database in the resolver. - */ - public void pushIntoContentResolver(ContentResolver resolver) { - pushIntoContentProviderOrResolver(resolver, 0); - } - - /** - * Push this object into AbstractSyncableContentProvider object. - * {@link #consolidateFields() must be called before this method is called} - * @hide - */ - public void pushIntoAbstractSyncableContentProvider( - AbstractSyncableContentProvider provider, long myContactsGroupId) { - boolean successful = false; - provider.beginBatch(); + + if (!TextUtils.isEmpty(mBirthday)) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Miscellaneous.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Miscellaneous.CONTENT_ITEM_TYPE); + builder.withValue(Miscellaneous.BIRTHDAY, mBirthday); + operationList.add(builder.build()); + } + try { - pushIntoContentProviderOrResolver(provider, myContactsGroupId); - successful = true; - } finally { - provider.endBatch(successful); + resolver.applyBatch(ContactsContract.AUTHORITY, operationList); + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); } } - + public boolean isIgnorable() { - return TextUtils.isEmpty(mName) && - TextUtils.isEmpty(mPhoneticName) && - (mPhoneList == null || mPhoneList.size() == 0) && - (mContactMethodList == null || mContactMethodList.size() == 0); + return getDisplayName().length() == 0; } private String listToString(List<String> list){ diff --git a/core/java/android/pim/vcard/EntryCommitter.java b/core/java/android/pim/vcard/EntryCommitter.java index e26fac5..3f1655d 100644 --- a/core/java/android/pim/vcard/EntryCommitter.java +++ b/core/java/android/pim/vcard/EntryCommitter.java @@ -15,11 +15,7 @@ */ package android.pim.vcard; -import android.content.AbstractSyncableContentProvider; -import android.content.ContentProvider; import android.content.ContentResolver; -import android.content.IContentProvider; -import android.provider.Contacts; import android.util.Log; /** @@ -27,62 +23,26 @@ import android.util.Log; */ public class EntryCommitter implements EntryHandler { public static String LOG_TAG = "vcard.EntryComitter"; - + private ContentResolver mContentResolver; - - // Ideally, this should be ContactsProvider but it seems Class loader cannot find it, - // even when it is subclass of ContactsProvider... - private AbstractSyncableContentProvider mProvider; - private long mMyContactsGroupId; - private long mTimeToCommit; public EntryCommitter(ContentResolver resolver) { mContentResolver = resolver; - - tryGetOriginalProvider(); + } + + public void onParsingStart() { } - public void onFinal() { + public void onParsingEnd() { if (VCardConfig.showPerformanceLog()) { - Log.d(LOG_TAG, - String.format("time to commit entries: %ld ms", mTimeToCommit)); + Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); } } - - private void tryGetOriginalProvider() { - final ContentResolver resolver = mContentResolver; - - if ((mMyContactsGroupId = Contacts.People.tryGetMyContactsGroupId(resolver)) == 0) { - Log.e(LOG_TAG, "Could not get group id of MyContact"); - return; - } - IContentProvider iProviderForName = resolver.acquireProvider(Contacts.CONTENT_URI); - ContentProvider contentProvider = - ContentProvider.coerceToLocalContentProvider(iProviderForName); - if (contentProvider == null) { - Log.e(LOG_TAG, "Fail to get ContentProvider object."); - return; - } - - if (!(contentProvider instanceof AbstractSyncableContentProvider)) { - Log.e(LOG_TAG, - "Acquired ContentProvider object is not AbstractSyncableContentProvider."); - return; - } - - mProvider = (AbstractSyncableContentProvider)contentProvider; - } - public void onEntryCreated(final ContactStruct contactStruct) { long start = System.currentTimeMillis(); - if (mProvider != null) { - contactStruct.pushIntoAbstractSyncableContentProvider( - mProvider, mMyContactsGroupId); - } else { - contactStruct.pushIntoContentResolver(mContentResolver); - } + contactStruct.pushIntoContentResolver(mContentResolver); mTimeToCommit += System.currentTimeMillis() - start; } }
\ No newline at end of file diff --git a/core/java/android/pim/vcard/EntryHandler.java b/core/java/android/pim/vcard/EntryHandler.java index 4015cb5..7fb8114 100644 --- a/core/java/android/pim/vcard/EntryHandler.java +++ b/core/java/android/pim/vcard/EntryHandler.java @@ -16,18 +16,23 @@ package android.pim.vcard; /** - * Unlike VCardBuilderBase, this (and VCardDataBuilder) assumes + * Unlike {@link VCardBuilder}, this (and {@link VCardDataBuilder}) assumes * "each VCard entry should be correctly parsed and passed to each EntryHandler object", */ public interface EntryHandler { /** - * Able to be use this method for showing performance log, etc. - * TODO: better name? + * Called when the parsing started. */ - public void onFinal(); + public void onParsingStart(); /** * The method called when one VCard entry is successfully created */ public void onEntryCreated(final ContactStruct entry); + + /** + * Called when the parsing ended. + * Able to be use this method for showing performance log, etc. + */ + public void onParsingEnd(); } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java new file mode 100644 index 0000000..283d00b --- /dev/null +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -0,0 +1,1433 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.pim.vcard; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.EntityIterator; +import android.content.Entity.NamedContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.os.RemoteException; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Miscellaneous; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.text.TextUtils; +import android.util.CharsetUtils; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <p> + * The class for composing VCard from Contacts information. Note that this is + * completely differnt implementation from + * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. + * </p> + * + * <p> + * Usually, this class should be used like this. + * </p> + * + * <pre class="prettyprint"> VCardComposer composer = null; try { composer = new + * VCardComposer(context); composer.addHandler(composer.new + * HandlerForOutputStream(outputStream)); if (!composer.init()) { // Do + * something handling the situation. return; } while (!composer.isAfterLast()) { + * if (mCanceled) { // Assume a user may cancel this operation during the + * export. return; } if (!composer.createOneEntry()) { // Do something handling + * the error situation. return; } } } finally { if (composer != null) { + * composer.terminate(); } } </pre> + */ +public class VCardComposer { + private static final String LOG_TAG = "vcard.VCardComposer"; + + public static interface OneEntryHandler { + public boolean onInit(Context context); + + public boolean onEntryCreated(String vcard); + + public void onTerminate(); + } + + /** + * <p> + * An useful example handler, which emits VCard String to outputstream one + * by one. + * </p> + * <p> + * The input OutputStream object is closed() on {{@link #onTerminate()}. + * Must not close the stream outside. + * </p> + */ + public class HandlerForOutputStream implements OneEntryHandler { + @SuppressWarnings("hiding") + private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; + + private OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + + private boolean mFinishIsCalled = false; + + /** + * Input stream will be closed on the detruction of this object. + */ + public HandlerForOutputStream(OutputStream outputStream) { + mOutputStream = outputStream; + } + + public boolean onInit(Context context) { + try { + mWriter = new BufferedWriter(new OutputStreamWriter( + mOutputStream, mCharsetString)); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); + mErrorReason = "Encoding is not supported (usually this does not happen!): " + + mCharsetString; + return false; + } + + if (mIsDoCoMo) { + try { + // Create one empty entry. + mWriter.write(createOneEntryInternal("-1")); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + } + return true; + } + + public boolean onEntryCreated(String vcard) { + try { + mWriter.write(vcard); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + return true; + } + + public void onTerminate() { + if (mWriter != null) { + try { + // Flush and sync the data so that a user is able to pull + // the SDCard just after + // the export. + mWriter.flush(); + if (mOutputStream != null + && mOutputStream instanceof FileOutputStream) { + ((FileOutputStream) mOutputStream).getFD().sync(); + } + } catch (IOException e) { + Log.d(LOG_TAG, + "IOException during closing the output stream: " + + e.getMessage()); + } finally { + try { + mWriter.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void finalize() { + if (!mFinishIsCalled) { + onTerminate(); + } + } + } + + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + private static final String VCARD_PROPERTY_ADR = "ADR"; + private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; + private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; + private static final String VCARD_PROPERTY_END = "END"; + private static final String VCARD_PROPERTY_NAME = "N"; + private static final String VCARD_PROPERTY_FULL_NAME = "FN"; + private static final String VCARD_PROPERTY_NOTE = "NOTE"; + private static final String VCARD_PROPERTY_ORG = "ORG"; + private static final String VCARD_PROPERTY_SOUND = "SOUND"; + private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; + private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; + private static final String VCARD_PROPERTY_TEL = "TEL"; + private static final String VCARD_PROPERTY_TITLE = "TITLE"; + private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; + private static final String VCARD_PROPERTY_VERSION = "VERSION"; + private static final String VCARD_PROPERTY_URL = "URL"; + private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; + + private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Android specific properties + private static final String VCARD_PROPERTY_X_PHONETIC_NAME = "X-PHONETIC-NAME"; + private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; + // TODO: add properties like X-LATITUDE + + // Properties for DoCoMo vCard. + private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; + private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; + private static final String VCARD_PROPERTY_X_NO = "X-NO"; + private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + + private static final String VCARD_DATA_VCARD = "VCARD"; + private static final String VCARD_DATA_PUBLIC = "PUBLIC"; + + private static final String VCARD_ATTR_SEPARATOR = ";"; + private static final String VCARD_COL_SEPARATOR = "\r\n"; + private static final String VCARD_DATA_SEPARATOR = ":"; + private static final String VCARD_ITEM_SEPARATOR = ";"; + private static final String VCARD_WS = " "; + + // Type strings are now in VCardConstants.java. + + private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; + + private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; + private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + + private final Context mContext; + private final int mVCardType; + private final boolean mCareHandlerErrors; + private final ContentResolver mContentResolver; + + // Convenient member variables about the restriction of the vCard format. + // Used for not calling the same methods returning same results. + private final boolean mIsV30; + private final boolean mIsJapaneseMobilePhone; + private final boolean mOnlyOneNoteFieldIsAvailable; + private final boolean mIsDoCoMo; + private final boolean mUsesQuotedPrintable; + private final boolean mUsesAndroidProperty; + private final boolean mUsesDefactProperty; + private final boolean mUsesShiftJis; + + private Cursor mCursor; + private int mIdColumn; + + private String mCharsetString; + private static String mVCardAttributeCharset; + private boolean mTerminateIsCalled; + private List<OneEntryHandler> mHandlerList; + + private String mErrorReason = "No error"; + + private static final Map<Integer, String> sImMap; + + static { + sImMap = new HashMap<Integer, String>(); + sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + // Google talk is a special case. + } + + + public VCardComposer(Context context) { + this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); + } + + public VCardComposer(Context context, String vcardTypeStr, + boolean careHandlerErrors) { + this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), + careHandlerErrors); + } + + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { + mContext = context; + mVCardType = vcardType; + mCareHandlerErrors = careHandlerErrors; + mContentResolver = context.getContentResolver(); + + mIsV30 = VCardConfig.isV30(vcardType); + mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mIsJapaneseMobilePhone = VCardConfig + .needsToConvertPhoneticString(vcardType); + mOnlyOneNoteFieldIsAvailable = VCardConfig + .onlyOneNoteFieldIsAvailable(vcardType); + mUsesAndroidProperty = VCardConfig + .usesAndroidSpecificProperty(vcardType); + mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); + mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); + + if (mIsDoCoMo) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but + // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in + // Android, not shown to the public). + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else if (mUsesShiftJis) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else { + mCharsetString = "UTF-8"; + mVCardAttributeCharset = "CHARSET=UTF-8"; + } + } + + /** + * Must call before {{@link #init()}. + */ + public void addHandler(OneEntryHandler handler) { + if (mHandlerList == null) { + mHandlerList = new ArrayList<OneEntryHandler>(); + } + mHandlerList.add(handler); + } + + public boolean init() { + return init(null, null); + } + + /** + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. + */ + public boolean init(final String selection, final String[] selectionArgs) { + if (mCareHandlerErrors) { + List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onInit(mContext)) { + for (OneEntryHandler finished : finishedList) { + finished.onTerminate(); + } + return false; + } + } + } else { + // Just ignore the false returned from onInit(). + for (OneEntryHandler handler : mHandlerList) { + handler.onInit(mContext); + } + } + + final String[] projection = new String[] {Contacts._ID,}; + + // TODO: thorow an appropriate exception! + mCursor = mContentResolver.query(RawContacts.CONTENT_URI, projection, + selection, selectionArgs, null); + if (mCursor == null || !mCursor.moveToFirst()) { + if (mCursor != null) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + + e.getMessage()); + } + mCursor = null; + } + mErrorReason = "Getting database information failed."; + return false; + } + + mIdColumn = mCursor.getColumnIndex(Contacts._ID); + + return true; + } + + public boolean createOneEntry() { + if (mCursor == null || mCursor.isAfterLast()) { + // TODO: ditto + mErrorReason = "Not initialized or database has some problem."; + return false; + } + String name = null; + String vcard; + try { + vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); + } catch (OutOfMemoryError error) { + // Maybe some data (e.g. photo) is too big to have in memory. But it + // should be rare. + Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + + name); + System.gc(); + // TODO: should tell users what happened? + return true; + } finally { + mCursor.moveToNext(); + } + + // This function does not care the OutOfMemoryError on the handler side + // :-P + if (mCareHandlerErrors) { + List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onEntryCreated(vcard)) { + return false; + } + } + } else { + for (OneEntryHandler handler : mHandlerList) { + handler.onEntryCreated(vcard); + } + } + + return true; + } + + private String createOneEntryInternal(final String contactId) { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + + final Map<String, List<ContentValues>> contentValuesListMap = + new HashMap<String, List<ContentValues>>(); + + final String selection = Data.RAW_CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; + EntityIterator entityIterator = null; + try { + entityIterator = mContentResolver.queryEntities( + RawContacts.CONTENT_URI, selection, selectionArgs, null); + while (entityIterator.hasNext()) { + Entity entity = entityIterator.next(); + for (NamedContentValues namedContentValues : entity + .getSubValues()) { + ContentValues contentValues = namedContentValues.values; + String key = contentValues.getAsString(Data.MIMETYPE); + if (key != null) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(key); + if (contentValuesList == null) { + contentValuesList = new ArrayList<ContentValues>(); + contentValuesListMap.put(key, contentValuesList); + } + contentValuesList.add(contentValues); + } + } + } + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)", + contactId, e.getMessage())); + return ""; + } finally { + if (entityIterator != null) { + entityIterator.close(); + } + } + + // TODO: consolidate order? (low priority) + 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... What? + + if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); + } + + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + public void terminate() { + for (OneEntryHandler handler : mHandlerList) { + handler.onTerminate(); + } + + if (mCursor != null) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + + e.getMessage()); + } + mCursor = null; + } + + mTerminateIsCalled = true; + } + + @Override + public void finalize() { + if (!mTerminateIsCalled) { + terminate(); + } + } + + public int getCount() { + if (mCursor == null) { + return 0; + } + return mCursor.getCount(); + } + + public boolean isAfterLast() { + if (mCursor == null) { + return false; + } + return mCursor.isAfterLast(); + } + + /** + * @return Return the error reason if possible. + */ + public String getErrorReason() { + return mErrorReason; + } + + private void appendStructuredNames(StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(StructuredName.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + appendStructuredNamesInternal(builder, contentValuesList); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } + } + + private void appendStructuredNamesInternal(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + for (ContentValues contentValues : 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); + + // For now, some primary element is not encoded into Quoted-Printable, which is not + // valid in vCard spec strictly. In the future, we may have to have some flag to + // enable composer to encode these primary field into Quoted-Printable. + if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final String encodedFamily = escapeCharacters(familyName); + final String encodedGiven = escapeCharacters(givenName); + final String encodedMiddle = escapeCharacters(middleName); + final String encodedPrefix = escapeCharacters(prefix); + final String encodedSuffix = escapeCharacters(suffix); + + // N property. This order is specified by vCard spec and does not depend on countries. + builder.append(VCARD_PROPERTY_NAME); + if (!(VCardUtils.containsOnlyAscii(familyName) && + VCardUtils.containsOnlyAscii(givenName) && + VCardUtils.containsOnlyAscii(middleName) && + VCardUtils.containsOnlyAscii(prefix) && + VCardUtils.containsOnlyAscii(suffix))) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFamily); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedGiven); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedMiddle); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPrefix); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedSuffix); + builder.append(VCARD_COL_SEPARATOR); + + final String encodedFullname = VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); + + // FN property + builder.append(VCARD_PROPERTY_FULL_NAME); + builder.append(VCARD_ATTR_SEPARATOR); + if (!VCardUtils.containsOnlyAscii(encodedFullname)) { + builder.append(mVCardAttributeCharset); + builder.append(VCARD_DATA_SEPARATOR); + } + builder.append(encodedFullname); + builder.append(VCARD_COL_SEPARATOR); + } else if (!TextUtils.isEmpty(displayName)) { + builder.append(VCARD_PROPERTY_NAME); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(escapeCharacters(displayName)); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } + + String phoneticFamilyName = contentValues + .getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String phoneticMiddleName = contentValues + .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String phoneticGivenName = contentValues + .getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (!(TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) && TextUtils + .isEmpty(phoneticGivenName))) { // if not empty + if (mIsJapaneseMobilePhone) { + phoneticFamilyName = VCardUtils + .toHalfWidthString(phoneticFamilyName); + phoneticMiddleName = VCardUtils + .toHalfWidthString(phoneticMiddleName); + phoneticGivenName = VCardUtils + .toHalfWidthString(phoneticGivenName); + } + + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, phoneticMiddleName, + phoneticGivenName); + builder.append(VCARD_PROPERTY_SORT_STRING); + + if (!VCardUtils.containsOnlyAscii(sortString)) { + // Strictly, adding charset information is NOT valid in + // VCard 3.0, + // but we'll add this info since parser side may be able to + // use the charset via + // this attribute field. + // + // 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. + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(sortString); + builder.append(VCARD_COL_SEPARATOR); + } else { + // Note: There is no appropriate property for expressing + // phonetic name in + // VCard 2.1, while there is in VCard 3.0 (SORT-STRING). + // We chose to use DoCoMo's way since it is supported by a + // lot of + // Japanese mobile phones. + // + // TODO: should use Quoted-Pritable? + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(VCARD_ATTR_SEPARATOR); + + if (!(VCardUtils.containsOnlyAscii(phoneticFamilyName) && + VCardUtils.containsOnlyAscii(phoneticMiddleName) && + VCardUtils.containsOnlyAscii(phoneticGivenName))) { + builder.append(mVCardAttributeCharset); + builder.append(VCARD_DATA_SEPARATOR); + } + + builder.append(escapeCharacters(phoneticFamilyName)); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(escapeCharacters(phoneticMiddleName)); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(escapeCharacters(phoneticGivenName)); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + + if (mUsesAndroidProperty) { + final String phoneticName = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, phoneticMiddleName, + phoneticGivenName); + builder.append(VCARD_PROPERTY_X_PHONETIC_NAME); + + if (!VCardUtils.containsOnlyAscii(phoneticName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + builder.append(VCARD_DATA_SEPARATOR); + // TODO: may need to make the text quoted-printable. + builder.append(phoneticName); + builder.append(VCARD_COL_SEPARATOR); + } + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + if (mUsesDefactProperty) { + if (!TextUtils.isEmpty(phoneticGivenName)) { + builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(phoneticGivenName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticMiddleName)) { + builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(phoneticMiddleName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticFamilyName)) { + builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(phoneticFamilyName); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + } + + private void appendNickNames(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Nickname.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + final String propertyNickname; + if (mIsV30) { + propertyNickname = VCARD_PROPERTY_NICKNAME; + } else if (mUsesAndroidProperty) { + propertyNickname = VCARD_PROPERTY_X_NICKNAME; + } else { + // There's no way to add this field. + return; + } + + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues + .getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + builder.append(propertyNickname); + + if (!VCardUtils.containsOnlyAscii(propertyNickname)) { + // Strictly, this is not valid in vCard 3.0. See above. + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(escapeCharacters(nickname)); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + + private void appendPhones(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Phone.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + appendVCardTelephoneLine(builder, contentValues + .getAsInteger(Phone.TYPE), contentValues + .getAsString(Phone.LABEL), contentValues + .getAsString(Phone.NUMBER)); + } + } else if (mIsDoCoMo) { + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + } + } + + private void appendEmails(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Email.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + appendVCardEmailLine(builder, contentValues + .getAsInteger(Email.TYPE), contentValues + .getAsString(Email.LABEL), contentValues + .getAsString(Email.DATA)); + } + } else if (mIsDoCoMo) { + appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + } + } + + private void appendPostals(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(StructuredPostal.CONTENT_ITEM_TYPE); + + if (contentValuesList != null) { + if (mIsDoCoMo) { + appendPostalsForDoCoMo(builder, contentValuesList); + } else { + appendPostalsForGeneric(builder, contentValuesList); + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_HOME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + } + + /** + * Try to append just one line. If there's no appropriate address + * information, append an empty line. + */ + private void appendPostalsForDoCoMo(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + // TODO: from old, inefficient code. fix this. + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_HOME)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_WORK)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_OTHER)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_CUSTOM)) { + return; + } + + Log.w(LOG_TAG, + "Should not come here. Must have at least one postal data."); + } + + private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, + final List<ContentValues> contentValuesList, int preferedType) { + for (ContentValues contentValues : contentValuesList) { + final int type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues + .getAsString(StructuredPostal.LABEL); + if (type == preferedType) { + appendVCardPostalLine(builder, type, label, contentValues); + return true; + } + } + return false; + } + + private void appendPostalsForGeneric(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + for (ContentValues contentValues : contentValuesList) { + final int type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues + .getAsString(StructuredPostal.LABEL); + appendVCardPostalLine(builder, type, label, contentValues); + } + } + + private void appendIms(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Im.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + int type = contentValues.getAsInteger(Im.PROTOCOL); + String data = contentValues.getAsString(Im.DATA); + + Log.d("@@@", "Im information. protocol=\"" + type + + "\", data=\"" + data + "\", protocol=\"" + + contentValues.getAsString(Im.PROTOCOL) + "\", custom_protocol=\"" + + contentValues.getAsString(Im.CUSTOM_PROTOCOL) + "\""); + + if (type == Im.PROTOCOL_GOOGLE_TALK) { + if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { + appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); + } + // TODO: add "X-GOOGLE TALK" case... + } + } + } + } + + private void appendWebsites(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Website.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + final String website = contentValues.getAsString(Website.URL); + appendVCardLine(builder, VCARD_PROPERTY_URL, website); + } + } + } + + private void appendBirthday(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Website.CONTENT_ITEM_TYPE); + if (contentValuesList != null && contentValuesList.size() > 0) { + // 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. + final String birthday = contentValuesList.get(0).getAsString(Miscellaneous.BIRTHDAY); + appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); + } + } + + private void appendOrganizations(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Organization.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + final String company = contentValues + .getAsString(Organization.COMPANY); + final String title = contentValues + .getAsString(Organization.TITLE); + appendVCardLine(builder, VCARD_PROPERTY_ORG, company, true, + mUsesQuotedPrintable); + appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, true, + mUsesQuotedPrintable); + } + } + } + + private void appendPhotos(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + List<ContentValues> contentValuesList = contentValuesListMap + .get(Photo.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + byte[] data = contentValues.getAsByteArray(Photo.PHOTO); + 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; + } + String photoString = VCardUtils.encodeBase64(data); + if (photoString.length() > 0) { + appendVCardPhotoLine(builder, photoString, photoType); + } + } + } + } + + private void appendNotes(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = + contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + if (mOnlyOneNoteFieldIsAvailable) { + StringBuilder noteBuilder = new StringBuilder(); + boolean first = true; + for (ContentValues contentValues : contentValuesList) { + final String note = contentValues.getAsString(Note.NOTE); + if (note.length() > 0) { + if (first) { + first = false; + } else { + noteBuilder.append('\n'); + } + noteBuilder.append(note); + } + } + appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteBuilder.toString(), + true, mUsesQuotedPrintable); + } else { + for (ContentValues contentValues : contentValuesList) { + final String note = contentValues.getAsString(Note.NOTE); + if (!TextUtils.isEmpty(note)) { + appendVCardLine(builder, VCARD_PROPERTY_NOTE, note, true, + mUsesQuotedPrintable); + } + } + } + } + } + + /** + * 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(String unescaped) { + if (TextUtils.isEmpty(unescaped)) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + final int length = unescaped.length(); + for (int i = 0; i < length; i++) { + char ch = unescaped.charAt(i); + switch (ch) { + case ';': + builder.append('\\'); + builder.append(';'); + break; + case '\r': + if (i + 1 < length) { + char nextChar = unescaped.charAt(i); + if (nextChar == '\n') { + continue; + } else { + // fall through + } + } else { + // fall through + } + case '\n': + // In vCard 2.1, there's no specification about this, while + // vCard 3.0 explicitly + // requires this should be encoded to "\n". + builder.append("\\n"); + break; + case '\\': + if (mIsV30) { + builder.append("\\\\"); + break; + } + case '<': + case '>': + if (mIsDoCoMo) { + builder.append('\\'); + builder.append(ch); + } + break; + case ',': + if (mIsV30) { + builder.append("\\,"); + break; + } + default: + builder.append(ch); + break; + } + } + return builder.toString(); + } + + private void appendVCardPhotoLine(StringBuilder builder, + String encodedData, String type) { + StringBuilder tmpBuilder = new StringBuilder(); + tmpBuilder.append(VCARD_PROPERTY_PHOTO); + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + if (mIsV30) { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); + } else { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); + } + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + tmpBuilder.append("TYPE="); + tmpBuilder.append(type); + tmpBuilder.append(VCARD_DATA_SEPARATOR); + tmpBuilder.append(encodedData); + + String tmpStr = tmpBuilder.toString(); + tmpBuilder = new StringBuilder(); + int lineCount = 0; + for (int i = 0; i < tmpStr.length(); i++) { + tmpBuilder.append(tmpStr.charAt(i)); + lineCount++; + if (lineCount > 72) { + tmpBuilder.append(VCARD_COL_SEPARATOR); + tmpBuilder.append(VCARD_WS); + lineCount = 0; + } + } + builder.append(tmpBuilder.toString()); + builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardPostalLine(StringBuilder builder, int type, + String label, final ContentValues contentValues) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + + boolean dataExists = false; + String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); + int length = dataArray.length; + final boolean useQuotedPrintable = mUsesQuotedPrintable; + for (int i = 0; i < length; i++) { + String data = dataArray[i]; + if (!TextUtils.isEmpty(data)) { + dataExists = true; + if (useQuotedPrintable) { + dataArray[i] = encodeQuotedPrintable(data); + } else { + dataArray[i] = escapeCharacters(data); + } + } + } + + boolean typeIsAppended = false; + switch (type) { + case StructuredPostal.TYPE_HOME: + builder.append(Constants.ATTR_TYPE_HOME); + typeIsAppended = true; + break; + case StructuredPostal.TYPE_WORK: + builder.append(Constants.ATTR_TYPE_WORK); + typeIsAppended = true; + break; + case StructuredPostal.TYPE_CUSTOM: + if (mUsesAndroidProperty && VCardUtils.containsOnlyAlphaDigitHyphen(label)){ + // We're not sure whether the label is valid in the spec ("IANA-token" in the vCard 3.1 + // is unclear...) + // Just for safety, we add "X-" at the beggining of each label. + // Also checks the label obeys with vCard 3.0 spec. + builder.append("X-"); + builder.append(label); + builder.append(VCARD_DATA_SEPARATOR); + } + break; + case StructuredPostal.TYPE_OTHER: + break; + default: + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); + break; + } + + if (dataExists) { + if (typeIsAppended) { + builder.append(VCARD_ATTR_SEPARATOR); + } + // Strictly, vCard 3.0 does not allow this, but we add this since + // this information + // should be useful, Assume no parser does not emit error with this + // attribute. + builder.append(mVCardAttributeCharset); + if (useQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + } + builder.append(VCARD_DATA_SEPARATOR); + if (dataExists) { + // The elements in dataArray are already encoded to quoted printable + // if needed. + // See above. + // + // TODO: in vCard 3.0, one line may become too huge. Fix this. + builder.append(dataArray[0]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[1]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[2]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[3]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[4]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[5]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[6]); + } + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardEmailLine(StringBuilder builder, int type, + String label, String data) { + builder.append(VCARD_PROPERTY_EMAIL); + builder.append(VCARD_ATTR_SEPARATOR); + + switch (type) { + case Email.TYPE_CUSTOM: + if (label.equals( + android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME)) { + builder.append(Constants.ATTR_TYPE_CELL); + } else if (mUsesAndroidProperty && VCardUtils.containsOnlyAlphaDigitHyphen(label)){ + builder.append("X-"); + builder.append(label); + } else { + // Default to INTERNET. + builder.append(Constants.ATTR_TYPE_INTERNET); + } + break; + case Email.TYPE_HOME: + builder.append(Constants.ATTR_TYPE_HOME); + break; + case Email.TYPE_WORK: + builder.append(Constants.ATTR_TYPE_WORK); + break; + case Email.TYPE_OTHER: + builder.append(Constants.ATTR_TYPE_INTERNET); + break; + default: + Log.e(LOG_TAG, "Unknown Email type: " + type); + builder.append(Constants.ATTR_TYPE_INTERNET); + break; + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(data); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardTelephoneLine(StringBuilder builder, int type, + String label, String encodedData) { + builder.append(VCARD_PROPERTY_TEL); + builder.append(VCARD_ATTR_SEPARATOR); + + switch (type) { + case Phone.TYPE_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_FAX_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_FAX_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_MOBILE: + builder.append(Constants.ATTR_TYPE_CELL); + break; + case Phone.TYPE_PAGER: + if (mIsDoCoMo) { + // Not sure about the reason, but previous implementation had + // used "VOICE" instead of "PAGER" + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + builder.append(Constants.ATTR_TYPE_PAGER); + } + break; + case Phone.TYPE_OTHER: + builder.append(Constants.ATTR_TYPE_VOICE); + break; + case Phone.TYPE_CUSTOM: + if (mUsesAndroidProperty) { + VCardUtils.containsOnlyAlphaDigitHyphen(label); + builder.append("X-" + label); + } else { + // Just ignore the custom type. + builder.append(Constants.ATTR_TYPE_VOICE); + } + break; + default: + appendUncommonPhoneType(builder, type); + break; + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + /** + * Appends phone type string which may not be available in some devices. + */ + private void appendUncommonPhoneType(StringBuilder builder, int type) { + if (mIsDoCoMo) { + // The previous implementation for DoCoMo had been conservative + // about + // miscellaneous types. + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + String phoneAttribute = VCardUtils.getPhoneAttributeString(type); + if (phoneAttribute != null) { + builder.append(phoneAttribute); + } else { + Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); + } + } + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLine(builder, propertyName, rawData, false, false); + } + + private void appendVCardLine(final StringBuilder builder, + final String field, final String rawData, boolean needCharset, + boolean needQuotedPrintable) { + builder.append(field); + if (needCharset) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + final String encodedData; + if (needQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + encodedData = encodeQuotedPrintable(rawData); + } else { + // TODO: one line may be too huge, which may be invalid in vCard spec, though + // several (even well-known) applications do not care this. + encodedData = escapeCharacters(rawData); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendTypeAttributes(final StringBuilder builder, + final List<String> types) { + // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, + // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. + boolean first = true; + for (String type : types) { + if (first) { + first = false; + } else { + builder.append(VCARD_ATTR_SEPARATOR); + } + if (mIsV30) { + builder.append(Constants.ATTR_TYPE); + builder.append('='); + } + builder.append(type); + } + } + + private String encodeQuotedPrintable(String str) { + if (TextUtils.isEmpty(str)) { + return ""; + } + { + // Replace "\n" and "\r" with "\r\n". + StringBuilder tmpBuilder = new StringBuilder(); + int length = str.length(); + for (int i = 0; i < length; i++) { + char ch = str.charAt(i); + if (ch == '\r') { + if (i + 1 < length && str.charAt(i + 1) == '\n') { + i++; + } + tmpBuilder.append("\r\n"); + } else if (ch == '\n') { + tmpBuilder.append("\r\n"); + } else { + tmpBuilder.append(ch); + } + } + str = tmpBuilder.toString(); + } + + StringBuilder builder = 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) { + builder.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 + builder.append("=\r\n"); + lineCount = 0; + } + } + + return builder.toString(); + } +} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index fef9dba..d87b002 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -15,43 +15,267 @@ */ package android.pim.vcard; +import java.util.HashMap; +import java.util.Map; + /** - * The class representing VCard related configurations + * The class representing VCard related configurations. Useful static methods are not in this class + * but in VCardUtils. */ public class VCardConfig { - static final int LOG_LEVEL_NONE = 0; - static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; - static final int LOG_LEVEL_SHOW_WARNING = 0x2; - static final int LOG_LEVEL_VERBOSE = + // TODO: may be better to make the instance of this available and stop using static methods and + // one integer. + + /* package */ static final int LOG_LEVEL_NONE = 0; + /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; + /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2; + /* package */ static final int LOG_LEVEL_VERBOSE = LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING; - + + /* package */ static final int LOG_LEVEL = LOG_LEVEL_PERFORMANCE_MEASUREMENT; + // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and // decode the unicode to the original charset. If not, this setting will cause some bug. public static final String DEFAULT_CHARSET = "iso-8859-1"; - // TODO: use this flag - public static boolean IGNORE_CASE_EXCEPT_VALUE = true; + // TODO: make the other codes use this flag + public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; + + private static final int FLAG_V21 = 0; + private static final int FLAG_V30 = 1; + + // 0x2 is reserved for the future use ... + + public static final int NAME_ORDER_DEFAULT = 0; + public static final int NAME_ORDER_EUROPE = 0x4; + public static final int NAME_ORDER_JAPANESE = 0x8; + private static final int NAME_ORDER_MASK = 0xC; + + // 0x10 is reserved for safety + + private static final int FLAG_CHARSET_UTF8 = 0; + private static final int FLAG_CHARSET_SHIFT_JIS = 0x20; + + /** + * The flag indicating the vCard composer will add some "X-" properties used only in Android + * when the formal vCard specification does not have appropriate fields for that data. + * + * For example, Android accepts nickname information while vCard 2.1 does not. + * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") + * instead of just dropping it. + * + * vCard parser code automatically parses the field emitted even when this flag is off. + * + * Note that this flag does not assure all the information must be hold in the emitted vCard. + */ + private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; + + /** + * The flag indicating the vCard composer will add some "X-" properties seen in the + * vCard data emitted by the other softwares/devices when the formal vCard specification + * does not have appropriate field(s) for that data. + * + * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are + * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other + * non-Android devices/softwares. We chose to enable the vCard composer to use those + * defact properties since they are also useful for Android devices. + * + * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 + * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens + * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. + */ + private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; + + /** + * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese + * mobile careers) should be used. This flag does not include any other information like + * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's + * dialect but the name order should be European", but it is not recommended. + */ + private static final int FLAG_DOCOMO = 0x20000000; + + + // VCard types + + + /** + * 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. + * + * 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". + */ + public static final int VCARD_TYPE_V21_GENERIC = + (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"; + + /** + * 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 especially + * in parsing part. + * + * TODO: implement this type. + */ + public static final int VCARD_TYPE_V30_GENERIC = + (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"; + + /** + * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + */ + public static final int VCARD_TYPE_V21_EUROPE = + (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"; + + /** + * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + */ + public static final int VCARD_TYPE_V30_EUROPE = + (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. + */ + 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"; - protected static final int LOG_LEVEL = LOG_LEVEL_PERFORMANCE_MEASUREMENT; + /** + * vCard format for miscellaneous Japanese devices, using Shift_Jis for + * parsing/composing the vCard data. + */ + public static final int VCARD_TYPE_V30_JAPANESE = + (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"; - // Note: phonetic name probably should be "LAST FIRST MIDDLE" for European languages, and - // space should be added between each element while it should not be in Japanese. - // But unfortunately, we currently do not have the data and are not sure whether we should - // support European version of name ordering. - // - // TODO: Implement the logic described above if we really need European version of - // phonetic name handling. Also, adding the appropriate test case of vCard would be - // highly appreciated. - public static final int NAME_ORDER_TYPE_ENGLISH = 0; - public static final int NAME_ORDER_TYPE_JAPANESE = 1; + /** + * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + */ + public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* 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. + */ + public static final int VCARD_TYPE_DOCOMO = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + + private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static final int NAME_ORDER_TYPE_DEFAULT = NAME_ORDER_TYPE_ENGLISH; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + + private static final Map<String, Integer> VCARD_TYPES_MAP; + + 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); + } + + public static int getVCardTypeFromString(String vcardTypeString) { + String loweredKey = vcardTypeString.toLowerCase(); + if (VCARD_TYPES_MAP.containsKey(loweredKey)) { + return VCARD_TYPES_MAP.get(loweredKey); + } else { + // XXX: should return the value indicating the input is invalid? + return VCARD_TYPE_DEFAULT; + } + } + + public static boolean isV30(int vcardType) { + return ((vcardType & FLAG_V30) != 0); + } + + public static boolean usesQuotedPrintable(int vcardType) { + 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 usesShiftJis(int vcardType) { + return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); + } /** - * @hide temporal. may be deleted + * @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; + } + + public static boolean usesAndroidSpecificProperty(int vcardType) { + return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); + } + + public static boolean usesDefactProperty(int vcardType) { + return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + public static boolean showPerformanceLog() { - return (LOG_LEVEL & LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; + return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; } private VCardConfig() { diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java index 4025f6c..fd165e9 100644 --- a/core/java/android/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -59,7 +59,7 @@ public class VCardDataBuilder implements VCardBuilder { private String mTargetCharset; private boolean mStrictLineBreakParsing; - private int mNameOrderType; + private int mVCardType; // Just for testing. private long mTimePushIntoContentResolver; @@ -67,23 +67,21 @@ public class VCardDataBuilder implements VCardBuilder { private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); public VCardDataBuilder() { - this(null, null, false, VCardConfig.NAME_ORDER_TYPE_DEFAULT); + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC); } /** * @hide */ - public VCardDataBuilder(int nameOrderType) { - this(null, null, false, nameOrderType); + public VCardDataBuilder(int vcardType) { + this(null, null, false, vcardType); } - + /** * @hide */ - public VCardDataBuilder(String charset, - boolean strictLineBreakParsing, - int nameOrderType) { - this(null, charset, strictLineBreakParsing, nameOrderType); + public VCardDataBuilder(String charset, boolean strictLineBreakParsing, int vcardType) { + this(null, charset, strictLineBreakParsing, vcardType); } /** @@ -92,7 +90,7 @@ public class VCardDataBuilder implements VCardBuilder { public VCardDataBuilder(String sourceCharset, String targetCharset, boolean strictLineBreakParsing, - int nameOrderType) { + int vcardType) { if (sourceCharset != null) { mSourceCharset = sourceCharset; } else { @@ -104,7 +102,7 @@ public class VCardDataBuilder implements VCardBuilder { mTargetCharset = TARGET_CHARSET; } mStrictLineBreakParsing = strictLineBreakParsing; - mNameOrderType = nameOrderType; + mVCardType = vcardType; } public void addEntryHandler(EntryHandler entryHandler) { @@ -112,11 +110,14 @@ public class VCardDataBuilder implements VCardBuilder { } public void start() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onParsingStart(); + } } public void end() { for (EntryHandler entryHandler : mEntryHandlers) { - entryHandler.onFinal(); + entryHandler.onParsingEnd(); } } @@ -135,7 +136,7 @@ public class VCardDataBuilder implements VCardBuilder { Log.e(LOG_TAG, "This is not VCARD!"); } - mCurrentContactStruct = new ContactStruct(mNameOrderType); + mCurrentContactStruct = new ContactStruct(mVCardType); } public void endRecord() { @@ -164,8 +165,7 @@ public class VCardDataBuilder implements VCardBuilder { public void propertyParamType(String type) { if (mParamType != null) { - Log.e(LOG_TAG, - "propertyParamType() is called more than once " + + Log.e(LOG_TAG, "propertyParamType() is called more than once " + "before propertyParamValue() is called"); } mParamType = type; @@ -173,6 +173,7 @@ public class VCardDataBuilder implements VCardBuilder { public void propertyParamValue(String value) { if (mParamType == null) { + // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. mParamType = "TYPE"; } mCurrentProperty.addParameter(mParamType, value); @@ -297,7 +298,7 @@ public class VCardDataBuilder implements VCardBuilder { String charset = ((charsetCollection != null) ? charsetCollection.iterator().next() : null); String targetCharset = CharsetUtils.nameForDefaultVendor(charset); - + final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); String encoding = ((encodingCollection != null) ? encodingCollection.iterator().next() : null); diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index 17a138f..974fca8 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -34,7 +34,7 @@ import java.util.HashSet; * This class is used to parse vcard. Please refer to vCard Specification 2.1. */ public class VCardParser_V21 extends VCardParser { - private static final String LOG_TAG = "VCardParser_V21"; + private static final String LOG_TAG = "vcard.VCardParser_V21"; /** Store the known-type */ private static final HashSet<String> sKnownTypeSet = new HashSet<String>( @@ -58,8 +58,10 @@ public class VCardParser_V21 extends VCardParser { "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); - // Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - // We allow it for safety... + /** + * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety... + */ private static final HashSet<String> sAvailableEncodingV21 = new HashSet<String>(Arrays.asList( "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); @@ -70,7 +72,10 @@ public class VCardParser_V21 extends VCardParser { /** The builder to build parsed data */ protected VCardBuilder mBuilder = null; - /** The encoding type */ + /** + * The encoding type. "Encoding" in vCard is different from "Charset". + * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. + */ protected String mEncoding = null; protected final String sDefaultEncoding = "8BIT"; @@ -88,17 +93,17 @@ public class VCardParser_V21 extends VCardParser { // Just for debugging private long mTimeTotal; - private long mTimeStartRecord; - private long mTimeEndRecord; + private long mTimeReadStartRecord; + private long mTimeReadEndRecord; private long mTimeStartProperty; private long mTimeEndProperty; private long mTimeParseItems; - private long mTimeParseItem1; - private long mTimeParseItem2; - private long mTimeParseItem3; - private long mTimeHandlePropertyValue1; - private long mTimeHandlePropertyValue2; - private long mTimeHandlePropertyValue3; + private long mTimeParseLineAndHandleGroup; + private long mTimeParsePropertyValues; + private long mTimeParseAdrOrgN; + private long mTimeHandleMiscPropertyValue; + private long mTimeHandleQuotedPrintable; + private long mTimeHandleBase64; /** * Create a new VCard parser. @@ -213,7 +218,7 @@ public class VCardParser_V21 extends VCardParser { if (mBuilder != null) { start = System.currentTimeMillis(); mBuilder.startRecord("VCARD"); - mTimeStartRecord += System.currentTimeMillis() - start; + mTimeReadStartRecord += System.currentTimeMillis() - start; } start = System.currentTimeMillis(); parseItems(); @@ -222,7 +227,7 @@ public class VCardParser_V21 extends VCardParser { if (mBuilder != null) { start = System.currentTimeMillis(); mBuilder.endRecord(); - mTimeEndRecord += System.currentTimeMillis() - start; + mTimeReadEndRecord += System.currentTimeMillis() - start; } return true; } @@ -250,26 +255,6 @@ public class VCardParser_V21 extends VCardParser { // Though vCard 2.1/3.0 specification does not allow lower cases, // some data may have them, so we allow it (Actually, previous code // had explicitly allowed "BEGIN:vCard" though there's no example). - // - // TODO: ignore non vCard entry (e.g. vcalendar). - // XXX: Not sure, but according to VDataBuilder.java, vcalendar - // entry - // may be nested. Just seeking "END:SOMETHING" may not be enough. - // e.g. - // BEGIN:VCARD - // ... (Valid. Must parse this) - // END:VCARD - // BEGIN:VSOMETHING - // ... (Must ignore this) - // BEGIN:VSOMETHING2 - // ... (Must ignore this) - // END:VSOMETHING2 - // ... (Must ignore this!) - // END:VSOMETHING - // BEGIN:VCARD - // ... (Valid. Must parse this) - // END:VCARD - // INVALID_STRING (VCardException should be thrown) if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") && strArray[1].trim().equalsIgnoreCase("VCARD")) { @@ -367,11 +352,11 @@ public class VCardParser_V21 extends VCardParser { } /** - * item = [groups "."] name [params] ":" value CRLF - * / [groups "."] "ADR" [params] ":" addressparts CRLF - * / [groups "."] "ORG" [params] ":" orgparts CRLF - * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF + * item = [groups "."] name [params] ":" value CRLF + * / [groups "."] "ADR" [params] ":" addressparts CRLF + * / [groups "."] "ORG" [params] ":" orgparts CRLF + * / [groups "."] "N" [params] ":" nameparts CRLF + * / [groups "."] "AGENT" [params] ":" vcard CRLF */ protected boolean parseItem() throws IOException, VCardException { mEncoding = sDefaultEncoding; @@ -389,14 +374,13 @@ public class VCardParser_V21 extends VCardParser { String propertyName = propertyNameAndValue[0].toUpperCase(); String propertyValue = propertyNameAndValue[1]; - mTimeParseItem1 += System.currentTimeMillis() - start; + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - if (propertyName.equals("ADR") || - propertyName.equals("ORG") || + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { start = System.currentTimeMillis(); handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseItem3 += System.currentTimeMillis() - start; + mTimeParseAdrOrgN += System.currentTimeMillis() - start; return false; } else if (propertyName.equals("AGENT")) { handleAgent(propertyValue); @@ -408,14 +392,13 @@ public class VCardParser_V21 extends VCardParser { } else { throw new VCardException("Unknown BEGIN type: " + propertyValue); } - } else if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersion())) { + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { throw new VCardVersionException("Incompatible version: " + propertyValue + " != " + getVersion()); } start = System.currentTimeMillis(); handlePropertyValue(propertyName, propertyValue); - mTimeParseItem2 += System.currentTimeMillis() - start; + mTimeParsePropertyValues += System.currentTimeMillis() - start; return false; } @@ -542,7 +525,7 @@ public class VCardParser_V21 extends VCardParser { } /** - * ptypeval = knowntype / "X-" word + * ptypeval = knowntype / "X-" word */ protected void handleType(String ptypeval) { String upperTypeValue = ptypeval; @@ -637,8 +620,7 @@ public class VCardParser_V21 extends VCardParser { } } - protected void handlePropertyValue( - String propertyName, String propertyValue) throws + protected void handlePropertyValue(String propertyName, String propertyValue) throws IOException, VCardException { if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { long start = System.currentTimeMillis(); @@ -648,7 +630,7 @@ public class VCardParser_V21 extends VCardParser { v.add(result); mBuilder.propertyValues(v); } - mTimeHandlePropertyValue2 += System.currentTimeMillis() - start; + mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; } else if (mEncoding.equalsIgnoreCase("BASE64") || mEncoding.equalsIgnoreCase("B")) { long start = System.currentTimeMillis(); @@ -667,7 +649,7 @@ public class VCardParser_V21 extends VCardParser { mBuilder.propertyValues(null); } } - mTimeHandlePropertyValue3 += System.currentTimeMillis() - start; + mTimeHandleBase64 += System.currentTimeMillis() - start; } else { if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") || mEncoding.equalsIgnoreCase("8BIT") @@ -681,7 +663,7 @@ public class VCardParser_V21 extends VCardParser { v.add(maybeUnescapeText(propertyValue)); mBuilder.propertyValues(v); } - mTimeHandlePropertyValue1 += System.currentTimeMillis() - start; + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; } } @@ -770,15 +752,15 @@ public class VCardParser_V21 extends VCardParser { * We are not sure whether we should add "\" CRLF to each value. * For now, we exclude them. */ - protected void handleMultiplePropertyValue( - String propertyName, String propertyValue) throws IOException, VCardException { - // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some data have it. + protected void handleMultiplePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + // vCard 2.1 does not allow QUOTED-PRINTABLE here, + // but some softwares/devices emit such data. if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { propertyValue = getQuotedPrintable(propertyValue); } if (mBuilder != null) { - // TODO: limit should be set in accordance with propertyName? StringBuilder builder = new StringBuilder(); ArrayList<String> list = new ArrayList<String>(); int length = propertyValue.length(); @@ -786,7 +768,7 @@ public class VCardParser_V21 extends VCardParser { char ch = propertyValue.charAt(i); if (ch == '\\' && i < length - 1) { char nextCh = propertyValue.charAt(i + 1); - String unescapedString = maybeUnescape(nextCh); + String unescapedString = maybeUnescapeCharacter(nextCh); if (unescapedString != null) { builder.append(unescapedString); i++; @@ -819,7 +801,6 @@ public class VCardParser_V21 extends VCardParser { throw new VCardNotSupportedException("AGENT Property is not supported now."); /* This is insufficient support. Also, AGENT Property is very rare. Ignore it for now. - TODO: fix this. String[] strArray = propertyValue.split(":", 2); if (!(strArray.length == 2 || @@ -843,7 +824,7 @@ public class VCardParser_V21 extends VCardParser { * Returns unescaped String if the character should be unescaped. Return null otherwise. * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. */ - protected String maybeUnescape(char ch) { + protected String maybeUnescapeCharacter(char ch) { // Original vCard 2.1 specification does not allow transformation // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // this class allowed them, so keep it as is. @@ -863,17 +844,11 @@ public class VCardParser_V21 extends VCardParser { @Override public boolean parse(InputStream is, String charset, VCardBuilder builder) throws IOException, VCardException { - // TODO: make this count error entries instead of just throwing VCardException. - - { - // TODO: If we really need to allow only CRLF as line break, - // we will have to develop our own BufferedReader(). - final InputStreamReader tmpReader = new InputStreamReader(is, charset); - if (VCardConfig.showPerformanceLog()) { - mReader = new CustomBufferedReader(tmpReader); - } else { - mReader = new BufferedReader(tmpReader); - } + final InputStreamReader tmpReader = new InputStreamReader(is, charset); + if (VCardConfig.showPerformanceLog()) { + mReader = new CustomBufferedReader(tmpReader); + } else { + mReader = new BufferedReader(tmpReader); } mBuilder = builder; @@ -903,21 +878,26 @@ public class VCardParser_V21 extends VCardParser { } private void showPerformanceInfo() { - Log.d(LOG_TAG, "total parsing time: " + mTimeTotal + " ms"); + Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); if (mReader instanceof CustomBufferedReader) { - Log.d(LOG_TAG, "total readLine time: " + + Log.d(LOG_TAG, "Total readLine time: " + ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); } - Log.d(LOG_TAG, "mTimeStartRecord: " + mTimeStartRecord + " ms"); - Log.d(LOG_TAG, "mTimeEndRecord: " + mTimeEndRecord + " ms"); - Log.d(LOG_TAG, "mTimeParseItem1: " + mTimeParseItem1 + " ms"); - Log.d(LOG_TAG, "mTimeParseItem2: " + mTimeParseItem2 + " ms"); - Log.d(LOG_TAG, "mTimeParseItem3: " + mTimeParseItem3 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue1: " + mTimeHandlePropertyValue1 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue2: " + mTimeHandlePropertyValue2 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue3: " + mTimeHandlePropertyValue3 + " ms"); + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + + mTimeReadStartRecord + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + + mTimeParseLineAndHandleGroup + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + + mTimeHandleMiscPropertyValue + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); } - + private boolean isLetter(char ch) { if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { return true; diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 634d9f5..475be4e 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -27,7 +27,7 @@ import java.util.HashSet; * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426) */ public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V30"; + private static final String LOG_TAG = "vcard.VCardParser_V30"; private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( Arrays.asList( @@ -49,7 +49,7 @@ public class VCardParser_V30 extends VCardParser_V21 { @Override protected String getVersion() { - return "3.0"; + return Constants.VERSION_V30; } @Override @@ -284,7 +284,7 @@ public class VCardParser_V30 extends VCardParser_V21 { if (ch == '\\' && i < length - 1) { char next_ch = text.charAt(++i); if (next_ch == 'n' || next_ch == 'N') { - builder.append("\r\n"); + builder.append("\n"); } else { builder.append(next_ch); } @@ -296,9 +296,9 @@ public class VCardParser_V30 extends VCardParser_V21 { } @Override - protected String maybeUnescape(char ch) { + protected String maybeUnescapeCharacter(char ch) { if (ch == 'n' || ch == 'N') { - return "\r\n"; + return "\n"; } else { return String.valueOf(ch); } diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java new file mode 100644 index 0000000..b7b706f --- /dev/null +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -0,0 +1,764 @@ +/* + * 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.ContentProviderOperation; +import android.content.ContentValues; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.text.TextUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for VCard handling codes. + */ +public class VCardUtils { + /* + * TODO: some of methods in this class should be placed to the more appropriate place... + */ + + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is + // converted to two attribute Strings. These only contain some minor fields valid in both + // vCard and current (as of 2009-08-07) Contacts structure. + private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; + private static final Set<String> sPhoneTypesSetUnknownToContacts; + + private static final Map<String, Integer> sKnownPhoneTypesMap_StoI; + + static { + sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); + sKnownPhoneTypesMap_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); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); + sKnownPhoneTypesMap_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); + + 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); + + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypesMap_StoI.put( + Constants.ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TELEX, Phone.TYPE_TELEX); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_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); + } + + public static String getPhoneAttributeString(int type) { + return sKnownPhoneTypesMap_ItoS.get(type); + } + + /** + * Returns Interger when the given types can be parsed as known type. Returns String object + * when not, which should be set to label. + */ + public static Object getPhoneTypeFromStrings(Collection<String> types) { + int type = -1; + String label = null; + boolean isFax = false; + boolean hasPref = false; + + if (types != null) { + for (String typeString : types) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + hasPref = true; + } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + isFax = true; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + if (tmp != null) { + type = tmp; + } else if (type < 0) { + type = Phone.TYPE_CUSTOM; + label = typeString; + } + } + } + } + if (type < 0) { + if (hasPref) { + type = Phone.TYPE_MAIN; + } else { + // default to TYPE_HOME + type = Phone.TYPE_HOME; + } + } + if (isFax) { + if (type == Phone.TYPE_HOME) { + type = Phone.TYPE_FAX_HOME; + } else if (type == Phone.TYPE_WORK) { + type = Phone.TYPE_FAX_WORK; + } else if (type == Phone.TYPE_OTHER) { + type = Phone.TYPE_OTHER_FAX; + } + } + if (type == Phone.TYPE_CUSTOM) { + return label; + } else { + return type; + } + } + + public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { + // TODO: check the following. + // - it may violate vCard spec + // - it may contain non-ASCII characters + // + // TODO: use vcardType + return (phoneAttribute.startsWith("X-") || phoneAttribute.startsWith("x-") || + sPhoneTypesSetUnknownToContacts.contains(phoneAttribute)); + } + + public static String[] sortNameElements(int vcardType, + String familyName, String middleName, String givenName) { + String[] list = new String[3]; + switch (VCardConfig.getNameOrderType(vcardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + // TODO: Should handle Ascii case? + list[0] = familyName; + list[1] = middleName; + list[2] = givenName; + break; + case VCardConfig.NAME_ORDER_EUROPE: + list[0] = middleName; + list[1] = givenName; + list[2] = familyName; + break; + default: + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + break; + } + return list; + } + + /** + * Inserts postal data into the builder object. + * + * 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 #getVCardPostalElements(ContentValues)} + */ + public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, + final ContentProviderOperation.Builder builder, + final ContactStruct.PostalData postalData) { + builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredPostal.TYPE, postalData.type); + if (postalData.type == StructuredPostal.TYPE_CUSTOM) { + builder.withValue(StructuredPostal.LABEL, postalData.label); + } + + builder.withValue(StructuredPostal.POBOX, postalData.pobox); + // Extended address is dropped since there's no relevant entry in ContactsContract. + builder.withValue(StructuredPostal.STREET, postalData.street); + builder.withValue(StructuredPostal.CITY, postalData.localty); + builder.withValue(StructuredPostal.REGION, postalData.region); + builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); + builder.withValue(StructuredPostal.COUNTRY, postalData.country); + + builder.withValue(StructuredPostal.FORMATTED_ADDRESS, + postalData.getFormattedAddress(vcardType)); + if (postalData.isPrimary) { + 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, + * android.content.ContentProviderOperation.Builder, + * android.pim.vcard.ContactStruct.PostalData)} + */ + public static String[] getVCardPostalElements(ContentValues contentValues) { + 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] = ""; + dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); + if (dataArray[2] == null) { + dataArray[2] = ""; + } + // Assume that localty == city + dataArray[3] = contentValues.getAsString(StructuredPostal.CITY); + if (dataArray[3] == null) { + dataArray[3] = ""; + } + String region = contentValues.getAsString(StructuredPostal.REGION); + if (!TextUtils.isEmpty(region)) { + dataArray[4] = region; + } else { + dataArray[4] = ""; + } + dataArray[5] = contentValues.getAsString(StructuredPostal.POSTCODE); + if (dataArray[5] == null) { + dataArray[5] = ""; + } + dataArray[6] = contentValues.getAsString(StructuredPostal.COUNTRY); + if (dataArray[6] == null) { + dataArray[6] = ""; + } + + return dataArray; + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName) { + return constructNameFromElements(nameOrderType, familyName, middleName, givenName, + null, null); + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName, + String prefix, String suffix) { + StringBuilder builder = new StringBuilder(); + String[] nameList = sortNameElements(nameOrderType, + familyName, middleName, givenName); + boolean first = true; + if (!TextUtils.isEmpty(prefix)) { + first = false; + builder.append(prefix); + } + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + if (!TextUtils.isEmpty(suffix)) { + if (!first) { + builder.append(' '); + } + builder.append(suffix); + } + return builder.toString(); + } + + public static boolean containsOnlyAscii(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int length = str.length(); + final int asciiFirst = 0x20; + final int asciiLast = 0x126; + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int c = str.codePointAt(i); + if (c < asciiFirst || asciiLast < c) { + return false; + } + } + return true; + } + + /** + * This is useful since vCard 3.0 often requires the ("X-") properties and groups + * should contain only alphabets, digits, and hyphen. + * + * Note: It is already known some devices (wrongly) outputs properties with characters + * which should not be in the field. One example is "X-GOOGLE TALK". We appreciate + * such kind of input but must never output it unless the target is very specific + * to the device which is able to parse the malformed input. + */ + public static boolean containsOnlyAlphaDigitHyphen(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int lowerAlphabetFirst = 0x41; // included ('A') + final int lowerAlphabetLast = 0x5b; // not included ('[') + final int upperAlphabetFirst = 0x61; // included ('a') + final int upperAlphabetLast = 0x7b; // included ('{') + final int digitFirst = 0x30; // included ('0') + final int digitLast = 0x39; // included ('9') + final int hyphen = '-'; + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int codepoint = str.codePointAt(i); + if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetLast) || + (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetLast) || + (digitFirst <= codepoint && codepoint < digitLast) || + (codepoint == hyphen))) { + return false; + } + } + return true; + } + + // TODO: Replace wth the method in Base64 class. + private static char PAD = '='; + private static final char[] ENCODE64 = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' + }; + + static public String encodeBase64(byte[] data) { + if (data == null) { + return ""; + } + + char[] charBuffer = new char[(data.length + 2) / 3 * 4]; + int position = 0; + int _3byte = 0; + for (int i=0; i<data.length-2; i+=3) { + _3byte = ((data[i] & 0xFF) << 16) + ((data[i+1] & 0xFF) << 8) + (data[i+2] & 0xFF); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = ENCODE64[_3byte & 0x3F]; + } + switch(data.length % 3) { + case 1: // [111111][11 0000][0000 00][000000] + _3byte = ((data[data.length-1] & 0xFF) << 16); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = PAD; + charBuffer[position++] = PAD; + break; + case 2: // [111111][11 1111][1111 00][000000] + _3byte = ((data[data.length-2] & 0xFF) << 16) + ((data[data.length-1] & 0xFF) << 8); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = PAD; + break; + } + + return new String(charBuffer); + } + + static public String toHalfWidthString(String orgString) { + if (TextUtils.isEmpty(orgString)) { + return null; + } + StringBuilder builder = new StringBuilder(); + int length = orgString.length(); + for (int i = 0; i < length; i++) { + // All Japanese character is able to be expressed by char. + // Do not need to use String#codepPointAt(). + char ch = orgString.charAt(i); + CharSequence halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); + if (halfWidthText != null) { + builder.append(halfWidthText); + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + private VCardUtils() { + } +} + +/** + * TextUtils especially for Japanese. + * TODO: make this in android.text in the future + */ +class JapaneseUtils { + static private final Map<Character, String> sHalfWidthMap = + new HashMap<Character, String>(); + + static { + // There's no logical mapping rule in Unicode. Sigh. + sHalfWidthMap.put('\u3001', "\uFF64"); + sHalfWidthMap.put('\u3002', "\uFF61"); + sHalfWidthMap.put('\u300C', "\uFF62"); + sHalfWidthMap.put('\u300D', "\uFF63"); + sHalfWidthMap.put('\u301C', "~"); + sHalfWidthMap.put('\u3041', "\uFF67"); + sHalfWidthMap.put('\u3042', "\uFF71"); + sHalfWidthMap.put('\u3043', "\uFF68"); + sHalfWidthMap.put('\u3044', "\uFF72"); + sHalfWidthMap.put('\u3045', "\uFF69"); + sHalfWidthMap.put('\u3046', "\uFF73"); + sHalfWidthMap.put('\u3047', "\uFF6A"); + sHalfWidthMap.put('\u3048', "\uFF74"); + sHalfWidthMap.put('\u3049', "\uFF6B"); + sHalfWidthMap.put('\u304A', "\uFF75"); + sHalfWidthMap.put('\u304B', "\uFF76"); + sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u304D', "\uFF77"); + sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u304F', "\uFF78"); + sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u3051', "\uFF79"); + sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u3053', "\uFF7A"); + sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u3055', "\uFF7B"); + sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u3057', "\uFF7C"); + sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u3059', "\uFF7D"); + sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u305B', "\uFF7E"); + sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u305D', "\uFF7F"); + sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u305F', "\uFF80"); + sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u3061', "\uFF81"); + sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u3063', "\uFF6F"); + sHalfWidthMap.put('\u3064', "\uFF82"); + sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u3066', "\uFF83"); + sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u3068', "\uFF84"); + sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u306A', "\uFF85"); + sHalfWidthMap.put('\u306B', "\uFF86"); + sHalfWidthMap.put('\u306C', "\uFF87"); + sHalfWidthMap.put('\u306D', "\uFF88"); + sHalfWidthMap.put('\u306E', "\uFF89"); + sHalfWidthMap.put('\u306F', "\uFF8A"); + sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u3072', "\uFF8B"); + sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u3075', "\uFF8C"); + sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u3078', "\uFF8D"); + sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u307B', "\uFF8E"); + sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u307E', "\uFF8F"); + sHalfWidthMap.put('\u307F', "\uFF90"); + sHalfWidthMap.put('\u3080', "\uFF91"); + sHalfWidthMap.put('\u3081', "\uFF92"); + sHalfWidthMap.put('\u3082', "\uFF93"); + sHalfWidthMap.put('\u3083', "\uFF6C"); + sHalfWidthMap.put('\u3084', "\uFF94"); + sHalfWidthMap.put('\u3085', "\uFF6D"); + sHalfWidthMap.put('\u3086', "\uFF95"); + sHalfWidthMap.put('\u3087', "\uFF6E"); + sHalfWidthMap.put('\u3088', "\uFF96"); + sHalfWidthMap.put('\u3089', "\uFF97"); + sHalfWidthMap.put('\u308A', "\uFF98"); + sHalfWidthMap.put('\u308B', "\uFF99"); + sHalfWidthMap.put('\u308C', "\uFF9A"); + sHalfWidthMap.put('\u308D', "\uFF9B"); + sHalfWidthMap.put('\u308E', "\uFF9C"); + sHalfWidthMap.put('\u308F', "\uFF9C"); + sHalfWidthMap.put('\u3090', "\uFF72"); + sHalfWidthMap.put('\u3091', "\uFF74"); + sHalfWidthMap.put('\u3092', "\uFF66"); + sHalfWidthMap.put('\u3093', "\uFF9D"); + sHalfWidthMap.put('\u309B', "\uFF9E"); + sHalfWidthMap.put('\u309C', "\uFF9F"); + sHalfWidthMap.put('\u30A1', "\uFF67"); + sHalfWidthMap.put('\u30A2', "\uFF71"); + sHalfWidthMap.put('\u30A3', "\uFF68"); + sHalfWidthMap.put('\u30A4', "\uFF72"); + sHalfWidthMap.put('\u30A5', "\uFF69"); + sHalfWidthMap.put('\u30A6', "\uFF73"); + sHalfWidthMap.put('\u30A7', "\uFF6A"); + sHalfWidthMap.put('\u30A8', "\uFF74"); + sHalfWidthMap.put('\u30A9', "\uFF6B"); + sHalfWidthMap.put('\u30AA', "\uFF75"); + sHalfWidthMap.put('\u30AB', "\uFF76"); + sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u30AD', "\uFF77"); + sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u30AF', "\uFF78"); + sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u30B1', "\uFF79"); + sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u30B3', "\uFF7A"); + sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u30B5', "\uFF7B"); + sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u30B7', "\uFF7C"); + sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u30B9', "\uFF7D"); + sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u30BB', "\uFF7E"); + sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u30BD', "\uFF7F"); + sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u30BF', "\uFF80"); + sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u30C1', "\uFF81"); + sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u30C3', "\uFF6F"); + sHalfWidthMap.put('\u30C4', "\uFF82"); + sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u30C6', "\uFF83"); + sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u30C8', "\uFF84"); + sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u30CA', "\uFF85"); + sHalfWidthMap.put('\u30CB', "\uFF86"); + sHalfWidthMap.put('\u30CC', "\uFF87"); + sHalfWidthMap.put('\u30CD', "\uFF88"); + sHalfWidthMap.put('\u30CE', "\uFF89"); + sHalfWidthMap.put('\u30CF', "\uFF8A"); + sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u30D2', "\uFF8B"); + sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u30D5', "\uFF8C"); + sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u30D8', "\uFF8D"); + sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u30DB', "\uFF8E"); + sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u30DE', "\uFF8F"); + sHalfWidthMap.put('\u30DF', "\uFF90"); + sHalfWidthMap.put('\u30E0', "\uFF91"); + sHalfWidthMap.put('\u30E1', "\uFF92"); + sHalfWidthMap.put('\u30E2', "\uFF93"); + sHalfWidthMap.put('\u30E3', "\uFF6C"); + sHalfWidthMap.put('\u30E4', "\uFF94"); + sHalfWidthMap.put('\u30E5', "\uFF6D"); + sHalfWidthMap.put('\u30E6', "\uFF95"); + sHalfWidthMap.put('\u30E7', "\uFF6E"); + sHalfWidthMap.put('\u30E8', "\uFF96"); + sHalfWidthMap.put('\u30E9', "\uFF97"); + sHalfWidthMap.put('\u30EA', "\uFF98"); + sHalfWidthMap.put('\u30EB', "\uFF99"); + sHalfWidthMap.put('\u30EC', "\uFF9A"); + sHalfWidthMap.put('\u30ED', "\uFF9B"); + sHalfWidthMap.put('\u30EE', "\uFF9C"); + sHalfWidthMap.put('\u30EF', "\uFF9C"); + sHalfWidthMap.put('\u30F0', "\uFF72"); + sHalfWidthMap.put('\u30F1', "\uFF74"); + sHalfWidthMap.put('\u30F2', "\uFF66"); + sHalfWidthMap.put('\u30F3', "\uFF9D"); + sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); + sHalfWidthMap.put('\u30F5', "\uFF76"); + sHalfWidthMap.put('\u30F6', "\uFF79"); + sHalfWidthMap.put('\u30FB', "\uFF65"); + sHalfWidthMap.put('\u30FC', "\uFF70"); + sHalfWidthMap.put('\uFF01', "!"); + sHalfWidthMap.put('\uFF02', "\""); + sHalfWidthMap.put('\uFF03', "#"); + sHalfWidthMap.put('\uFF04', "$"); + sHalfWidthMap.put('\uFF05', "%"); + sHalfWidthMap.put('\uFF06', "&"); + sHalfWidthMap.put('\uFF07', "'"); + sHalfWidthMap.put('\uFF08', "("); + sHalfWidthMap.put('\uFF09', ")"); + sHalfWidthMap.put('\uFF0A', "*"); + sHalfWidthMap.put('\uFF0B', "+"); + sHalfWidthMap.put('\uFF0C', ","); + sHalfWidthMap.put('\uFF0D', "-"); + sHalfWidthMap.put('\uFF0E', "."); + sHalfWidthMap.put('\uFF0F', "/"); + sHalfWidthMap.put('\uFF10', "0"); + sHalfWidthMap.put('\uFF11', "1"); + sHalfWidthMap.put('\uFF12', "2"); + sHalfWidthMap.put('\uFF13', "3"); + sHalfWidthMap.put('\uFF14', "4"); + sHalfWidthMap.put('\uFF15', "5"); + sHalfWidthMap.put('\uFF16', "6"); + sHalfWidthMap.put('\uFF17', "7"); + sHalfWidthMap.put('\uFF18', "8"); + sHalfWidthMap.put('\uFF19', "9"); + sHalfWidthMap.put('\uFF1A', ":"); + sHalfWidthMap.put('\uFF1B', ";"); + sHalfWidthMap.put('\uFF1C', "<"); + sHalfWidthMap.put('\uFF1D', "="); + sHalfWidthMap.put('\uFF1E', ">"); + sHalfWidthMap.put('\uFF1F', "?"); + sHalfWidthMap.put('\uFF20', "@"); + sHalfWidthMap.put('\uFF21', "A"); + sHalfWidthMap.put('\uFF22', "B"); + sHalfWidthMap.put('\uFF23', "C"); + sHalfWidthMap.put('\uFF24', "D"); + sHalfWidthMap.put('\uFF25', "E"); + sHalfWidthMap.put('\uFF26', "F"); + sHalfWidthMap.put('\uFF27', "G"); + sHalfWidthMap.put('\uFF28', "H"); + sHalfWidthMap.put('\uFF29', "I"); + sHalfWidthMap.put('\uFF2A', "J"); + sHalfWidthMap.put('\uFF2B', "K"); + sHalfWidthMap.put('\uFF2C', "L"); + sHalfWidthMap.put('\uFF2D', "M"); + sHalfWidthMap.put('\uFF2E', "N"); + sHalfWidthMap.put('\uFF2F', "O"); + sHalfWidthMap.put('\uFF30', "P"); + sHalfWidthMap.put('\uFF31', "Q"); + sHalfWidthMap.put('\uFF32', "R"); + sHalfWidthMap.put('\uFF33', "S"); + sHalfWidthMap.put('\uFF34', "T"); + sHalfWidthMap.put('\uFF35', "U"); + sHalfWidthMap.put('\uFF36', "V"); + sHalfWidthMap.put('\uFF37', "W"); + sHalfWidthMap.put('\uFF38', "X"); + sHalfWidthMap.put('\uFF39', "Y"); + sHalfWidthMap.put('\uFF3A', "Z"); + sHalfWidthMap.put('\uFF3B', "["); + sHalfWidthMap.put('\uFF3C', "\\"); + sHalfWidthMap.put('\uFF3D', "]"); + sHalfWidthMap.put('\uFF3E', "^"); + sHalfWidthMap.put('\uFF3F', "_"); + sHalfWidthMap.put('\uFF41', "a"); + sHalfWidthMap.put('\uFF42', "b"); + sHalfWidthMap.put('\uFF43', "c"); + sHalfWidthMap.put('\uFF44', "d"); + sHalfWidthMap.put('\uFF45', "e"); + sHalfWidthMap.put('\uFF46', "f"); + sHalfWidthMap.put('\uFF47', "g"); + sHalfWidthMap.put('\uFF48', "h"); + sHalfWidthMap.put('\uFF49', "i"); + sHalfWidthMap.put('\uFF4A', "j"); + sHalfWidthMap.put('\uFF4B', "k"); + sHalfWidthMap.put('\uFF4C', "l"); + sHalfWidthMap.put('\uFF4D', "m"); + sHalfWidthMap.put('\uFF4E', "n"); + sHalfWidthMap.put('\uFF4F', "o"); + sHalfWidthMap.put('\uFF50', "p"); + sHalfWidthMap.put('\uFF51', "q"); + sHalfWidthMap.put('\uFF52', "r"); + sHalfWidthMap.put('\uFF53', "s"); + sHalfWidthMap.put('\uFF54', "t"); + sHalfWidthMap.put('\uFF55', "u"); + sHalfWidthMap.put('\uFF56', "v"); + sHalfWidthMap.put('\uFF57', "w"); + sHalfWidthMap.put('\uFF58', "x"); + sHalfWidthMap.put('\uFF59', "y"); + sHalfWidthMap.put('\uFF5A', "z"); + sHalfWidthMap.put('\uFF5B', "{"); + sHalfWidthMap.put('\uFF5C', "|"); + sHalfWidthMap.put('\uFF5D', "}"); + sHalfWidthMap.put('\uFF5E', "~"); + sHalfWidthMap.put('\uFF61', "\uFF61"); + sHalfWidthMap.put('\uFF62', "\uFF62"); + sHalfWidthMap.put('\uFF63', "\uFF63"); + sHalfWidthMap.put('\uFF64', "\uFF64"); + sHalfWidthMap.put('\uFF65', "\uFF65"); + sHalfWidthMap.put('\uFF66', "\uFF66"); + sHalfWidthMap.put('\uFF67', "\uFF67"); + sHalfWidthMap.put('\uFF68', "\uFF68"); + sHalfWidthMap.put('\uFF69', "\uFF69"); + sHalfWidthMap.put('\uFF6A', "\uFF6A"); + sHalfWidthMap.put('\uFF6B', "\uFF6B"); + sHalfWidthMap.put('\uFF6C', "\uFF6C"); + sHalfWidthMap.put('\uFF6D', "\uFF6D"); + sHalfWidthMap.put('\uFF6E', "\uFF6E"); + sHalfWidthMap.put('\uFF6F', "\uFF6F"); + sHalfWidthMap.put('\uFF70', "\uFF70"); + sHalfWidthMap.put('\uFF71', "\uFF71"); + sHalfWidthMap.put('\uFF72', "\uFF72"); + sHalfWidthMap.put('\uFF73', "\uFF73"); + sHalfWidthMap.put('\uFF74', "\uFF74"); + sHalfWidthMap.put('\uFF75', "\uFF75"); + sHalfWidthMap.put('\uFF76', "\uFF76"); + sHalfWidthMap.put('\uFF77', "\uFF77"); + sHalfWidthMap.put('\uFF78', "\uFF78"); + sHalfWidthMap.put('\uFF79', "\uFF79"); + sHalfWidthMap.put('\uFF7A', "\uFF7A"); + sHalfWidthMap.put('\uFF7B', "\uFF7B"); + sHalfWidthMap.put('\uFF7C', "\uFF7C"); + sHalfWidthMap.put('\uFF7D', "\uFF7D"); + sHalfWidthMap.put('\uFF7E', "\uFF7E"); + sHalfWidthMap.put('\uFF7F', "\uFF7F"); + sHalfWidthMap.put('\uFF80', "\uFF80"); + sHalfWidthMap.put('\uFF81', "\uFF81"); + sHalfWidthMap.put('\uFF82', "\uFF82"); + sHalfWidthMap.put('\uFF83', "\uFF83"); + sHalfWidthMap.put('\uFF84', "\uFF84"); + sHalfWidthMap.put('\uFF85', "\uFF85"); + sHalfWidthMap.put('\uFF86', "\uFF86"); + sHalfWidthMap.put('\uFF87', "\uFF87"); + sHalfWidthMap.put('\uFF88', "\uFF88"); + sHalfWidthMap.put('\uFF89', "\uFF89"); + sHalfWidthMap.put('\uFF8A', "\uFF8A"); + sHalfWidthMap.put('\uFF8B', "\uFF8B"); + sHalfWidthMap.put('\uFF8C', "\uFF8C"); + sHalfWidthMap.put('\uFF8D', "\uFF8D"); + sHalfWidthMap.put('\uFF8E', "\uFF8E"); + sHalfWidthMap.put('\uFF8F', "\uFF8F"); + sHalfWidthMap.put('\uFF90', "\uFF90"); + sHalfWidthMap.put('\uFF91', "\uFF91"); + sHalfWidthMap.put('\uFF92', "\uFF92"); + sHalfWidthMap.put('\uFF93', "\uFF93"); + sHalfWidthMap.put('\uFF94', "\uFF94"); + sHalfWidthMap.put('\uFF95', "\uFF95"); + sHalfWidthMap.put('\uFF96', "\uFF96"); + sHalfWidthMap.put('\uFF97', "\uFF97"); + sHalfWidthMap.put('\uFF98', "\uFF98"); + sHalfWidthMap.put('\uFF99', "\uFF99"); + sHalfWidthMap.put('\uFF9A', "\uFF9A"); + sHalfWidthMap.put('\uFF9B', "\uFF9B"); + sHalfWidthMap.put('\uFF9C', "\uFF9C"); + sHalfWidthMap.put('\uFF9D', "\uFF9D"); + sHalfWidthMap.put('\uFF9E', "\uFF9E"); + sHalfWidthMap.put('\uFF9F', "\uFF9F"); + sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); + } + + /** + * Return half-width version of that character if possible. Return null if not possible + * @param ch input character + * @return CharSequence object if the mapping for ch exists. Return null otherwise. + */ + public static CharSequence tryGetHalfWidthText(char ch) { + if (sHalfWidthMap.containsKey(ch)) { + return sHalfWidthMap.get(ch); + } else { + return null; + } + } +}
\ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java index a0513f1..f2a2733 100644 --- a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java @@ -28,6 +28,7 @@ import android.syncml.pim.PropertyNode; import android.syncml.pim.VBuilder; import android.syncml.pim.VNode; import android.syncml.pim.VParser; +import android.text.TextUtils; import android.util.CharsetUtils; import android.util.Log; @@ -403,7 +404,10 @@ public class VCardDataBuilder implements VBuilder { String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); String encoding = paramMap.getAsString("ENCODING"); - if (targetCharset == null || targetCharset.length() == 0) { + Log.d("@@@", String.format("targetCharset: \"%s\", encoding: \"%s\"", + targetCharset, encoding)); + + if (TextUtils.isEmpty(targetCharset)) { targetCharset = mTargetCharset; } diff --git a/core/java/android/syncml/pim/vcard/VCardParser.java b/core/java/android/syncml/pim/vcard/VCardParser.java index 6dad852d..9a590dd 100644 --- a/core/java/android/syncml/pim/vcard/VCardParser.java +++ b/core/java/android/syncml/pim/vcard/VCardParser.java @@ -17,7 +17,6 @@ package android.syncml.pim.vcard; import android.syncml.pim.VDataBuilder; -import android.syncml.pim.VParser; import android.util.Config; import android.util.Log; |