diff options
Diffstat (limited to 'core/java/android/pim')
18 files changed, 3583 insertions, 0 deletions
diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java new file mode 100644 index 0000000..46725d3 --- /dev/null +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -0,0 +1,1244 @@ +/* + * 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.AbstractSyncableContentProvider; +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.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +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"; + + /** + * @hide only for testing + */ + static public class PhoneData { + public final int type; + public final String data; + public final String label; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PhoneData(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 PhoneData) { + return false; + } + PhoneData phoneData = (PhoneData)obj; + return (type == phoneData.type && data.equals(phoneData.data) && + label.equals(phoneData.label) && isPrimary == phoneData.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 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. + public final int type; + public final String data; + // Used only when TYPE 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 ContactMethod(int kind, int type, String data, String label, + boolean isPrimary) { + this.kind = kind; + this.type = type; + this.data = data; + this.label = data; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ContactMethod) { + return false; + } + ContactMethod contactMethod = (ContactMethod)obj; + return (kind == contactMethod.kind && type == contactMethod.type && + data.equals(contactMethod.data) && label.equals(contactMethod.label) && + isPrimary == contactMethod.isPrimary); + } + + @Override + public String toString() { + return String.format("kind: %d, type: %d, data: %s, label: %s, isPrimary: %s", + kind, type, data, label, isPrimary); + } + } + + /** + * @hide only for testing + */ + static public class OrganizationData { + public final int type; + public final String companyName; + // can be changed in some VCard format. + public String positionName; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public OrganizationData(int type, String companyName, String positionName, + boolean isPrimary) { + this.type = type; + this.companyName = companyName; + this.positionName = positionName; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof OrganizationData) { + return false; + } + OrganizationData organization = (OrganizationData)obj; + return (type == organization.type && companyName.equals(organization.companyName) && + positionName.equals(organization.positionName) && + isPrimary == organization.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, company: %s, position: %s, isPrimary: %s", + type, companyName, positionName, isPrimary); + } + } + + static class Property { + private String mPropertyName; + private Map<String, Collection<String>> mParameterMap = + new HashMap<String, Collection<String>>(); + private List<String> mPropertyValueList = new ArrayList<String>(); + private byte[] mPropertyBytes; + + public Property() { + clear(); + } + + public void setPropertyName(final String propertyName) { + mPropertyName = propertyName; + } + + public void addParameter(final String paramName, final String paramValue) { + Collection<String> values; + if (mParameterMap.containsKey(paramName)) { + if (paramName.equals("TYPE")) { + values = new HashSet<String>(); + } else { + values = new ArrayList<String>(); + } + mParameterMap.put(paramName, values); + } else { + values = mParameterMap.get(paramName); + } + } + + public void addToPropertyValueList(final String propertyValue) { + mPropertyValueList.add(propertyValue); + } + + public void setPropertyBytes(final byte[] propertyBytes) { + mPropertyBytes = propertyBytes; + } + + public final Collection<String> getParameters(String type) { + return mParameterMap.get(type); + } + + public final List<String> getPropertyValueList() { + return mPropertyValueList; + } + + public void clear() { + mPropertyName = null; + mParameterMap.clear(); + mPropertyValueList.clear(); + } + } + + 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 int mNameOrderType; + + /* private variables bellow is for temporary use. */ + + // 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; + + // 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; + + // 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. + private boolean mPrefIsSet_Address; + private boolean mPrefIsSet_Phone; + private boolean mPrefIsSet_Email; + private boolean mPrefIsSet_Organization; + + public ContactStruct() { + mNameOrderType = VCardConfig.NAME_ORDER_TYPE_DEFAULT; + } + + public ContactStruct(int nameOrderType) { + mNameOrderType = nameOrderType; + } + + /** + * @hide only for test + */ + public ContactStruct(String name, + String phoneticName, + byte[] photoBytes, + List<String> notes, + List<PhoneData> phoneList, + List<ContactMethod> contactMethodList, + List<OrganizationData> organizationList, + Map<String, List<String>> extensionMap) { + mName = name; + mPhoneticName = phoneticName; + mPhotoBytes = photoBytes; + mContactMethodList = contactMethodList; + mOrganizationList = organizationList; + mExtensionMap = extensionMap; + } + + /** + * @hide only for test + */ + public String getName() { + return mName; + } + + /** + * @hide only for test + */ + public String getPhoneticName() { + return mPhoneticName; + } + + /** + * @hide only for test + */ + public final byte[] getPhotoBytes() { + return mPhotoBytes; + } + + /** + * @hide only for test + */ + public final List<String> getNotes() { + return mNotes; + } + + /** + * @hide only for test + */ + public final List<PhoneData> getPhoneList() { + return mPhoneList; + } + + /** + * @hide only for test + */ + public final List<ContactMethod> getContactMethodList() { + return mContactMethodList; + } + + /** + * @hide only for test + */ + public final List<OrganizationData> getOrganizationList() { + return mOrganizationList; + } + + /** + * @hide only for test + */ + public final Map<String, List<String>> getExtensionMap() { + return mExtensionMap; + } + + /** + * Add a phone info to phoneList. + * @param data phone number + * @param type type col of content://contacts/phones + * @param label lable col of content://contacts/phones + */ + private void addPhone(int type, String data, String label, boolean isPrimary){ + if (mPhoneList == null) { + mPhoneList = new ArrayList<PhoneData>(); + } + StringBuilder builder = new StringBuilder(); + String trimed = data.trim(); + int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } + } + + PhoneData phoneData = new PhoneData(type, + PhoneNumberUtils.formatNumber(builder.toString()), + label, isPrimary); + + 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>(); + } + mContactMethodList.add(new ContactMethod(kind, type, data, label, isPrimary)); + } + + /** + * Add a Organization info to organizationList. + */ + private void addOrganization(int type, String companyName, String positionName, + boolean isPrimary) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList<OrganizationData>(); + } + mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); + } + + /** + * Set "position" value to the appropriate data. If there's more than one + * OrganizationData objects, the value is set to the last one. If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * empty. + * + * TODO: incomplete logic. fix this: + * + * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not + * know how to handle it in general cases... + * ---- + * TITLE:Software Engineer + * ORG:Google + * ---- + */ + private void setPosition(String positionValue) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList<OrganizationData>(); + } + int size = mOrganizationList.size(); + if (size == 0) { + addOrganization(Contacts.OrganizationColumns.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) { + return; + } + // Now store the string into extensionMap. + List<String> list; + if (mExtensionMap == null) { + mExtensionMap = new HashMap<String, List<String>>(); + } + 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("],"); + } + + 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("],"); + } + + 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("],"); + } + + 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 ""; + } + } + + public void addProperty(Property property) { + String propName = property.mPropertyName; + final Map<String, Collection<String>> paramMap = property.mParameterMap; + final List<String> propValueList = property.mPropertyValueList; + byte[] propBytes = property.mPropertyBytes; + + if (propValueList.size() == 0) { + return; + } + + String propValue = listToString(propValueList); + + 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; + } else if (propName.equals("N")) { + mTmpNameFromNProperty = getNameFromNProperty(propValueList, mNameOrderType); + } else if (propName.equals("SORT-STRING")) { + mPhoneticName = 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(); + } + } else { + addExtension(propName, paramMap, propValueList); + } + } else if (propName.equals("ADR")) { + boolean valuesAreAllEmpty = true; + for (String value : propValueList) { + if (value.length() > 0) { + valuesAreAllEmpty = false; + break; + } + } + if (valuesAreAllEmpty) { + return; + } + + int kind = Contacts.KIND_POSTAL; + int type = -1; + String label = ""; + boolean isPrimary = false; + Collection<String> typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals("PREF") && !mPrefIsSet_Address) { + // Only first "PREF" is considered. + mPrefIsSet_Address = true; + isPrimary = true; + } else if (typeString.equalsIgnoreCase("HOME")) { + type = Contacts.ContactMethodsColumns.TYPE_HOME; + label = ""; + } else if (typeString.equalsIgnoreCase("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; + 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) { + // 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; + label = typeString; + } + } + } + // We use "HOME" as default + if (type < 0) { + type = Contacts.ContactMethodsColumns.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; + } + } + } + address = builder.toString().trim(); + } else { + address = propValue; + } + addContactmethod(kind, type, address, label, isPrimary); + } else if (propName.equals("ORG")) { + // vCard specification does not specify other types. + int type = Contacts.OrganizationColumns.TYPE_WORK; + boolean isPrimary = false; + + Collection<String> typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals("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()); + if (iter.hasNext()) { + 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. + Collection<String> paramMapValue = paramMap.get("VALUE"); + if (paramMapValue != null && paramMapValue.contains("URL")) { + // TODO: do something. + } 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(); + }*/ + } + } 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; + } + } + } + if (type < 0) { + type = Contacts.ContactMethodsColumns.TYPE_OTHER; + } + addContactmethod(Contacts.KIND_EMAIL, type, propValue,label, isPrimary); + } else if (propName.equals("TEL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + boolean isFax = false; + Collection<String> typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals("PREF") && !mPrefIsSet_Phone) { + // Only first "PREF" is considered. + mPrefIsSet_Phone = true; + 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; + } + } + } + if (type < 0) { + type = Contacts.PhonesColumns.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); + } else if (propName.equals("NOTE")) { + if (mNotes == null) { + mNotes = new ArrayList<String>(1); + } + mNotes.add(propValue); + } else if (propName.equals("BDAY")) { + addExtension(propName, paramMap, propValueList); + } else if (propName.equals("URL")) { + addExtension(propName, paramMap, propValueList); + } 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; + } 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; + } + } + } + if (mPhoneList != null && mPhoneList.size() > 0) { + for (PhoneData phoneData : mPhoneList) { + if (phoneData.isPrimary) { + return phoneData.data; + } + } + } + return ""; + } + + /** + * 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(); + } + + // 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 there is no "PREF", we choose the first entries as primary. + if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) { + 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_Email && mContactMethodList != null) { + for (ContactMethod contactMethod : mContactMethodList) { + if (contactMethod.kind == Contacts.KIND_EMAIL) { + contactMethod.isPrimary = true; + break; + } + } + } + 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)); + } + } + + 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; + } + } + } + + if (personUri == null) { + Log.e(LOG_TAG, "Failed to create the contact."); + return; + } + + 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); + } + } + + long primaryPhoneId = -1; + if (mPhoneList != null && mPhoneList.size() > 0) { + 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); + } + if (phoneData.isPrimary) { + primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment()); + } + } + } + + long primaryOrganizationId = -1; + if (mOrganizationList != null && mOrganizationList.size() > 0) { + for (OrganizationData organizationData : mOrganizationList) { + ContentValues values = new ContentValues(); + // 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()); + } + } + } + + 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); + } + 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); + } + } + } + } + + if (mExtensionMap != null && mExtensionMap.size() > 0) { + ArrayList<ContentValues> contentValuesArray; + if (resolver != null) { + contentValuesArray = new ArrayList<ContentValues>(); + } else { + contentValuesArray = null; + } + 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 (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 (primaryEmailId >= 0) { + values.put(People.PRIMARY_EMAIL_ID, primaryEmailId); + } + if (resolver != null) { + resolver.update(personUri, values, null, null); + } else { + provider.update(personUri, values, null, null); + } + } + } + + /** + * 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(); + try { + pushIntoContentProviderOrResolver(provider, myContactsGroupId); + successful = true; + } finally { + provider.endBatch(successful); + } + } + + public boolean isIgnorable() { + return TextUtils.isEmpty(mName) && + TextUtils.isEmpty(mPhoneticName) && + (mPhoneList == null || mPhoneList.size() == 0) && + (mContactMethodList == null || mContactMethodList.size() == 0); + } + + private String listToString(List<String> list){ + final int size = list.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + int i = 0; + for (String type : list) { + builder.append(type); + if (i < size - 1) { + builder.append(";"); + } + } + return builder.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; + } + } +} diff --git a/core/java/android/pim/vcard/EntryCommitter.java b/core/java/android/pim/vcard/EntryCommitter.java new file mode 100644 index 0000000..e26fac5 --- /dev/null +++ b/core/java/android/pim/vcard/EntryCommitter.java @@ -0,0 +1,88 @@ +/* + * 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.AbstractSyncableContentProvider; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.IContentProvider; +import android.provider.Contacts; +import android.util.Log; + +/** + * EntryHandler implementation which commits the entry to Contacts Provider + */ +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 onFinal() { + if (VCardConfig.showPerformanceLog()) { + Log.d(LOG_TAG, + String.format("time to commit entries: %ld 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); + } + 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 new file mode 100644 index 0000000..4015cb5 --- /dev/null +++ b/core/java/android/pim/vcard/EntryHandler.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Unlike VCardBuilderBase, this (and 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? + */ + public void onFinal(); + + /** + * The method called when one VCard entry is successfully created + */ + public void onEntryCreated(final ContactStruct entry); +} diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java new file mode 100644 index 0000000..e1c4b33 --- /dev/null +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -0,0 +1,64 @@ +/* + * 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 java.util.List; + +public interface VCardBuilder { + void start(); + + void end(); + + /** + * BEGIN:VCARD + */ + void startRecord(String type); + + /** END:VXX */ + void endRecord(); + + void startProperty(); + + void endProperty(); + + /** + * @param group + */ + void propertyGroup(String group); + + /** + * @param name + * N <br> + * N + */ + void propertyName(String name); + + /** + * @param type + * LANGUAGE \ ENCODING <br> + * ;LANGUage= \ ;ENCODING= + */ + void propertyParamType(String type); + + /** + * @param value + * FR-EN \ GBK <br> + * FR-EN \ GBK + */ + void propertyParamValue(String value); + + void propertyValues(List<String> values); +} diff --git a/core/java/android/pim/vcard/VCardBuilderCollection.java b/core/java/android/pim/vcard/VCardBuilderCollection.java new file mode 100644 index 0000000..e3985b6 --- /dev/null +++ b/core/java/android/pim/vcard/VCardBuilderCollection.java @@ -0,0 +1,99 @@ +/* + * 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 java.util.Collection; +import java.util.List; + +public class VCardBuilderCollection implements VCardBuilder { + + private final Collection<VCardBuilder> mVCardBuilderCollection; + + public VCardBuilderCollection(Collection<VCardBuilder> vBuilderCollection) { + mVCardBuilderCollection = vBuilderCollection; + } + + public Collection<VCardBuilder> getVCardBuilderBaseCollection() { + return mVCardBuilderCollection; + } + + public void start() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.start(); + } + } + + public void end() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.end(); + } + } + + public void startRecord(String type) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.startRecord(type); + } + } + + public void endRecord() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.endRecord(); + } + } + + public void startProperty() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.startProperty(); + } + } + + + public void endProperty() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.endProperty(); + } + } + + public void propertyGroup(String group) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyGroup(group); + } + } + + public void propertyName(String name) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyName(name); + } + } + + public void propertyParamType(String type) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyParamType(type); + } + } + + public void propertyParamValue(String value) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyParamValue(value); + } + } + + public void propertyValues(List<String> values) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyValues(values); + } + } +} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java new file mode 100644 index 0000000..fef9dba --- /dev/null +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -0,0 +1,59 @@ +/* + * 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; + +/** + * The class representing VCard related configurations + */ +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 = + LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING; + + // 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; + + protected static final int LOG_LEVEL = LOG_LEVEL_PERFORMANCE_MEASUREMENT; + + // 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; + + public static final int NAME_ORDER_TYPE_DEFAULT = NAME_ORDER_TYPE_ENGLISH; + + /** + * @hide temporal. may be deleted + */ + public static boolean showPerformanceLog() { + return (LOG_LEVEL & LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; + } + + private VCardConfig() { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java new file mode 100644 index 0000000..4025f6c --- /dev/null +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -0,0 +1,319 @@ +/* + * 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.util.CharsetUtils; +import android.util.Log; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * VBuilder for VCard. VCard may contain big photo images encoded by BASE64, + * If we store all VNode entries in memory like VDataBuilder.java, + * OutOfMemoryError may be thrown. Thus, this class push each VCard entry into + * ContentResolver immediately. + */ +public class VCardDataBuilder implements VCardBuilder { + static private String LOG_TAG = "VCardDataBuilder"; + + /** + * If there's no other information available, this class uses this charset for encoding + * byte arrays. + */ + static public String TARGET_CHARSET = "UTF-8"; + + private ContactStruct.Property mCurrentProperty = new ContactStruct.Property(); + private ContactStruct mCurrentContactStruct; + private String mParamType; + + /** + * The charset using which VParser parses the text. + */ + private String mSourceCharset; + + /** + * The charset with which byte array is encoded to String. + */ + private String mTargetCharset; + private boolean mStrictLineBreakParsing; + + private int mNameOrderType; + + // Just for testing. + private long mTimePushIntoContentResolver; + + private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); + + public VCardDataBuilder() { + this(null, null, false, VCardConfig.NAME_ORDER_TYPE_DEFAULT); + } + + /** + * @hide + */ + public VCardDataBuilder(int nameOrderType) { + this(null, null, false, nameOrderType); + } + + /** + * @hide + */ + public VCardDataBuilder(String charset, + boolean strictLineBreakParsing, + int nameOrderType) { + this(null, charset, strictLineBreakParsing, nameOrderType); + } + + /** + * @hide + */ + public VCardDataBuilder(String sourceCharset, + String targetCharset, + boolean strictLineBreakParsing, + int nameOrderType) { + if (sourceCharset != null) { + mSourceCharset = sourceCharset; + } else { + mSourceCharset = VCardConfig.DEFAULT_CHARSET; + } + if (targetCharset != null) { + mTargetCharset = targetCharset; + } else { + mTargetCharset = TARGET_CHARSET; + } + mStrictLineBreakParsing = strictLineBreakParsing; + mNameOrderType = nameOrderType; + } + + public void addEntryHandler(EntryHandler entryHandler) { + mEntryHandlers.add(entryHandler); + } + + public void start() { + } + + public void end() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onFinal(); + } + } + + /** + * Assume that VCard is not nested. In other words, this code does not accept + */ + public void startRecord(String type) { + // TODO: add the method clear() instead of using null for reducing GC? + if (mCurrentContactStruct != null) { + // This means startRecord() is called inside startRecord() - endRecord() block. + // TODO: should throw some Exception + Log.e(LOG_TAG, "Nested VCard code is not supported now."); + } + if (!type.equalsIgnoreCase("VCARD")) { + // TODO: add test case for this + Log.e(LOG_TAG, "This is not VCARD!"); + } + + mCurrentContactStruct = new ContactStruct(mNameOrderType); + } + + public void endRecord() { + mCurrentContactStruct.consolidateFields(); + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEntryCreated(mCurrentContactStruct); + } + mCurrentContactStruct = null; + } + + public void startProperty() { + mCurrentProperty.clear(); + } + + public void endProperty() { + mCurrentContactStruct.addProperty(mCurrentProperty); + } + + public void propertyName(String name) { + mCurrentProperty.setPropertyName(name); + } + + public void propertyGroup(String group) { + // ContactStruct does not support Group. + } + + public void propertyParamType(String type) { + if (mParamType != null) { + Log.e(LOG_TAG, + "propertyParamType() is called more than once " + + "before propertyParamValue() is called"); + } + mParamType = type; + } + + public void propertyParamValue(String value) { + if (mParamType == null) { + mParamType = "TYPE"; + } + mCurrentProperty.addParameter(mParamType, value); + mParamType = null; + } + + private String encodeString(String originalString, String targetCharset) { + if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + Charset charset = Charset.forName(mSourceCharset); + ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return null; + } + } + + private String handleOneValue(String value, String targetCharset, String encoding) { + if (encoding != null) { + if (encoding.equals("BASE64") || encoding.equals("B")) { + mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); + return value; + } else if (encoding.equals("QUOTED-PRINTABLE")) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + String quotedPrintable = builder.toString(); + + String[] lines; + if (mStrictLineBreakParsing) { + lines = quotedPrintable.split("\r\n"); + } else { + builder = new StringBuilder(); + length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + String finalLine = builder.toString(); + if (finalLine.length() > 0) { + list.add(finalLine); + } + lines = list.toArray(new String[0]); + } + + builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + byte[] bytes; + try { + bytes = builder.toString().getBytes(mSourceCharset); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); + bytes = builder.toString().getBytes(); + } + + try { + bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + return ""; + } + + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); + } + } + // Unknown encoding. Fall back to default. + } + return encodeString(value, targetCharset); + } + + public void propertyValues(List<String> values) { + if (values == null || values.size() == 0) { + return; + } + + final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); + 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); + + if (targetCharset == null || targetCharset.length() == 0) { + targetCharset = mTargetCharset; + } + + for (String value : values) { + mCurrentProperty.addToPropertyValueList( + handleOneValue(value, targetCharset, encoding)); + } + } + + public void showPerformanceInfo() { + Log.d(LOG_TAG, "time for insert ContactStruct to database: " + + mTimePushIntoContentResolver + " ms"); + } +} diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java new file mode 100644 index 0000000..f99b46c --- /dev/null +++ b/core/java/android/pim/vcard/VCardEntryCounter.java @@ -0,0 +1,60 @@ +/* + * 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 java.util.List; + +public class VCardEntryCounter implements VCardBuilder { + private int mCount; + + public int getCount() { + return mCount; + } + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void endRecord() { + mCount++; + } + + public void startProperty() { + } + + public void endProperty() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java new file mode 100644 index 0000000..b5e5049 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser.java @@ -0,0 +1,90 @@ +/* + * 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.pim.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class VCardParser { + + protected boolean mCanceled; + + /** + * Parses the given stream and send the VCard data into VCardBuilderBase object. + * + * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets + * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is + * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, + * In some exreme case, some VCard may have different charsets in one VCard (though + * we do not see any device which emits such kind of malicious data) + * + * In order to avoid "misunderstanding" charset as much as possible, this method + * use "ISO-8859-1" for reading the stream. When charset is specified in some property + * (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to + * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit + * characters, which is not completely sure. In some cases, this "decoding-encoding" + * scheme may fail. To avoid the case, + * + * We recommend you to use VCardSourceDetector and detect which kind of source the + * VCard comes from and explicitly specify a charset using the result. + * + * @param is The source to parse. + * @param builder The VCardBuilderBase object which used to construct data. If you want to + * include multiple VCardBuilderBase objects in this field, consider using + * {#link VCardBuilderCollection} class. + * @return Returns true for success. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which accept charset. + * + * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use + * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese + * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses + * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification + * (e.g. W53K). + * + * @param is The source to parse. + * @param charset Charset to be used. + * @param builder The VCardBuilderBase object. + * @return Returns true when successful. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, String charset, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which tells this object the operation is already canceled. + * XXX: Is this really necessary? + * @hide + */ + public abstract void parse(InputStream is, String charset, + VCardBuilder builder, boolean canceled) + throws IOException, VCardException; + + /** + * Cancel parsing. + * Actual cancel is done after the end of the current one vcard entry parsing. + */ + public void cancel() { + mCanceled = true; + } +} diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java new file mode 100644 index 0000000..17a138f --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -0,0 +1,948 @@ +/* + * 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.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardNestedException; +import android.pim.vcard.exception.VCardNotSupportedException; +import android.pim.vcard.exception.VCardVersionException; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +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"; + + /** Store the known-type */ + private static final HashSet<String> sKnownTypeSet = new HashSet<String>( + Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", + "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", + "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", + "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", + "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", + "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", + "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", + "WAVE", "AIFF", "PCM", "X509", "PGP")); + + /** Store the known-value */ + private static final HashSet<String> sKnownValueSet = new HashSet<String>( + Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); + + /** Store the property names available in vCard 2.1 */ + private static final HashSet<String> sAvailablePropertyNameV21 = + new HashSet<String>(Arrays.asList( + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "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... + private static final HashSet<String> sAvailableEncodingV21 = + new HashSet<String>(Arrays.asList( + "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); + + // Used only for parsing END:VCARD. + private String mPreviousLine; + + /** The builder to build parsed data */ + protected VCardBuilder mBuilder = null; + + /** The encoding type */ + protected String mEncoding = null; + + protected final String sDefaultEncoding = "8BIT"; + + // Should not directly read a line from this. Use getLine() instead. + protected BufferedReader mReader; + + // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. + // See v21_foma_1.vcf in test directory for more information. + private int mNestCount; + + // In order to reduce warning message as much as possible, we hold the value which made Logger + // emit a warning message. + protected HashSet<String> mWarningValueMap = new HashSet<String>(); + + // Just for debugging + private long mTimeTotal; + private long mTimeStartRecord; + private long mTimeEndRecord; + 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; + + /** + * Create a new VCard parser. + */ + public VCardParser_V21() { + super(); + } + + public VCardParser_V21(VCardSourceDetector detector) { + super(); + if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { + mNestCount = 1; + } + } + + /** + * Parse the file at the given position + * vcard_file = [wsls] vcard [wsls] + */ + protected void parseVCardFile() throws IOException, VCardException { + boolean firstReading = true; + while (true) { + if (mCanceled) { + break; + } + if (!parseOneVCard(firstReading)) { + break; + } + firstReading = false; + } + + if (mNestCount > 0) { + boolean useCache = true; + for (int i = 0; i < mNestCount; i++) { + readEndVCard(useCache, true); + useCache = false; + } + } + } + + protected String getVersion() { + return "2.1"; + } + + /** + * @return true when the propertyName is a valid property name. + */ + protected boolean isValidPropertyName(String propertyName) { + if (!(sAvailablePropertyNameV21.contains(propertyName.toUpperCase()) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); + } + return true; + } + + /** + * @return true when the encoding is a valid encoding. + */ + protected boolean isValidEncoding(String encoding) { + return sAvailableEncodingV21.contains(encoding.toUpperCase()); + } + + /** + * @return String. It may be null, or its length may be 0 + * @throws IOException + */ + protected String getLine() throws IOException { + return mReader.readLine(); + } + + /** + * @return String with it's length > 0 + * @throws IOException + * @throws VCardException when the stream reached end of line + */ + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Reached end of buffer."); + } else if (line.trim().length() > 0) { + return line; + } + } + } + + /** + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF + * "END" [ws] ":" [ws] "VCARD" + */ + private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { + boolean allowGarbage = false; + if (firstReading) { + if (mNestCount > 0) { + for (int i = 0; i < mNestCount; i++) { + if (!readBeginVCard(allowGarbage)) { + return false; + } + allowGarbage = true; + } + } + } + + if (!readBeginVCard(allowGarbage)) { + return false; + } + long start; + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.startRecord("VCARD"); + mTimeStartRecord += System.currentTimeMillis() - start; + } + start = System.currentTimeMillis(); + parseItems(); + mTimeParseItems += System.currentTimeMillis() - start; + readEndVCard(true, false); + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.endRecord(); + mTimeEndRecord += System.currentTimeMillis() - start; + } + return true; + } + + /** + * @return True when successful. False when reaching the end of line + * @throws IOException + * @throws VCardException + */ + protected boolean readBeginVCard(boolean allowGarbage) + throws IOException, VCardException { + String line; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } + } + String[] strArray = line.split(":", 2); + int length = strArray.length; + + // 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")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException( + "Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while(allowGarbage); + + throw new VCardException("Reached where must not be reached."); + } + + /** + * The arguments useCache and allowGarbase are usually true and false accordingly when + * this function is called outside this function itself. + * + * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() + * is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + String line; + do { + if (useCache) { + // Though vCard specification does not allow lower cases, + // some data may have them, so we allow it. + line = mPreviousLine; + } else { + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Expected END:VCARD was not found."); + } else if (line.trim().length() > 0) { + break; + } + } + } + + String[] strArray = line.split(":", 2); + if (strArray.length == 2 && + strArray[0].trim().equalsIgnoreCase("END") && + strArray[1].trim().equalsIgnoreCase("VCARD")) { + return; + } else if (!allowGarbage) { + throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); + } + useCache = false; + } while (allowGarbage); + } + + /** + * items = *CRLF item + * / item + */ + protected void parseItems() throws IOException, VCardException { + /* items *CRLF item / item */ + boolean ended = false; + + if (mBuilder != null) { + long start = System.currentTimeMillis(); + mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + ended = parseItem(); + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + + while (!ended) { + // follow VCARD ,it wont reach endProperty + if (mBuilder != null) { + long start = System.currentTimeMillis(); + mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + ended = parseItem(); + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + } + } + + /** + * 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; + + String line = getNonEmptyLine(); + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { + return true; + } + if (propertyNameAndValue.length != 2) { + throw new VCardException("Invalid line \"" + line + "\""); + } + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseItem1 += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || + propertyName.equals("ORG") || + propertyName.equals("N")) { + start = System.currentTimeMillis(); + handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseItem3 += System.currentTimeMillis() - start; + return false; + } else if (propertyName.equals("AGENT")) { + handleAgent(propertyValue); + return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } 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; + return false; + } + + throw new VCardException("Unknown property name: \"" + + propertyName + "\""); + } + + static private final int STATE_GROUP_OR_PROPNAME = 0; + static private final int STATE_PARAMS = 1; + // vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not. + // This is just for safety. + static private final int STATE_PARAMS_IN_DQUOTE = 2; + + protected String[] separateLineAndHandleGroup(String line) throws VCardException { + int length = line.length(); + int state = STATE_GROUP_OR_PROPNAME; + int nameIndex = 0; + + String[] propertyNameAndValue = new String[2]; + + for (int i = 0; i < length; i++) { + char ch = line.charAt(i); + switch (state) { + case STATE_GROUP_OR_PROPNAME: + if (ch == ':') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { + String groupName = line.substring(nameIndex, i); + if (mBuilder != null) { + mBuilder.propertyGroup(groupName); + } + nameIndex = i + 1; + } else if (ch == ';') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; + } + break; + case STATE_PARAMS: + if (ch == '"') { + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } + break; + case STATE_PARAMS_IN_DQUOTE: + if (ch == '"') { + state = STATE_PARAMS; + } + break; + } + } + + throw new VCardException("Invalid line: \"" + line + "\""); + } + + + /** + * params = ";" [ws] paramlist + * paramlist = paramlist [ws] ";" [ws] param + * / param + * param = "TYPE" [ws] "=" [ws] ptypeval + * / "VALUE" [ws] "=" [ws] pvalueval + * / "ENCODING" [ws] "=" [ws] pencodingval + * / "CHARSET" [ws] "=" [ws] charsetval + * / "LANGUAGE" [ws] "=" [ws] langval + * / "X-" word [ws] "=" [ws] word + * / knowntype + */ + protected void handleParams(String params) throws VCardException { + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + String paramName = strArray[0].trim(); + String paramValue = strArray[1].trim(); + if (paramName.equals("TYPE")) { + handleType(paramValue); + } else if (paramName.equals("VALUE")) { + handleValue(paramValue); + } else if (paramName.equals("ENCODING")) { + handleEncoding(paramValue); + } else if (paramName.equals("CHARSET")) { + handleCharset(paramValue); + } else if (paramName.equals("LANGUAGE")) { + handleLanguage(paramValue); + } else if (paramName.startsWith("X-")) { + handleAnyParam(paramName, paramValue); + } else { + throw new VCardException("Unknown type \"" + paramName + "\""); + } + } else { + handleType(strArray[0]); + } + } + + /** + * ptypeval = knowntype / "X-" word + */ + protected void handleType(String ptypeval) { + String upperTypeValue = ptypeval; + if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && + !mWarningValueMap.contains(ptypeval)) { + mWarningValueMap.add(ptypeval); + Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); + } + if (mBuilder != null) { + mBuilder.propertyParamType("TYPE"); + mBuilder.propertyParamValue(upperTypeValue); + } + } + + /** + * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word + */ + protected void handleValue(String pvalueval) throws VCardException { + if (sKnownValueSet.contains(pvalueval.toUpperCase()) || + pvalueval.startsWith("X-")) { + if (mBuilder != null) { + mBuilder.propertyParamType("VALUE"); + mBuilder.propertyParamValue(pvalueval); + } + } else { + throw new VCardException("Unknown value \"" + pvalueval + "\""); + } + } + + /** + * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word + */ + protected void handleEncoding(String pencodingval) throws VCardException { + if (isValidEncoding(pencodingval) || + pencodingval.startsWith("X-")) { + if (mBuilder != null) { + mBuilder.propertyParamType("ENCODING"); + mBuilder.propertyParamValue(pencodingval); + } + mEncoding = pencodingval; + } else { + throw new VCardException("Unknown encoding \"" + pencodingval + "\""); + } + } + + /** + * vCard specification only allows us-ascii and iso-8859-xxx (See RFC 1521), + * but some vCard contains other charset, so we allow them. + */ + protected void handleCharset(String charsetval) { + if (mBuilder != null) { + mBuilder.propertyParamType("CHARSET"); + mBuilder.propertyParamValue(charsetval); + } + } + + /** + * See also Section 7.1 of RFC 1521 + */ + protected void handleLanguage(String langval) throws VCardException { + String[] strArray = langval.split("-"); + if (strArray.length != 2) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + String tmp = strArray[0]; + int length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + tmp = strArray[1]; + length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + if (mBuilder != null) { + mBuilder.propertyParamType("LANGUAGE"); + mBuilder.propertyParamValue(langval); + } + } + + /** + * Mainly for "X-" type. This accepts any kind of type without check. + */ + protected void handleAnyParam(String paramName, String paramValue) { + if (mBuilder != null) { + mBuilder.propertyParamType(paramName); + mBuilder.propertyParamValue(paramValue); + } + } + + protected void handlePropertyValue( + String propertyName, String propertyValue) throws + IOException, VCardException { + if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + long start = System.currentTimeMillis(); + String result = getQuotedPrintable(propertyValue); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mBuilder.propertyValues(v); + } + mTimeHandlePropertyValue2 += System.currentTimeMillis() - start; + } else if (mEncoding.equalsIgnoreCase("BASE64") || + mEncoding.equalsIgnoreCase("B")) { + long start = System.currentTimeMillis(); + // It is very rare, but some BASE64 data may be so big that + // OutOfMemoryError occurs. To ignore such cases, use try-catch. + try { + String result = getBase64(propertyValue); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mBuilder.propertyValues(v); + } + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + if (mBuilder != null) { + mBuilder.propertyValues(null); + } + } + mTimeHandlePropertyValue3 += System.currentTimeMillis() - start; + } else { + if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") + || mEncoding.equalsIgnoreCase("8BIT") + || mEncoding.toUpperCase().startsWith("X-"))) { + Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); + } + + long start = System.currentTimeMillis(); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(maybeUnescapeText(propertyValue)); + mBuilder.propertyValues(v); + } + mTimeHandlePropertyValue1 += System.currentTimeMillis() - start; + } + } + + protected String getQuotedPrintable(String firstString) throws IOException, VCardException { + // Specifically, there may be some padding between = and CRLF. + // See the following: + // + // qp-line := *(qp-segment transport-padding CRLF) + // qp-part transport-padding + // qp-segment := qp-section *(SPACE / TAB) "=" + // ; Maximum length of 76 characters + // + // e.g. (from RFC 2045) + // Now's the time = + // for all folk to come= + // to the aid of their country. + if (firstString.trim().endsWith("=")) { + // remove "transport-padding" + int pos = firstString.length() - 1; + while(firstString.charAt(pos) != '=') { + } + StringBuilder builder = new StringBuilder(); + builder.append(firstString.substring(0, pos + 1)); + builder.append("\r\n"); + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing quoted-printable String"); + } + if (line.trim().endsWith("=")) { + // remove "transport-padding" + pos = line.length() - 1; + while(line.charAt(pos) != '=') { + } + builder.append(line.substring(0, pos + 1)); + builder.append("\r\n"); + } else { + builder.append(line); + break; + } + } + return builder.toString(); + } else { + return firstString; + } + } + + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * Mainly for "ADR", "ORG", and "N" + * We do not care the number of strnosemi here. + * + * addressparts = 0*6(strnosemi ";") strnosemi + * ; PO Box, Extended Addr, Street, Locality, Region, + * Postal Code, Country Name + * orgparts = *(strnosemi ";") strnosemi + * ; First is Organization Name, + * remainder are Organization Units. + * nameparts = 0*4(strnosemi ";") strnosemi + * ; Family, Given, Middle, Prefix, Suffix. + * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. + * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi + * ; To include a semicolon in this string, it must be escaped + * ; with a "\" character. + * + * 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. + 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(); + for (int i = 0; i < length; i++) { + char ch = propertyValue.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = propertyValue.charAt(i + 1); + String unescapedString = maybeUnescape(nextCh); + if (unescapedString != null) { + builder.append(unescapedString); + i++; + } else { + builder.append(ch); + } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); + } + } + list.add(builder.toString()); + mBuilder.propertyValues(list); + } + } + + /** + * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. + * + * item = ... + * / [groups "."] "AGENT" + * [params] ":" vcard CRLF + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF "END" [ws] ":" [ws] "VCARD" + * + */ + protected void handleAgent(String propertyValue) throws VCardException { + 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 || + strArray[0].trim().equalsIgnoreCase("BEGIN") && + strArray[1].trim().equalsIgnoreCase("VCARD"))) { + throw new VCardException("BEGIN:VCARD != \"" + propertyValue + "\""); + } + parseItems(); + readEndVCard(); + */ + } + + /** + * For vCard 3.0. + */ + protected String maybeUnescapeText(String text) { + return text; + } + + /** + * 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) { + // Original vCard 2.1 specification does not allow transformation + // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of + // this class allowed them, so keep it as is. + if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { + return String.valueOf(ch); + } else { + return null; + } + } + + @Override + public boolean parse(InputStream is, VCardBuilder builder) + throws IOException, VCardException { + return parse(is, VCardConfig.DEFAULT_CHARSET, builder); + } + + @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); + } + } + + mBuilder = builder; + + long start = System.currentTimeMillis(); + if (mBuilder != null) { + mBuilder.start(); + } + parseVCardFile(); + if (mBuilder != null) { + mBuilder.end(); + } + mTimeTotal += System.currentTimeMillis() - start; + + if (VCardConfig.showPerformanceLog()) { + showPerformanceInfo(); + } + + return true; + } + + @Override + public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled) + throws IOException, VCardException { + mCanceled = canceled; + parse(is, charset, builder); + } + + private void showPerformanceInfo() { + Log.d(LOG_TAG, "total parsing time: " + mTimeTotal + " ms"); + if (mReader instanceof CustomBufferedReader) { + 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"); + } + + private boolean isLetter(char ch) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + return true; + } + return false; + } +} + +class CustomBufferedReader extends BufferedReader { + private long mTime; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + long start = System.currentTimeMillis(); + String ret = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return ret; + } + + public long getTotalmillisecond() { + return mTime; + } +} diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java new file mode 100644 index 0000000..634d9f5 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -0,0 +1,306 @@ +/* + * 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.pim.vcard.exception.VCardException; +import android.util.Log; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +/** + * This class is used to parse vcard3.0. <br> + * 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 HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( + Arrays.asList( + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 + "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", + "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 + + // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. + private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( + Arrays.asList("7BIT", "8BIT", "BASE64", "B")); + + // Although RFC 2426 specifies some property must not have parameters, we allow it, + // since there may be some careers which violates the RFC... + private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); + + private String mPreviousLine; + + @Override + protected String getVersion() { + return "3.0"; + } + + @Override + protected boolean isValidPropertyName(String propertyName) { + if (!(sAcceptablePropsWithParam.contains(propertyName) || + acceptablePropsWithoutParam.contains(propertyName) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); + } + return true; + } + + @Override + protected boolean isValidEncoding(String encoding) { + return sAcceptableEncodingV30.contains(encoding.toUpperCase()); + } + + @Override + protected String getLine() throws IOException { + if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } else { + return mReader.readLine(); + } + } + + /** + * vCard 3.0 requires that the line with space at the beginning of the line + * must be combined with previous line. + */ + @Override + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + StringBuilder builder = null; + while (true) { + line = mReader.readLine(); + if (line == null) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + throw new VCardException("Reached end of buffer."); + } else if (line.length() == 0) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { + if (builder != null) { + // See Section 5.8.1 of RFC 2425 (MIME-DIR document). + // Following is the excerpts from it. + // + // DESCRIPTION:This is a long description that exists on a long line. + // + // Can be represented as: + // + // DESCRIPTION:This is a long description + // that exists on a long line. + // + // It could also be represented as: + // + // DESCRIPTION:This is a long descrip + // tion that exists o + // n a long line. + builder.append(line.substring(1)); + } else if (mPreviousLine != null) { + builder = new StringBuilder(); + builder.append(mPreviousLine); + mPreviousLine = null; + builder.append(line.substring(1)); + } else { + throw new VCardException("Space exists at the beginning of the line"); + } + } else { + if (mPreviousLine == null) { + mPreviousLine = line; + if (builder != null) { + return builder.toString(); + } + } else { + String ret = mPreviousLine; + mPreviousLine = line; + return ret; + } + } + } + } + + + /** + * vcard = [group "."] "BEGIN" ":" "VCARD" 1*CRLF + * 1*(contentline) + * ;A vCard object MUST include the VERSION, FN and N types. + * [group "."] "END" ":" "VCARD" 1*CRLF + */ + @Override + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + return super.readBeginVCard(allowGarbage); + } + + @Override + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + super.readEndVCard(useCache, allowGarbage); + } + + /** + * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. + */ + @Override + protected void handleParams(String params) throws VCardException { + try { + super.handleParams(params); + } catch (VCardException e) { + // maybe IANA type + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + handleAnyParam(strArray[0], strArray[1]); + } else { + // Must not come here in the current implementation. + throw new VCardException( + "Unknown params value: " + params); + } + } + } + + @Override + protected void handleAnyParam(String paramName, String paramValue) { + // vCard 3.0 accept comma-separated multiple values, but + // current PropertyNode does not accept it. + // For now, we do not split the values. + // + // TODO: fix this. + super.handleAnyParam(paramName, paramValue); + } + + /** + * vCard 3.0 defines + * + * param = param-name "=" param-value *("," param-value) + * param-name = iana-token / x-name + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + */ + @Override + protected void handleType(String ptypevalues) { + String[] ptypeArray = ptypevalues.split(","); + mBuilder.propertyParamType("TYPE"); + for (String value : ptypeArray) { + int length = value.length(); + if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); + } else { + mBuilder.propertyParamValue(value); + } + } + } + + @Override + protected void handleAgent(String propertyValue) throws VCardException { + // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.0. + // + // e.g. + // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n + // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n + // ET:jfriday@host.com\nEND:VCARD\n + // + // TODO: fix this. + // + // issue: + // vCard 3.0 also allows this as an example. + // + // AGENT;VALUE=uri: + // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com + // + // This is not VCARD. Should we support this? + throw new VCardException("AGENT in vCard 3.0 is not supported yet."); + } + + /** + * vCard 3.0 does not require two CRLF at the last of BASE64 data. + * It only requires that data should be MIME-encoded. + */ + @Override + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } else if (!line.startsWith(" ") && !line.startsWith("\t")) { + mPreviousLine = line; + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") + * ; \\ encodes \, \n or \N encodes newline + * ; \; encodes ;, \, encodes , + * + * Note: Apple escape ':' into '\:' while does not escape '\' + */ + @Override + protected String maybeUnescapeText(String text) { + StringBuilder builder = new StringBuilder(); + int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\r\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + protected String maybeUnescape(char ch) { + if (ch == 'n' || ch == 'N') { + return "\r\n"; + } else { + return String.valueOf(ch); + } + } +} diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java new file mode 100644 index 0000000..7e2be2b --- /dev/null +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -0,0 +1,137 @@ +/* + * 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 java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class which tries to detects the source of the vCard from its properties. + * Currently this implementation is very premature. + * @hide + */ +public class VCardSourceDetector implements VCardBuilder { + // Should only be used in package. + static final int TYPE_UNKNOWN = 0; + static final int TYPE_APPLE = 1; + static final int TYPE_JAPANESE_MOBILE_PHONE = 2; // Used in Japanese mobile phones. + static final int TYPE_FOMA = 3; // Used in some Japanese FOMA mobile phones. + static final int TYPE_WINDOWS_MOBILE_JP = 4; + // TODO: Excel, etc. + + private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( + "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", + "X-ABADR", "X-ABUID")); + + private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-GNO", "X-GN", "X-REDUCTION")); + + private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC")); + + // Note: these signes appears before the signs of the other type (e.g. "X-GN"). + // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES. + private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList( + "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", + "X-SD-DESCRIPTION")); + private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; + + private int mType = TYPE_UNKNOWN; + // Some mobile phones (like FOMA) tells us the charset of the data. + private boolean mNeedParseSpecifiedCharset; + private String mSpecifiedCharset; + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void startProperty() { + mNeedParseSpecifiedCharset = false; + } + + public void endProperty() { + } + + public void endRecord() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { + mType = TYPE_FOMA; + mNeedParseSpecifiedCharset = true; + return; + } + if (mType != TYPE_UNKNOWN) { + return; + } + if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_WINDOWS_MOBILE_JP; + } else if (FOMA_SIGNS.contains(name)) { + mType = TYPE_FOMA; + } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_JAPANESE_MOBILE_PHONE; + } else if (APPLE_SIGNS.contains(name)) { + mType = TYPE_APPLE; + } + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + if (mNeedParseSpecifiedCharset && values.size() > 0) { + mSpecifiedCharset = values.get(0); + } + } + + int getType() { + return mType; + } + + /** + * Return charset String guessed from the source's properties. + * This method must be called after parsing target file(s). + * @return Charset String. Null is returned if guessing the source fails. + */ + public String getEstimatedCharset() { + if (mSpecifiedCharset != null) { + return mSpecifiedCharset; + } + switch (mType) { + case TYPE_WINDOWS_MOBILE_JP: + case TYPE_FOMA: + case TYPE_JAPANESE_MOBILE_PHONE: + return "SHIFT_JIS"; + case TYPE_APPLE: + return "UTF-8"; + default: + return null; + } + } +} diff --git a/core/java/android/pim/vcard/exception/VCardException.java b/core/java/android/pim/vcard/exception/VCardException.java new file mode 100644 index 0000000..e557219 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardException.java @@ -0,0 +1,35 @@ +/* + * 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.exception; + +public class VCardException extends java.lang.Exception { + /** + * Constructs a VCardException object + */ + public VCardException() { + super(); + } + + /** + * Constructs a VCardException object + * + * @param message the error message + */ + public VCardException(String message) { + super(message); + } + +} diff --git a/core/java/android/pim/vcard/exception/VCardNestedException.java b/core/java/android/pim/vcard/exception/VCardNestedException.java new file mode 100644 index 0000000..503c2fb --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardNestedException.java @@ -0,0 +1,29 @@ +/* + * 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.exception; + +/** + * VCardException thrown when VCard is nested without VCardParser's being notified. + */ +public class VCardNestedException extends VCardNotSupportedException { + public VCardNestedException() { + super(); + } + public VCardNestedException(String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java new file mode 100644 index 0000000..616aa77 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java @@ -0,0 +1,33 @@ +/* + * 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.exception; + +/** + * The exception which tells that the input VCard is probably valid from the view of + * specification but not supported in the current framework for now. + * + * This is a kind of a good news from the view of development. + * It may be good to ask users to send a report with the VCard example + * for the future development. + */ +public class VCardNotSupportedException extends VCardException { + public VCardNotSupportedException() { + super(); + } + public VCardNotSupportedException(String message) { + super(message); + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/pim/vcard/exception/VCardVersionException.java new file mode 100644 index 0000000..9fe8b7f --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardVersionException.java @@ -0,0 +1,29 @@ +/* + * 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.exception; + +/** + * VCardException used only when the version of the vCard is different. + */ +public class VCardVersionException extends VCardException { + public VCardVersionException() { + super(); + } + public VCardVersionException(String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/package.html b/core/java/android/pim/vcard/exception/package.html new file mode 100644 index 0000000..26b8a32 --- /dev/null +++ b/core/java/android/pim/vcard/exception/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +{@hide} +</BODY> +</HTML>
\ No newline at end of file diff --git a/core/java/android/pim/vcard/package.html b/core/java/android/pim/vcard/package.html new file mode 100644 index 0000000..26b8a32 --- /dev/null +++ b/core/java/android/pim/vcard/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +{@hide} +</BODY> +</HTML>
\ No newline at end of file |