diff options
author | Daisuke Miyakawa <dmiyakawa@google.com> | 2009-08-23 10:01:32 +0900 |
---|---|---|
committer | Daisuke Miyakawa <dmiyakawa@google.com> | 2009-08-25 15:39:06 +0900 |
commit | f4ddea769098e24a7316b9ee895d323005433c2c (patch) | |
tree | b3397a8bd06d6d499bf089c2ea0aa5457e43b385 /core/java/android/pim | |
parent | 2e951b5511750ebfbba28263ee5384bfa7f45128 (diff) | |
download | frameworks_base-f4ddea769098e24a7316b9ee895d323005433c2c.zip frameworks_base-f4ddea769098e24a7316b9ee895d323005433c2c.tar.gz frameworks_base-f4ddea769098e24a7316b9ee895d323005433c2c.tar.bz2 |
Refactor VCard handling code, phase 2, 3, 4, 5
Phase 2
Make VCard Importer use Data structures in ContactsContract instead of
using old Conatacts structure.
Phase 3
Developed VCardComposer, which was originally in Contacts package, but
now in base/core/java. Also made it use queryEntries() as per jsharkey's
suggestion.
Phase 4
Added VCardUtils and moved some common methods to it, some of which should be
in public API, but hidden for now.
Phase 5
Made VCardComposer emits (almost) valid vCard 3.0 data.
Confirmed with vCard data emitted by Mac.
Related issue:
1784580, 1728351, 1967349
Note:
Probable next step:
- Add "fast parse" mode in VCradBuilder, in which, VCardBuilder skip parsing the value of
each property. It will make the parsing faster.
-- Note that parsing the parameters of each entry cannot be skipped, since it may contains
the information about Encoding of the property. In other words, if the line is
in Quoted-Printable format, the next line may be the part of the property, not a
separated property, which should be parsed accordingly.
- Needs test
Diffstat (limited to 'core/java/android/pim')
-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 |
10 files changed, 3344 insertions, 905 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 |