summaryrefslogtreecommitdiffstats
path: root/core/java/android/pim
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/pim')
-rw-r--r--core/java/android/pim/vcard/ContactStruct.java1244
-rw-r--r--core/java/android/pim/vcard/EntryCommitter.java88
-rw-r--r--core/java/android/pim/vcard/EntryHandler.java33
-rw-r--r--core/java/android/pim/vcard/VCardBuilder.java64
-rw-r--r--core/java/android/pim/vcard/VCardBuilderCollection.java99
-rw-r--r--core/java/android/pim/vcard/VCardConfig.java59
-rw-r--r--core/java/android/pim/vcard/VCardDataBuilder.java319
-rw-r--r--core/java/android/pim/vcard/VCardEntryCounter.java60
-rw-r--r--core/java/android/pim/vcard/VCardParser.java90
-rw-r--r--core/java/android/pim/vcard/VCardParser_V21.java948
-rw-r--r--core/java/android/pim/vcard/VCardParser_V30.java306
-rw-r--r--core/java/android/pim/vcard/VCardSourceDetector.java137
-rw-r--r--core/java/android/pim/vcard/exception/VCardException.java35
-rw-r--r--core/java/android/pim/vcard/exception/VCardNestedException.java29
-rw-r--r--core/java/android/pim/vcard/exception/VCardNotSupportedException.java33
-rw-r--r--core/java/android/pim/vcard/exception/VCardVersionException.java29
-rw-r--r--core/java/android/pim/vcard/exception/package.html5
-rw-r--r--core/java/android/pim/vcard/package.html5
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