summaryrefslogtreecommitdiffstats
path: root/core/java/android
diff options
context:
space:
mode:
authorAndroid (Google) Code Review <android-gerrit@google.com>2009-05-18 16:29:27 -0700
committerAndroid (Google) Code Review <android-gerrit@google.com>2009-05-18 16:29:27 -0700
commit842a1f4b0beaacfaab940318fe19909e087aae81 (patch)
tree8eeb209249ce3602d7015c32bcff9849be8cafe1 /core/java/android
parent656a2726ad977be194c3b8f4bf4ec3068bf3e1c9 (diff)
parent841ddefcb5720bcc1925f373b66224411f99a9fa (diff)
downloadframeworks_base-842a1f4b0beaacfaab940318fe19909e087aae81.zip
frameworks_base-842a1f4b0beaacfaab940318fe19909e087aae81.tar.gz
frameworks_base-842a1f4b0beaacfaab940318fe19909e087aae81.tar.bz2
Merge change 1912 into donut
* changes: Hand merge from cupcake_dcm to donut, part 2.
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/content/AbstractSyncableContentProvider.java170
-rw-r--r--core/java/android/provider/Contacts.java19
-rw-r--r--core/java/android/syncml/pim/PropertyNode.java194
-rw-r--r--core/java/android/syncml/pim/VBuilderCollection.java100
-rw-r--r--core/java/android/syncml/pim/VDataBuilder.java293
-rw-r--r--core/java/android/syncml/pim/VParser.java17
-rw-r--r--core/java/android/syncml/pim/vcard/ContactStruct.java925
-rw-r--r--core/java/android/syncml/pim/vcard/VCardComposer.java35
-rw-r--r--core/java/android/syncml/pim/vcard/VCardDataBuilder.java442
-rw-r--r--core/java/android/syncml/pim/vcard/VCardEntryCounter.java63
-rw-r--r--core/java/android/syncml/pim/vcard/VCardNestedException.java27
-rw-r--r--core/java/android/syncml/pim/vcard/VCardParser_V21.java638
-rw-r--r--core/java/android/syncml/pim/vcard/VCardParser_V30.java111
-rw-r--r--core/java/android/syncml/pim/vcard/VCardSourceDetector.java140
-rw-r--r--core/java/android/util/CharsetUtils.java13
-rw-r--r--core/java/android/widget/ArrayAdapter.java7
16 files changed, 2743 insertions, 451 deletions
diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java
index ce6501c..249d9ba 100644
--- a/core/java/android/content/AbstractSyncableContentProvider.java
+++ b/core/java/android/content/AbstractSyncableContentProvider.java
@@ -147,7 +147,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro
@Override
public boolean onCreate() {
if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
- mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName);
+ mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(),
+ mDatabaseName);
mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
AccountMonitorListener listener = new AccountMonitorListener() {
@@ -235,76 +236,147 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro
return Collections.emptyList();
}
- @Override
- public final int update(final Uri url, final ContentValues values,
- final String selection, final String[] selectionArgs) {
+ /**
+ * <p>
+ * Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction().
+ * {@link #endTransaction} MUST be called after calling this method.
+ * Those methods should be used like this:
+ * </p>
+ *
+ * <pre class="prettyprint">
+ * boolean successful = false;
+ * beginTransaction();
+ * try {
+ * // Do something related to mDb
+ * successful = true;
+ * return ret;
+ * } finally {
+ * endTransaction(successful);
+ * }
+ * </pre>
+ *
+ * @hide This method is dangerous from the view of database manipulation, though using
+ * this makes batch insertion/update/delete much faster.
+ */
+ public final void beginTransaction() {
mDb = mOpenHelper.getWritableDatabase();
mDb.beginTransaction();
+ }
+
+ /**
+ * <p>
+ * Call mDb.endTransaction(). If successful is true, try to call
+ * mDb.setTransactionSuccessful() before calling mDb.endTransaction().
+ * This method MUST be used with {@link #beginTransaction()}.
+ * </p>
+ *
+ * @hide This method is dangerous from the view of database manipulation, though using
+ * this makes batch insertion/update/delete much faster.
+ */
+ public final void endTransaction(boolean successful) {
try {
- if (isTemporary() && mSyncState.matches(url)) {
- int numRows = mSyncState.asContentProvider().update(
- url, values, selection, selectionArgs);
+ if (successful) {
+ // setTransactionSuccessful() must be called just once during opening the
+ // transaction.
mDb.setTransactionSuccessful();
- return numRows;
}
+ } finally {
+ mDb.endTransaction();
+ }
+ }
- int result = updateInternal(url, values, selection, selectionArgs);
- mDb.setTransactionSuccessful();
+ @Override
+ public final int update(final Uri uri, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+ boolean successful = false;
+ beginTransaction();
+ try {
+ int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs);
+ successful = true;
+ return ret;
+ } finally {
+ endTransaction(successful);
+ }
+ }
- if (!isTemporary() && result > 0) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
+ /**
+ * @hide
+ */
+ public final int nonTransactionalUpdate(final Uri uri, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+ if (isTemporary() && mSyncState.matches(uri)) {
+ int numRows = mSyncState.asContentProvider().update(
+ uri, values, selection, selectionArgs);
+ return numRows;
+ }
- return result;
- } finally {
- mDb.endTransaction();
+ int result = updateInternal(uri, values, selection, selectionArgs);
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
}
+
+ return result;
}
@Override
- public final int delete(final Uri url, final String selection,
+ public final int delete(final Uri uri, final String selection,
final String[] selectionArgs) {
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
+ boolean successful = false;
+ beginTransaction();
try {
- if (isTemporary() && mSyncState.matches(url)) {
- int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
- mDb.setTransactionSuccessful();
- return numRows;
- }
- int result = deleteInternal(url, selection, selectionArgs);
- mDb.setTransactionSuccessful();
- if (!isTemporary() && result > 0) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
- return result;
+ int ret = nonTransactionalDelete(uri, selection, selectionArgs);
+ successful = true;
+ return ret;
} finally {
- mDb.endTransaction();
+ endTransaction(successful);
}
}
+ /**
+ * @hide
+ */
+ public final int nonTransactionalDelete(final Uri uri, final String selection,
+ final String[] selectionArgs) {
+ if (isTemporary() && mSyncState.matches(uri)) {
+ int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs);
+ return numRows;
+ }
+ int result = deleteInternal(uri, selection, selectionArgs);
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
+ }
+ return result;
+ }
+
@Override
- public final Uri insert(final Uri url, final ContentValues values) {
- mDb = mOpenHelper.getWritableDatabase();
- mDb.beginTransaction();
+ public final Uri insert(final Uri uri, final ContentValues values) {
+ boolean successful = false;
+ beginTransaction();
try {
- if (isTemporary() && mSyncState.matches(url)) {
- Uri result = mSyncState.asContentProvider().insert(url, values);
- mDb.setTransactionSuccessful();
- return result;
- }
- Uri result = insertInternal(url, values);
- mDb.setTransactionSuccessful();
- if (!isTemporary() && result != null) {
- getContext().getContentResolver().notifyChange(url, null /* observer */,
- changeRequiresLocalSync(url));
- }
- return result;
+ Uri ret = nonTransactionalInsert(uri, values);
+ successful = true;
+ return ret;
} finally {
- mDb.endTransaction();
+ endTransaction(successful);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) {
+ if (isTemporary() && mSyncState.matches(uri)) {
+ Uri result = mSyncState.asContentProvider().insert(uri, values);
+ return result;
+ }
+ Uri result = insertInternal(uri, values);
+ if (!isTemporary() && result != null) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
}
+ return result;
}
@Override
diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java
index 3141f1a..8d6ebdd 100644
--- a/core/java/android/provider/Contacts.java
+++ b/core/java/android/provider/Contacts.java
@@ -869,6 +869,17 @@ public class Contacts {
public static final int TYPE_OTHER = 3;
/**
+ * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future.
+ */
+ public static final int MOBILE_EMAIL_TYPE_INDEX = 2;
+
+ /**
+ * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future.
+ * This is not "mobile" but "CELL" since vCard uses it for identifying mobile phone.
+ */
+ public static final String MOBILE_EMAIL_TYPE_NAME = "_AUTO_CELL";
+
+ /**
* The user defined label for the the contact method.
* <P>Type: TEXT</P>
*/
@@ -1005,7 +1016,13 @@ public class Contacts {
}
} else {
if (!TextUtils.isEmpty(label)) {
- display = label;
+ if (label.toString().equals(MOBILE_EMAIL_TYPE_NAME)) {
+ display =
+ context.getString(
+ com.android.internal.R.string.mobileEmailTypeName);
+ } else {
+ display = label;
+ }
}
}
break;
diff --git a/core/java/android/syncml/pim/PropertyNode.java b/core/java/android/syncml/pim/PropertyNode.java
index cc52499..983ecb8 100644
--- a/core/java/android/syncml/pim/PropertyNode.java
+++ b/core/java/android/syncml/pim/PropertyNode.java
@@ -17,12 +17,16 @@
package android.syncml.pim;
import android.content.ContentValues;
-import android.util.Log;
+import org.apache.commons.codec.binary.Base64;
+
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
public class PropertyNode {
@@ -52,7 +56,9 @@ public class PropertyNode {
public Set<String> propGroupSet;
public PropertyNode() {
+ propName = "";
propValue = "";
+ propValue_vector = new ArrayList<String>();
paramMap = new ContentValues();
paramMap_TYPE = new HashSet<String>();
propGroupSet = new HashSet<String>();
@@ -62,13 +68,21 @@ public class PropertyNode {
String propName, String propValue, List<String> propValue_vector,
byte[] propValue_bytes, ContentValues paramMap, Set<String> paramMap_TYPE,
Set<String> propGroupSet) {
- this.propName = propName;
+ if (propName != null) {
+ this.propName = propName;
+ } else {
+ this.propName = "";
+ }
if (propValue != null) {
this.propValue = propValue;
} else {
this.propValue = "";
}
- this.propValue_vector = propValue_vector;
+ if (propValue_vector != null) {
+ this.propValue_vector = propValue_vector;
+ } else {
+ this.propValue_vector = new ArrayList<String>();
+ }
this.propValue_bytes = propValue_bytes;
if (paramMap != null) {
this.paramMap = paramMap;
@@ -117,17 +131,9 @@ public class PropertyNode {
// decoded by BASE64 or QUOTED-PRINTABLE. When the size of propValue_vector
// is 1, the encoded value is stored in propValue, so we do not have to
// check it.
- if (propValue_vector != null) {
- // Log.d("@@@", "===" + propValue_vector + ", " + node.propValue_vector);
- return (propValue_vector.equals(node.propValue_vector) ||
- (propValue_vector.size() == 1));
- } else if (node.propValue_vector != null) {
- // Log.d("@@@", "===" + propValue_vector + ", " + node.propValue_vector);
- return (node.propValue_vector.equals(propValue_vector) ||
- (node.propValue_vector.size() == 1));
- } else {
- return true;
- }
+ return (propValue_vector.equals(node.propValue_vector) ||
+ propValue_vector.size() == 1 ||
+ node.propValue_vector.size() == 1);
}
}
@@ -154,4 +160,164 @@ public class PropertyNode {
builder.append(propValue);
return builder.toString();
}
+
+ /**
+ * Encode this object into a string which can be decoded.
+ */
+ public String encode() {
+ // 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("],");
+ }
+ int size = propGroupSet.size();
+ if (size > 0) {
+ Set<String> set = propGroupSet;
+ builder.append("propGroup:[");
+ int i = 0;
+ for (String group : set) {
+ // We do not need to double quote groups.
+ // group = 1*(ALPHA / DIGIT / "-")
+ builder.append(group);
+ if (i < size - 1) {
+ builder.append(",");
+ }
+ i++;
+ }
+ builder.append("],");
+ }
+
+ if (paramMap.size() > 0 || paramMap_TYPE.size() > 0) {
+ ContentValues values = paramMap;
+ builder.append("paramMap:[");
+ size = paramMap.size();
+ int i = 0;
+ for (Entry<String, Object> entry : values.valueSet()) {
+ // Assuming param-key does not contain NON-ASCII nor symbols.
+ //
+ // According to vCard 3.0:
+ // param-name = iana-token / x-name
+ builder.append(entry.getKey());
+
+ // param-value may contain any value including NON-ASCIIs.
+ // We use the following replacing rule.
+ // \ -> \\
+ // , -> \,
+ // In String#replaceAll(), "\\\\" means a single backslash.
+ builder.append("=");
+ builder.append(entry.getValue().toString()
+ .replaceAll("\\\\", "\\\\\\\\")
+ .replaceAll(",", "\\\\,"));
+ if (i < size -1) {
+ builder.append(",");
+ }
+ i++;
+ }
+
+ Set<String> set = paramMap_TYPE;
+ size = paramMap_TYPE.size();
+ if (i > 0 && size > 0) {
+ builder.append(",");
+ }
+ i = 0;
+ for (String type : set) {
+ builder.append("TYPE=");
+ builder.append(type
+ .replaceAll("\\\\", "\\\\\\\\")
+ .replaceAll(",", "\\\\,"));
+ if (i < size - 1) {
+ builder.append(",");
+ }
+ i++;
+ }
+ builder.append("],");
+ }
+
+ size = propValue_vector.size();
+ if (size > 0) {
+ builder.append("propValue:[");
+ List<String> list = propValue_vector;
+ for (int i = 0; i < size; i++) {
+ builder.append(list.get(i)
+ .replaceAll("\\\\", "\\\\\\\\")
+ .replaceAll(",", "\\\\,"));
+ if (i < size -1) {
+ builder.append(",");
+ }
+ }
+ builder.append("],");
+ }
+
+ return builder.toString();
+ }
+
+ public static PropertyNode decode(String encodedString) {
+ PropertyNode propertyNode = new PropertyNode();
+ String trimed = encodedString.trim();
+ if (trimed.length() == 0) {
+ return propertyNode;
+ }
+ String[] elems = trimed.split("],");
+
+ for (String elem : elems) {
+ int index = elem.indexOf('[');
+ String name = elem.substring(0, index - 1);
+ Pattern pattern = Pattern.compile("(?<!\\\\),");
+ String[] values = pattern.split(elem.substring(index + 1), -1);
+ if (name.equals("propName")) {
+ propertyNode.propName = values[0];
+ } else if (name.equals("propGroupSet")) {
+ for (String value : values) {
+ propertyNode.propGroupSet.add(value);
+ }
+ } else if (name.equals("paramMap")) {
+ ContentValues paramMap = propertyNode.paramMap;
+ Set<String> paramMap_TYPE = propertyNode.paramMap_TYPE;
+ for (String value : values) {
+ String[] tmp = value.split("=", 2);
+ String mapKey = tmp[0];
+ // \, -> ,
+ // \\ -> \
+ // In String#replaceAll(), "\\\\" means a single backslash.
+ String mapValue =
+ tmp[1].replaceAll("\\\\,", ",").replaceAll("\\\\\\\\", "\\\\");
+ if (mapKey.equalsIgnoreCase("TYPE")) {
+ paramMap_TYPE.add(mapValue);
+ } else {
+ paramMap.put(mapKey, mapValue);
+ }
+ }
+ } else if (name.equals("propValue")) {
+ StringBuilder builder = new StringBuilder();
+ List<String> list = propertyNode.propValue_vector;
+ int length = values.length;
+ for (int i = 0; i < length; i++) {
+ String normValue = values[i]
+ .replaceAll("\\\\,", ",")
+ .replaceAll("\\\\\\\\", "\\\\");
+ list.add(normValue);
+ builder.append(normValue);
+ if (i < length - 1) {
+ builder.append(";");
+ }
+ }
+ propertyNode.propValue = builder.toString();
+ }
+ }
+
+ // At this time, QUOTED-PRINTABLE is already decoded to Java String.
+ // We just need to decode BASE64 String to binary.
+ String encoding = propertyNode.paramMap.getAsString("ENCODING");
+ if (encoding != null &&
+ (encoding.equalsIgnoreCase("BASE64") ||
+ encoding.equalsIgnoreCase("B"))) {
+ propertyNode.propValue_bytes =
+ Base64.decodeBase64(propertyNode.propValue_vector.get(0).getBytes());
+ }
+
+ return propertyNode;
+ }
}
diff --git a/core/java/android/syncml/pim/VBuilderCollection.java b/core/java/android/syncml/pim/VBuilderCollection.java
new file mode 100644
index 0000000..f09c1c4
--- /dev/null
+++ b/core/java/android/syncml/pim/VBuilderCollection.java
@@ -0,0 +1,100 @@
+/*
+ * 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.syncml.pim;
+
+import java.util.Collection;
+import java.util.List;
+
+public class VBuilderCollection implements VBuilder {
+
+ private final Collection<VBuilder> mVBuilderCollection;
+
+ public VBuilderCollection(Collection<VBuilder> vBuilderCollection) {
+ mVBuilderCollection = vBuilderCollection;
+ }
+
+ public Collection<VBuilder> getVBuilderCollection() {
+ return mVBuilderCollection;
+ }
+
+ public void start() {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.start();
+ }
+ }
+
+ public void end() {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.end();
+ }
+ }
+
+ public void startRecord(String type) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.startRecord(type);
+ }
+ }
+
+ public void endRecord() {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.endRecord();
+ }
+ }
+
+ public void startProperty() {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.startProperty();
+ }
+ }
+
+
+ public void endProperty() {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.endProperty();
+ }
+ }
+
+ public void propertyGroup(String group) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.propertyGroup(group);
+ }
+ }
+
+ public void propertyName(String name) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.propertyName(name);
+ }
+ }
+
+ public void propertyParamType(String type) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.propertyParamType(type);
+ }
+ }
+
+ public void propertyParamValue(String value) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.propertyParamValue(value);
+ }
+ }
+
+ public void propertyValues(List<String> values) {
+ for (VBuilder builder : mVBuilderCollection) {
+ builder.propertyValues(values);
+ }
+ }
+}
diff --git a/core/java/android/syncml/pim/VDataBuilder.java b/core/java/android/syncml/pim/VDataBuilder.java
index 8c67cf5..f6e5b65 100644
--- a/core/java/android/syncml/pim/VDataBuilder.java
+++ b/core/java/android/syncml/pim/VDataBuilder.java
@@ -17,8 +17,10 @@
package android.syncml.pim;
import android.content.ContentValues;
+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;
@@ -26,9 +28,7 @@ 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;
-import java.util.Vector;
/**
* Store the parse result to custom datastruct: VNode, PropertyNode
@@ -38,7 +38,13 @@ import java.util.Vector;
*/
public class VDataBuilder implements VBuilder {
static private String LOG_TAG = "VDATABuilder";
-
+
+ /**
+ * If there's no other information available, this class uses this charset for encoding
+ * byte arrays.
+ */
+ static public String DEFAULT_CHARSET = "UTF-8";
+
/** type=VNode */
public List<VNode> vNodeList = new ArrayList<VNode>();
private int mNodeListPos = 0;
@@ -47,34 +53,74 @@ public class VDataBuilder implements VBuilder {
private String mCurrentParamType;
/**
- * Assumes that each String can be encoded into byte array using this encoding.
+ * The charset using which VParser parses the text.
+ */
+ private String mSourceCharset;
+
+ /**
+ * The charset with which byte array is encoded to String.
*/
- private String mCharset;
+ private String mTargetCharset;
private boolean mStrictLineBreakParsing;
public VDataBuilder() {
- mCharset = "ISO-8859-1";
- mStrictLineBreakParsing = false;
+ this(VParser.DEFAULT_CHARSET, DEFAULT_CHARSET, false);
}
- public VDataBuilder(String encoding, boolean strictLineBreakParsing) {
- mCharset = encoding;
- mStrictLineBreakParsing = strictLineBreakParsing;
+ public VDataBuilder(String charset, boolean strictLineBreakParsing) {
+ this(null, charset, strictLineBreakParsing);
}
+ /**
+ * @hide sourceCharset is temporal.
+ */
+ public VDataBuilder(String sourceCharset, String targetCharset,
+ boolean strictLineBreakParsing) {
+ if (sourceCharset != null) {
+ mSourceCharset = sourceCharset;
+ } else {
+ mSourceCharset = VParser.DEFAULT_CHARSET;
+ }
+ if (targetCharset != null) {
+ mTargetCharset = targetCharset;
+ } else {
+ mTargetCharset = DEFAULT_CHARSET;
+ }
+ mStrictLineBreakParsing = strictLineBreakParsing;
+ }
+
public void start() {
}
public void end() {
}
+ // Note: I guess that this code assumes the Record may nest like this:
+ // START:VPOS
+ // ...
+ // START:VPOS2
+ // ...
+ // END:VPOS2
+ // ...
+ // END:VPOS
+ //
+ // However the following code has a bug.
+ // When error occurs after calling startRecord(), the entry which is probably
+ // the cause of the error remains to be in vNodeList, while endRecord() is not called.
+ //
+ // I leave this code as is since I'm not familiar with vcalendar specification.
+ // But I believe we should refactor this code in the future.
+ // Until this, the last entry has to be removed when some error occurs.
public void startRecord(String type) {
+
VNode vnode = new VNode();
vnode.parseStatus = 1;
vnode.VName = type;
+ // I feel this should be done in endRecord(), but it cannot be done because of
+ // the reason above.
vNodeList.add(vnode);
- mNodeListPos = vNodeList.size()-1;
+ mNodeListPos = vNodeList.size() - 1;
mCurrentVNode = vNodeList.get(mNodeListPos);
}
@@ -90,15 +136,14 @@ public class VDataBuilder implements VBuilder {
}
public void startProperty() {
- // System.out.println("+ startProperty. ");
+ mCurrentPropNode = new PropertyNode();
}
public void endProperty() {
- // System.out.println("- endProperty. ");
+ mCurrentVNode.propList.add(mCurrentPropNode);
}
public void propertyName(String name) {
- mCurrentPropNode = new PropertyNode();
mCurrentPropNode.propName = name;
}
@@ -122,139 +167,145 @@ public class VDataBuilder implements VBuilder {
mCurrentParamType = null;
}
- private String encodeString(String originalString, String targetEncoding) {
- Charset charset = Charset.forName(mCharset);
+ 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, targetEncoding);
+ return new String(bytes, targetCharset);
} catch (UnsupportedEncodingException e) {
- return null;
+ Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
+ return new String(bytes);
}
}
- public void propertyValues(List<String> values) {
- ContentValues paramMap = mCurrentPropNode.paramMap;
-
- String charsetString = paramMap.getAsString("CHARSET");
-
- boolean setupParamValues = false;
- //decode value string to propValue_bytes
- if (paramMap.containsKey("ENCODING")) {
- String encoding = paramMap.getAsString("ENCODING");
- if (encoding.equalsIgnoreCase("BASE64") ||
- encoding.equalsIgnoreCase("B")) {
- if (values.size() > 1) {
- Log.e(LOG_TAG,
- ("BASE64 encoding is used while " +
- "there are multiple values (" + values.size()));
- }
+ private String handleOneValue(String value, String targetCharset, String encoding) {
+ if (encoding != null) {
+ if (encoding.equals("BASE64") || encoding.equals("B")) {
+ // Assume BASE64 is used only when the number of values is 1.
mCurrentPropNode.propValue_bytes =
- Base64.decodeBase64(values.get(0).
- replaceAll(" ","").replaceAll("\t","").
- replaceAll("\r\n","").
- getBytes());
- }
-
- if(encoding.equalsIgnoreCase("QUOTED-PRINTABLE")){
- // if CHARSET is defined, we translate each String into the Charset.
- List<String> tmpValues = new ArrayList<String>();
- Vector<byte[]> byteVector = new Vector<byte[]>();
- int size = 0;
- try{
- for (String value : values) {
- String quotedPrintable = value
- .replaceAll("= ", " ").replaceAll("=\t", "\t");
- String[] lines;
- if (mStrictLineBreakParsing) {
- lines = quotedPrintable.split("\r\n");
- } else {
- lines = quotedPrintable
- .replace("\r\n", "\n").replace("\r", "\n").split("\n");
- }
- StringBuilder builder = new StringBuilder();
- for (String line : lines) {
- if (line.endsWith("=")) {
- line = line.substring(0, line.length() - 1);
- }
- builder.append(line);
- }
- byte[] bytes = QuotedPrintableCodec.decodeQuotedPrintable(
- builder.toString().getBytes());
- if (charsetString != null) {
- try {
- tmpValues.add(new String(bytes, charsetString));
- } catch (UnsupportedEncodingException e) {
- Log.e(LOG_TAG, "Failed to encode: charset=" + charsetString);
- tmpValues.add(new String(bytes));
+ Base64.decodeBase64(value.getBytes());
+ return value;
+ } else if (encoding.equals("QUOTED-PRINTABLE")) {
+ String quotedPrintable = value
+ .replaceAll("= ", " ").replaceAll("=\t", "\t");
+ String[] lines;
+ if (mStrictLineBreakParsing) {
+ lines = quotedPrintable.split("\r\n");
+ } else {
+ StringBuilder builder = new StringBuilder();
+ int 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 {
- tmpValues.add(new String(bytes));
- }
- byteVector.add(bytes);
- size += bytes.length;
- } // for (String value : values) {
- mCurrentPropNode.propValue_vector = tmpValues;
- mCurrentPropNode.propValue = listToString(tmpValues);
-
- mCurrentPropNode.propValue_bytes = new byte[size];
-
- {
- byte[] tmpBytes = mCurrentPropNode.propValue_bytes;
- int index = 0;
- for (byte[] bytes : byteVector) {
- int length = bytes.length;
- for (int i = 0; i < length; i++, index++) {
- tmpBytes[index] = bytes[i];
- }
+ builder.append(ch);
}
}
- setupParamValues = true;
- } catch(Exception e) {
- Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e);
+ String finalLine = builder.toString();
+ if (finalLine.length() > 0) {
+ list.add(finalLine);
+ }
+ lines = list.toArray(new String[0]);
}
- } // QUOTED-PRINTABLE
- } // ENCODING
-
- if (!setupParamValues) {
- // if CHARSET is defined, we translate each String into the Charset.
- if (charsetString != null) {
- List<String> tmpValues = new ArrayList<String>();
- for (String value : values) {
- String result = encodeString(value, charsetString);
- if (result != null) {
- tmpValues.add(result);
- } else {
- Log.e(LOG_TAG, "Failed to encode: charset=" + charsetString);
- tmpValues.add(value);
+ StringBuilder 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);
}
- values = tmpValues;
}
-
- mCurrentPropNode.propValue_vector = values;
- mCurrentPropNode.propValue = listToString(values);
+ // Unknown encoding. Fall back to default.
}
- mCurrentVNode.propList.add(mCurrentPropNode);
+ return encodeString(value, targetCharset);
}
-
- private String listToString(Collection<String> list){
- StringBuilder typeListB = new StringBuilder();
- for (String type : list) {
- typeListB.append(type).append(";");
+
+ public void propertyValues(List<String> values) {
+ if (values == null || values.size() == 0) {
+ mCurrentPropNode.propValue_bytes = null;
+ mCurrentPropNode.propValue_vector.clear();
+ mCurrentPropNode.propValue_vector.add("");
+ mCurrentPropNode.propValue = "";
+ return;
+ }
+
+ ContentValues paramMap = mCurrentPropNode.paramMap;
+
+ String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET"));
+ String encoding = paramMap.getAsString("ENCODING");
+
+ if (targetCharset == null || targetCharset.length() == 0) {
+ targetCharset = mTargetCharset;
+ }
+
+ for (String value : values) {
+ mCurrentPropNode.propValue_vector.add(
+ handleOneValue(value, targetCharset, encoding));
}
- int len = typeListB.length();
- if (len > 0 && typeListB.charAt(len - 1) == ';') {
- return typeListB.substring(0, len - 1);
+
+ mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector);
+ }
+
+ private String listToString(List<String> list){
+ int size = list.size();
+ if (size > 1) {
+ StringBuilder typeListB = new StringBuilder();
+ for (String type : list) {
+ typeListB.append(type).append(";");
+ }
+ int len = typeListB.length();
+ if (len > 0 && typeListB.charAt(len - 1) == ';') {
+ return typeListB.substring(0, len - 1);
+ }
+ return typeListB.toString();
+ } else if (size == 1) {
+ return list.get(0);
+ } else {
+ return "";
}
- return typeListB.toString();
}
public String getResult(){
return null;
}
}
-
diff --git a/core/java/android/syncml/pim/VParser.java b/core/java/android/syncml/pim/VParser.java
index df93f38..57c5f7a 100644
--- a/core/java/android/syncml/pim/VParser.java
+++ b/core/java/android/syncml/pim/VParser.java
@@ -26,6 +26,9 @@ import java.io.UnsupportedEncodingException;
*
*/
abstract public class VParser {
+ // Assume 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 String DEFAULT_CHARSET = "iso-8859-1";
/**
* The buffer used to store input stream
@@ -96,6 +99,20 @@ abstract public class VParser {
}
/**
+ * Parse the given stream with the default encoding.
+ *
+ * @param is
+ * The source to parse.
+ * @param builder
+ * The v builder which used to construct data.
+ * @return Return true for success, otherwise false.
+ * @throws IOException
+ */
+ public boolean parse(InputStream is, VBuilder builder) throws IOException {
+ return parse(is, DEFAULT_CHARSET, builder);
+ }
+
+ /**
* Copy the content of input stream and filter the "folding"
*/
protected void setInputStream(InputStream is, String encoding)
diff --git a/core/java/android/syncml/pim/vcard/ContactStruct.java b/core/java/android/syncml/pim/vcard/ContactStruct.java
index 8d9b7fa..afeb5cd 100644
--- a/core/java/android/syncml/pim/vcard/ContactStruct.java
+++ b/core/java/android/syncml/pim/vcard/ContactStruct.java
@@ -16,45 +16,102 @@
package android.syncml.pim.vcard;
-import java.util.List;
+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.syncml.pim.PropertyNode;
+import android.syncml.pim.VNode;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
/**
- * The parameter class of VCardCreator.
+ * The parameter class of VCardComposer.
* This class standy by the person-contact in
* Android system, we must use this class instance as parameter to transmit to
- * VCardCreator so that create vCard string.
+ * VCardComposer so that create vCard string.
*/
// TODO: rename the class name, next step
public class ContactStruct {
- public String company;
+ private static final String LOG_TAG = "ContactStruct";
+
+ // 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;
+
/** MUST exist */
public String name;
+ public String phoneticName;
/** maybe folding */
- public String notes;
+ public List<String> notes = new ArrayList<String>();
/** maybe folding */
public String title;
/** binary bytes of pic. */
public byte[] photoBytes;
- /** mime_type col of images table */
+ /** The type of Photo (e.g. JPEG, BMP, etc.) */
public String photoType;
/** Only for GET. Use addPhoneList() to PUT. */
public List<PhoneData> phoneList;
/** Only for GET. Use addContactmethodList() to PUT. */
public List<ContactMethod> contactmethodList;
+ /** Only for GET. Use addOrgList() to PUT. */
+ public List<OrganizationData> organizationList;
+ /** Only for GET. Use addExtension() to PUT */
+ public Map<String, List<String>> extensionMap;
- public static class PhoneData{
+ // Use organizationList instead when handling ORG.
+ @Deprecated
+ public String company;
+
+ public static class PhoneData {
+ public int type;
/** maybe folding */
public String data;
- public String type;
public String label;
+ public boolean isPrimary;
}
- public static class ContactMethod{
- public String kind;
- public String type;
+ public static class ContactMethod {
+ // Contacts.KIND_EMAIL, Contacts.KIND_POSTAL
+ public int kind;
+ // e.g. Contacts.ContactMethods.TYPE_HOME, Contacts.PhoneColumns.TYPE_HOME
+ // If type == Contacts.PhoneColumns.TYPE_CUSTOM, label is used.
+ public int type;
public String data;
+ // Used only when TYPE is TYPE_CUSTOM.
public String label;
+ public boolean isPrimary;
+ }
+
+ public static class OrganizationData {
+ public int type;
+ public String companyName;
+ public String positionName;
+ public boolean isPrimary;
}
/**
@@ -63,29 +120,841 @@ public class ContactStruct {
* @param type type col of content://contacts/phones
* @param label lable col of content://contacts/phones
*/
- public void addPhone(String data, String type, String label){
- if(phoneList == null)
+ public void addPhone(int type, String data, String label, boolean isPrimary){
+ if (phoneList == null) {
phoneList = new ArrayList<PhoneData>();
- PhoneData st = new PhoneData();
- st.data = data;
- st.type = type;
- st.label = label;
- phoneList.add(st);
+ }
+ PhoneData phoneData = new PhoneData();
+ phoneData.type = type;
+
+ 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.data = PhoneNumberUtils.formatNumber(builder.toString());
+ phoneData.label = label;
+ phoneData.isPrimary = isPrimary;
+ phoneList.add(phoneData);
}
+
/**
* Add a contactmethod info to contactmethodList.
- * @param data contact data
+ * @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.
*/
- public void addContactmethod(String kind, String data, String type,
- String label){
- if(contactmethodList == null)
+ public void addContactmethod(int kind, int type, String data,
+ String label, boolean isPrimary){
+ if (contactmethodList == null) {
contactmethodList = new ArrayList<ContactMethod>();
- ContactMethod st = new ContactMethod();
- st.kind = kind;
- st.data = data;
- st.type = type;
- st.label = label;
- contactmethodList.add(st);
+ }
+ ContactMethod contactMethod = new ContactMethod();
+ contactMethod.kind = kind;
+ contactMethod.type = type;
+ contactMethod.data = data;
+ contactMethod.label = label;
+ contactMethod.isPrimary = isPrimary;
+ contactmethodList.add(contactMethod);
+ }
+
+ /**
+ * Add a Organization info to organizationList.
+ */
+ public void addOrganization(int type, String companyName, String positionName,
+ boolean isPrimary) {
+ if (organizationList == null) {
+ organizationList = new ArrayList<OrganizationData>();
+ }
+ OrganizationData organizationData = new OrganizationData();
+ organizationData.type = type;
+ organizationData.companyName = companyName;
+ organizationData.positionName = positionName;
+ organizationData.isPrimary = isPrimary;
+ organizationList.add(organizationData);
+ }
+
+ public void addExtension(PropertyNode propertyNode) {
+ if (propertyNode.propValue.length() == 0) {
+ return;
+ }
+ // Now store the string into extensionMap.
+ List<String> list;
+ String name = propertyNode.propName;
+ if (extensionMap == null) {
+ extensionMap = new HashMap<String, List<String>>();
+ }
+ if (!extensionMap.containsKey(name)){
+ list = new ArrayList<String>();
+ extensionMap.put(name, list);
+ } else {
+ list = extensionMap.get(name);
+ }
+
+ list.add(propertyNode.encode());
+ }
+
+ 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 == 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 static ContactStruct constructContactFromVNode(VNode node,
+ int nameOrderType) {
+ if (!node.VName.equals("VCARD")) {
+ // Impossible in current implementation. Just for safety.
+ Log.e(LOG_TAG, "Non VCARD data is inserted.");
+ return null;
+ }
+
+ // 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.
+ String fullName = null;
+ String nameFromNProperty = null;
+
+ // Some vCard has "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", and
+ // "X-PHONETIC-LAST-NAME"
+ String xPhoneticFirstName = null;
+ String xPhoneticMiddleName = null;
+ String xPhoneticLastName = null;
+
+ ContactStruct contact = new ContactStruct();
+
+ // 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.
+ boolean prefIsSetAddress = false;
+ boolean prefIsSetPhone = false;
+ boolean prefIsSetEmail = false;
+ boolean prefIsSetOrganization = false;
+
+ for (PropertyNode propertyNode: node.propList) {
+ String name = propertyNode.propName;
+
+ if (TextUtils.isEmpty(propertyNode.propValue)) {
+ continue;
+ }
+
+ if (name.equals("VERSION")) {
+ // vCard version. Ignore this.
+ } else if (name.equals("FN")) {
+ fullName = propertyNode.propValue;
+ } else if (name.equals("NAME") && fullName == null) {
+ // Only in vCard 3.0. Use this if FN does not exist.
+ // Though, note that vCard 3.0 requires FN.
+ fullName = propertyNode.propValue;
+ } else if (name.equals("N")) {
+ nameFromNProperty = getNameFromNProperty(propertyNode.propValue_vector,
+ nameOrderType);
+ } else if (name.equals("SORT-STRING")) {
+ contact.phoneticName = propertyNode.propValue;
+ } else if (name.equals("SOUND")) {
+ if (propertyNode.paramMap_TYPE.contains("X-IRMC-N") &&
+ contact.phoneticName == 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 ';' in it.
+ // We remove them.
+ StringBuilder builder = new StringBuilder();
+ String value = propertyNode.propValue;
+ int length = value.length();
+ for (int i = 0; i < length; i++) {
+ char ch = value.charAt(i);
+ if (ch != ';') {
+ builder.append(ch);
+ }
+ }
+ contact.phoneticName = builder.toString();
+ } else {
+ contact.addExtension(propertyNode);
+ }
+ } else if (name.equals("ADR")) {
+ List<String> values = propertyNode.propValue_vector;
+ boolean valuesAreAllEmpty = true;
+ for (String value : values) {
+ if (value.length() > 0) {
+ valuesAreAllEmpty = false;
+ break;
+ }
+ }
+ if (valuesAreAllEmpty) {
+ continue;
+ }
+
+ int kind = Contacts.KIND_POSTAL;
+ int type = -1;
+ String label = "";
+ boolean isPrimary = false;
+ for (String typeString : propertyNode.paramMap_TYPE) {
+ if (typeString.equals("PREF") && !prefIsSetAddress) {
+ // Only first "PREF" is considered.
+ prefIsSetAddress = 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;
+ List<String> list = propertyNode.propValue_vector;
+ int size = list.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 = list.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 = list.get(i);
+ if (addressPart.length() > 0) {
+ if (!builderIsEmpty) {
+ builder.append(' ');
+ }
+ builder.append(addressPart);
+ builderIsEmpty = false;
+ }
+ }
+ }
+ address = builder.toString().trim();
+ } else {
+ address = propertyNode.propValue;
+ }
+ contact.addContactmethod(kind, type, address, label, isPrimary);
+ } else if (name.equals("ORG")) {
+ // vCard specification does not specify other types.
+ int type = Contacts.OrganizationColumns.TYPE_WORK;
+ String companyName = "";
+ String positionName = "";
+ boolean isPrimary = false;
+
+ for (String typeString : propertyNode.paramMap_TYPE) {
+ if (typeString.equals("PREF") && !prefIsSetOrganization) {
+ // vCard specification officially does not have PREF in ORG.
+ // This is just for safety.
+ prefIsSetOrganization = true;
+ isPrimary = true;
+ }
+ // XXX: Should we cope with X- words?
+ }
+
+ List<String> list = propertyNode.propValue_vector;
+ int size = list.size();
+ if (size > 1) {
+ companyName = list.get(0);
+ StringBuilder builder = new StringBuilder();
+ for (int i = 1; i < size; i++) {
+ builder.append(list.get(1));
+ if (i != size - 1) {
+ builder.append(", ");
+ }
+ }
+ positionName = builder.toString();
+ } else if (size == 1) {
+ companyName = propertyNode.propValue;
+ positionName = "";
+ }
+ contact.addOrganization(type, companyName, positionName, isPrimary);
+ } else if (name.equals("TITLE")) {
+ contact.title = propertyNode.propValue;
+ // XXX: What to do this? Isn't ORG enough?
+ contact.addExtension(propertyNode);
+ } else if (name.equals("ROLE")) {
+ // XXX: What to do this? Isn't ORG enough?
+ contact.addExtension(propertyNode);
+ } else if (name.equals("PHOTO")) {
+ // We prefer PHOTO to LOGO.
+ String valueType = propertyNode.paramMap.getAsString("VALUE");
+ if (valueType != null && valueType.equals("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)
+ contact.photoBytes = propertyNode.propValue_bytes;
+ String type = propertyNode.paramMap.getAsString("TYPE");
+ if (type != null) {
+ contact.photoType = type;
+ }
+ }
+ } else if (name.equals("LOGO")) {
+ // When PHOTO is not available this is not URL,
+ // we use this instead of PHOTO.
+ String valueType = propertyNode.paramMap.getAsString("VALUE");
+ if (valueType != null && valueType.equals("URL")) {
+ // TODO: do something.
+ } else if (contact.photoBytes == null) {
+ contact.photoBytes = propertyNode.propValue_bytes;
+ String type = propertyNode.paramMap.getAsString("TYPE");
+ if (type != null) {
+ contact.photoType = type;
+ }
+ }
+ } else if (name.equals("EMAIL")) {
+ int type = -1;
+ String label = null;
+ boolean isPrimary = false;
+ for (String typeString : propertyNode.paramMap_TYPE) {
+ if (typeString.equals("PREF") && !prefIsSetEmail) {
+ // Only first "PREF" is considered.
+ prefIsSetEmail = 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;
+ }
+ }
+ // We use "OTHER" as default.
+ if (type < 0) {
+ type = Contacts.ContactMethodsColumns.TYPE_OTHER;
+ }
+ contact.addContactmethod(Contacts.KIND_EMAIL,
+ type, propertyNode.propValue,label, isPrimary);
+ } else if (name.equals("TEL")) {
+ int type = -1;
+ String label = null;
+ boolean isPrimary = false;
+ boolean isFax = false;
+ for (String typeString : propertyNode.paramMap_TYPE) {
+ if (typeString.equals("PREF") && !prefIsSetPhone) {
+ // Only first "PREF" is considered.
+ prefIsSetPhone = 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;
+ }
+ }
+ // We use "HOME" as default
+ 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;
+ }
+ }
+
+ contact.addPhone(type, propertyNode.propValue, label, isPrimary);
+ } else if (name.equals("NOTE")) {
+ contact.notes.add(propertyNode.propValue);
+ } else if (name.equals("BDAY")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("URL")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("REV")) {
+ // Revision of this VCard entry. I think we can ignore this.
+ contact.addExtension(propertyNode);
+ } else if (name.equals("UID")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("KEY")) {
+ // Type is X509 or PGP? I don't know how to handle this...
+ contact.addExtension(propertyNode);
+ } else if (name.equals("MAILER")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("TZ")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("GEO")) {
+ contact.addExtension(propertyNode);
+ } else if (name.equals("NICKNAME")) {
+ // vCard 3.0 only.
+ contact.addExtension(propertyNode);
+ } else if (name.equals("CLASS")) {
+ // vCard 3.0 only.
+ // e.g. CLASS:CONFIDENTIAL
+ contact.addExtension(propertyNode);
+ } else if (name.equals("PROFILE")) {
+ // VCard 3.0 only. Must be "VCARD". I think we can ignore this.
+ contact.addExtension(propertyNode);
+ } else if (name.equals("CATEGORIES")) {
+ // VCard 3.0 only.
+ // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY
+ contact.addExtension(propertyNode);
+ } else if (name.equals("SOURCE")) {
+ // VCard 3.0 only.
+ contact.addExtension(propertyNode);
+ } else if (name.equals("PRODID")) {
+ // VCard 3.0 only.
+ // To specify the identifier for the product that created
+ // the vCard object.
+ contact.addExtension(propertyNode);
+ } else if (name.equals("X-PHONETIC-FIRST-NAME")) {
+ xPhoneticFirstName = propertyNode.propValue;
+ } else if (name.equals("X-PHONETIC-MIDDLE-NAME")) {
+ xPhoneticMiddleName = propertyNode.propValue;
+ } else if (name.equals("X-PHONETIC-LAST-NAME")) {
+ xPhoneticLastName = propertyNode.propValue;
+ } else {
+ // Unknown X- words and IANA token.
+ contact.addExtension(propertyNode);
+ }
+ }
+
+ if (fullName != null) {
+ contact.name = fullName;
+ } else if(nameFromNProperty != null) {
+ contact.name = nameFromNProperty;
+ } else {
+ contact.name = "";
+ }
+
+ if (contact.phoneticName == null &&
+ (xPhoneticFirstName != null || xPhoneticMiddleName != null ||
+ xPhoneticLastName != 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 (nameOrderType == NAME_ORDER_TYPE_JAPANESE) {
+ first = xPhoneticLastName;
+ second = xPhoneticFirstName;
+ } else {
+ first = xPhoneticFirstName;
+ second = xPhoneticLastName;
+ }
+ StringBuilder builder = new StringBuilder();
+ if (first != null) {
+ builder.append(first);
+ }
+ if (xPhoneticMiddleName != null) {
+ builder.append(xPhoneticMiddleName);
+ }
+ if (second != null) {
+ builder.append(second);
+ }
+ contact.phoneticName = 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 (contact.phoneticName != null) {
+ contact.phoneticName = contact.phoneticName.trim();
+ }
+
+ // If there is no "PREF", we choose the first entries as primary.
+ if (!prefIsSetPhone &&
+ contact.phoneList != null &&
+ contact.phoneList.size() > 0) {
+ contact.phoneList.get(0).isPrimary = true;
+ }
+
+ if (!prefIsSetAddress && contact.contactmethodList != null) {
+ for (ContactMethod contactMethod : contact.contactmethodList) {
+ if (contactMethod.kind == Contacts.KIND_POSTAL) {
+ contactMethod.isPrimary = true;
+ break;
+ }
+ }
+ }
+ if (!prefIsSetEmail && contact.contactmethodList != null) {
+ for (ContactMethod contactMethod : contact.contactmethodList) {
+ if (contactMethod.kind == Contacts.KIND_EMAIL) {
+ contactMethod.isPrimary = true;
+ break;
+ }
+ }
+ }
+ if (!prefIsSetOrganization &&
+ contact.organizationList != null &&
+ contact.organizationList.size() > 0) {
+ contact.organizationList.get(0).isPrimary = true;
+ }
+
+ return contact;
+ }
+
+ public String displayString() {
+ if (name.length() > 0) {
+ return name;
+ }
+ if (contactmethodList != null && contactmethodList.size() > 0) {
+ for (ContactMethod contactMethod : contactmethodList) {
+ if (contactMethod.kind == Contacts.KIND_EMAIL && contactMethod.isPrimary) {
+ return contactMethod.data;
+ }
+ }
+ }
+ if (phoneList != null && phoneList.size() > 0) {
+ for (PhoneData phoneData : phoneList) {
+ if (phoneData.isPrimary) {
+ return phoneData.data;
+ }
+ }
+ }
+ return "";
+ }
+
+ 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, name);
+ contentValues.put(People.PHONETIC_NAME, phoneticName);
+
+ if (notes.size() > 1) {
+ StringBuilder builder = new StringBuilder();
+ for (String note : notes) {
+ builder.append(note);
+ builder.append("\n");
+ }
+ contentValues.put(People.NOTES, builder.toString());
+ } else if (notes.size() == 1){
+ contentValues.put(People.NOTES, notes.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.nonTransactionalInsert(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.nonTransactionalInsert(
+ GroupMembership.CONTENT_URI, values);
+ if (resultUri == null) {
+ Log.e(LOG_TAG, "Faild to insert the person to MyContact.");
+ provider.nonTransactionalDelete(personUri, null, null);
+ personUri = null;
+ }
+ }
+ }
+
+ if (personUri == null) {
+ Log.e(LOG_TAG, "Failed to create the contact.");
+ return;
+ }
+
+ if (photoBytes != null) {
+ if (resolver != null) {
+ People.setPhotoData(resolver, personUri, photoBytes);
+ } else {
+ Uri photoUri = Uri.withAppendedPath(personUri, Contacts.Photos.CONTENT_DIRECTORY);
+ ContentValues values = new ContentValues();
+ values.put(Photos.DATA, photoBytes);
+ provider.update(photoUri, values, null, null);
+ }
+ }
+
+ long primaryPhoneId = -1;
+ if (phoneList != null && phoneList.size() > 0) {
+ for (PhoneData phoneData : phoneList) {
+ 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.nonTransactionalInsert(Phones.CONTENT_URI, values);
+ }
+ if (phoneData.isPrimary) {
+ primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment());
+ }
+ }
+ }
+
+ long primaryOrganizationId = -1;
+ if (organizationList != null && organizationList.size() > 0) {
+ for (OrganizationData organizationData : organizationList) {
+ 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.nonTransactionalInsert(
+ Organizations.CONTENT_URI, values);
+ }
+ if (organizationData.isPrimary) {
+ primaryOrganizationId = Long.parseLong(organizationUri.getLastPathSegment());
+ }
+ }
+ }
+
+ long primaryEmailId = -1;
+ if (contactmethodList != null && contactmethodList.size() > 0) {
+ for (ContactMethod contactMethod : contactmethodList) {
+ 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.nonTransactionalInsert(
+ 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.nonTransactionalInsert(
+ ContactMethods.CONTENT_URI, values);
+ }
+ }
+ }
+ }
+
+ if (extensionMap != null && extensionMap.size() > 0) {
+ ArrayList<ContentValues> contentValuesArray;
+ if (resolver != null) {
+ contentValuesArray = new ArrayList<ContentValues>();
+ } else {
+ contentValuesArray = null;
+ }
+ for (Entry<String, List<String>> entry : extensionMap.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.nonTransactionalInsert(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.nonTransactionalUpdate(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.
+ */
+ public void pushIntoAbstractSyncableContentProvider(
+ AbstractSyncableContentProvider provider, long myContactsGroupId) {
+ boolean successful = false;
+ provider.beginTransaction();
+ try {
+ pushIntoContentProviderOrResolver(provider, myContactsGroupId);
+ successful = true;
+ } finally {
+ provider.endTransaction(successful);
+ }
+ }
+
+ public boolean isIgnorable() {
+ return TextUtils.isEmpty(name) &&
+ TextUtils.isEmpty(phoneticName) &&
+ (phoneList == null || phoneList.size() == 0) &&
+ (contactmethodList == null || contactmethodList.size() == 0);
}
}
diff --git a/core/java/android/syncml/pim/vcard/VCardComposer.java b/core/java/android/syncml/pim/vcard/VCardComposer.java
index 05e8f40..192736a 100644
--- a/core/java/android/syncml/pim/vcard/VCardComposer.java
+++ b/core/java/android/syncml/pim/vcard/VCardComposer.java
@@ -124,9 +124,9 @@ public class VCardComposer {
mResult.append("ORG:").append(struct.company).append(mNewline);
}
- if (!isNull(struct.notes)) {
+ if (struct.notes.size() > 0 && !isNull(struct.notes.get(0))) {
mResult.append("NOTE:").append(
- foldingString(struct.notes, vcardversion)).append(mNewline);
+ foldingString(struct.notes.get(0), vcardversion)).append(mNewline);
}
if (!isNull(struct.title)) {
@@ -190,7 +190,7 @@ public class VCardComposer {
*/
private void appendPhotoStr(byte[] bytes, String type, int version)
throws VCardException {
- String value, apptype, encodingStr;
+ String value, encodingStr;
try {
value = foldingString(new String(Base64.encodeBase64(bytes, true)),
version);
@@ -198,20 +198,23 @@ public class VCardComposer {
throw new VCardException(e.getMessage());
}
- if (isNull(type)) {
- type = "image/jpeg";
- }
- if (type.indexOf("jpeg") > 0) {
- apptype = "JPEG";
- } else if (type.indexOf("gif") > 0) {
- apptype = "GIF";
- } else if (type.indexOf("bmp") > 0) {
- apptype = "BMP";
+ if (isNull(type) || type.toUpperCase().indexOf("JPEG") >= 0) {
+ type = "JPEG";
+ } else if (type.toUpperCase().indexOf("GIF") >= 0) {
+ type = "GIF";
+ } else if (type.toUpperCase().indexOf("BMP") >= 0) {
+ type = "BMP";
} else {
- apptype = type.substring(type.indexOf("/")).toUpperCase();
+ // Handle the string like "image/tiff".
+ int indexOfSlash = type.indexOf("/");
+ if (indexOfSlash >= 0) {
+ type = type.substring(indexOfSlash + 1).toUpperCase();
+ } else {
+ type = type.toUpperCase();
+ }
}
- mResult.append("LOGO;TYPE=").append(apptype);
+ mResult.append("LOGO;TYPE=").append(type);
if (version == VERSION_VCARD21_INT) {
encodingStr = ";ENCODING=BASE64:";
value = value + mNewline;
@@ -281,7 +284,7 @@ public class VCardComposer {
private String getPhoneTypeStr(PhoneData phone) {
- int phoneType = Integer.parseInt(phone.type);
+ int phoneType = phone.type;
String typeStr, label;
if (phoneTypeMap.containsKey(phoneType)) {
@@ -308,7 +311,7 @@ public class VCardComposer {
String joinMark = version == VERSION_VCARD21_INT ? ";" : ",";
for (ContactStruct.ContactMethod contactMethod : contactMList) {
// same with v2.1 and v3.0
- switch (Integer.parseInt(contactMethod.kind)) {
+ switch (contactMethod.kind) {
case Contacts.KIND_EMAIL:
String mailType = "INTERNET";
if (!isNull(contactMethod.data)) {
diff --git a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java
new file mode 100644
index 0000000..a0513f1
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2007 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.syncml.pim.vcard;
+
+import android.app.ProgressDialog;
+import android.content.AbstractSyncableContentProvider;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.IContentProvider;
+import android.os.Handler;
+import android.provider.Contacts;
+import android.syncml.pim.PropertyNode;
+import android.syncml.pim.VBuilder;
+import android.syncml.pim.VNode;
+import android.syncml.pim.VParser;
+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.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 VBuilder {
+ 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 DEFAULT_CHARSET = "UTF-8";
+
+ private class ProgressShower implements Runnable {
+ private ContactStruct mContact;
+
+ public ProgressShower(ContactStruct contact) {
+ mContact = contact;
+ }
+
+ public void run () {
+ mProgressDialog.setMessage(mProgressMessage + "\n" +
+ mContact.displayString());
+ }
+ }
+
+ /** type=VNode */
+ private VNode mCurrentVNode;
+ private PropertyNode mCurrentPropNode;
+ private String mCurrentParamType;
+
+ /**
+ * 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 ContentResolver mContentResolver;
+
+ // For letting VCardDataBuilder show the display name of VCard while handling it.
+ private Handler mHandler;
+ private ProgressDialog mProgressDialog;
+ private String mProgressMessage;
+ private Runnable mOnProgressRunnable;
+ private boolean mLastNameComesBeforeFirstName;
+
+ // Just for testing.
+ private long mTimeCreateContactStruct;
+ private long mTimePushIntoContentResolver;
+
+ // 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;
+
+ public VCardDataBuilder(ContentResolver resolver) {
+ mTargetCharset = DEFAULT_CHARSET;
+ mContentResolver = resolver;
+ }
+
+ /**
+ * Constructor which requires minimum requiredvariables.
+ *
+ * @param resolver insert each data into this ContentResolver
+ * @param progressDialog
+ * @param progressMessage
+ * @param handler if this importer works on the different thread than main one,
+ * set appropriate handler object. If not, it is ok to set this null.
+ */
+ public VCardDataBuilder(ContentResolver resolver,
+ ProgressDialog progressDialog,
+ String progressMessage,
+ Handler handler) {
+ this(resolver, progressDialog, progressMessage, handler,
+ null, null, false, false);
+ }
+
+ public VCardDataBuilder(ContentResolver resolver,
+ ProgressDialog progressDialog,
+ String progressMessage,
+ Handler handler,
+ String charset,
+ boolean strictLineBreakParsing,
+ boolean lastNameComesBeforeFirstName) {
+ this(resolver, progressDialog, progressMessage, handler,
+ null, charset, strictLineBreakParsing,
+ lastNameComesBeforeFirstName);
+ }
+
+ /**
+ * @hide
+ */
+ public VCardDataBuilder(ContentResolver resolver,
+ ProgressDialog progressDialog,
+ String progressMessage,
+ Handler handler,
+ String sourceCharset,
+ String targetCharset,
+ boolean strictLineBreakParsing,
+ boolean lastNameComesBeforeFirstName) {
+ if (sourceCharset != null) {
+ mSourceCharset = sourceCharset;
+ } else {
+ mSourceCharset = VParser.DEFAULT_CHARSET;
+ }
+ if (targetCharset != null) {
+ mTargetCharset = targetCharset;
+ } else {
+ mTargetCharset = DEFAULT_CHARSET;
+ }
+ mContentResolver = resolver;
+ mStrictLineBreakParsing = strictLineBreakParsing;
+ mHandler = handler;
+ mProgressDialog = progressDialog;
+ mProgressMessage = progressMessage;
+ mLastNameComesBeforeFirstName = lastNameComesBeforeFirstName;
+
+ tryGetOriginalProvider();
+ }
+
+ 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 setOnProgressRunnable(Runnable runnable) {
+ mOnProgressRunnable = runnable;
+ }
+
+ public void start() {
+ }
+
+ public void end() {
+ }
+
+ /**
+ * Assume that VCard is not nested. In other words, this code does not accept
+ */
+ public void startRecord(String type) {
+ if (mCurrentVNode != 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.");
+ }
+ mCurrentVNode = new VNode();
+ mCurrentVNode.parseStatus = 1;
+ mCurrentVNode.VName = type;
+ }
+
+ public void endRecord() {
+ mCurrentVNode.parseStatus = 0;
+ long start = System.currentTimeMillis();
+ ContactStruct contact = ContactStruct.constructContactFromVNode(mCurrentVNode,
+ mLastNameComesBeforeFirstName ? ContactStruct.NAME_ORDER_TYPE_JAPANESE :
+ ContactStruct.NAME_ORDER_TYPE_ENGLISH);
+ mTimeCreateContactStruct += System.currentTimeMillis() - start;
+ if (!contact.isIgnorable()) {
+ if (mProgressDialog != null && mProgressMessage != null) {
+ if (mHandler != null) {
+ mHandler.post(new ProgressShower(contact));
+ } else {
+ mProgressDialog.setMessage(mProgressMessage + "\n" +
+ contact.displayString());
+ }
+ }
+ start = System.currentTimeMillis();
+ if (mProvider != null) {
+ contact.pushIntoAbstractSyncableContentProvider(
+ mProvider, mMyContactsGroupId);
+ } else {
+ contact.pushIntoContentResolver(mContentResolver);
+ }
+ mTimePushIntoContentResolver += System.currentTimeMillis() - start;
+ }
+ if (mOnProgressRunnable != null) {
+ mOnProgressRunnable.run();
+ }
+ mCurrentVNode = null;
+ }
+
+ public void startProperty() {
+ mCurrentPropNode = new PropertyNode();
+ }
+
+ public void endProperty() {
+ mCurrentVNode.propList.add(mCurrentPropNode);
+ mCurrentPropNode = null;
+ }
+
+ public void propertyName(String name) {
+ mCurrentPropNode.propName = name;
+ }
+
+ public void propertyGroup(String group) {
+ mCurrentPropNode.propGroupSet.add(group);
+ }
+
+ public void propertyParamType(String type) {
+ mCurrentParamType = type;
+ }
+
+ public void propertyParamValue(String value) {
+ if (mCurrentParamType == null ||
+ mCurrentParamType.equalsIgnoreCase("TYPE")) {
+ mCurrentPropNode.paramMap_TYPE.add(value);
+ } else {
+ mCurrentPropNode.paramMap.put(mCurrentParamType, value);
+ }
+
+ mCurrentParamType = 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 new String(bytes);
+ }
+ }
+
+ private String handleOneValue(String value, String targetCharset, String encoding) {
+ if (encoding != null) {
+ if (encoding.equals("BASE64") || encoding.equals("B")) {
+ mCurrentPropNode.propValue_bytes =
+ 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) {
+ mCurrentPropNode.propValue_bytes = null;
+ mCurrentPropNode.propValue_vector.clear();
+ mCurrentPropNode.propValue_vector.add("");
+ mCurrentPropNode.propValue = "";
+ return;
+ }
+
+ ContentValues paramMap = mCurrentPropNode.paramMap;
+
+ String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET"));
+ String encoding = paramMap.getAsString("ENCODING");
+
+ if (targetCharset == null || targetCharset.length() == 0) {
+ targetCharset = mTargetCharset;
+ }
+
+ for (String value : values) {
+ mCurrentPropNode.propValue_vector.add(
+ handleOneValue(value, targetCharset, encoding));
+ }
+
+ mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector);
+ }
+
+ public void showDebugInfo() {
+ Log.d(LOG_TAG, "time for creating ContactStruct: " + mTimeCreateContactStruct + " ms");
+ Log.d(LOG_TAG, "time for insert ContactStruct to database: " +
+ mTimePushIntoContentResolver + " ms");
+ }
+
+ private String listToString(List<String> list){
+ 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/syncml/pim/vcard/VCardEntryCounter.java b/core/java/android/syncml/pim/vcard/VCardEntryCounter.java
new file mode 100644
index 0000000..03cd1d9
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardEntryCounter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.syncml.pim.vcard;
+
+import java.util.List;
+
+import android.syncml.pim.VBuilder;
+
+public class VCardEntryCounter implements VBuilder {
+ 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/syncml/pim/vcard/VCardNestedException.java b/core/java/android/syncml/pim/vcard/VCardNestedException.java
new file mode 100644
index 0000000..def6f3b
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardNestedException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.syncml.pim.vcard;
+
+/**
+ * VCardException thrown when VCard is nested without VCardParser's being notified.
+ */
+public class VCardNestedException extends VCardException {
+ public VCardNestedException() {}
+ public VCardNestedException(String message) {
+ super(message);
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V21.java b/core/java/android/syncml/pim/vcard/VCardParser_V21.java
index f853c5e..d865668 100644
--- a/core/java/android/syncml/pim/vcard/VCardParser_V21.java
+++ b/core/java/android/syncml/pim/vcard/VCardParser_V21.java
@@ -17,21 +17,26 @@
package android.syncml.pim.vcard;
import android.syncml.pim.VBuilder;
+import android.syncml.pim.VParser;
+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;
-import java.util.regex.Pattern;
/**
- * This class is used to parse vcard. Please refer to vCard Specification 2.1
+ * This class is used to parse vcard. Please refer to vCard Specification 2.1.
*/
public class VCardParser_V21 {
-
+ private static final String LOG_TAG = "VCardParser_V21";
+
+ public static final String DEFAULT_CHARSET = VParser.DEFAULT_CHARSET;
+
/** Store the known-type */
private static final HashSet<String> sKnownTypeSet = new HashSet<String>(
Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK",
@@ -42,19 +47,17 @@ public class VCardParser_V21 {
"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 name available in vCard 2.1 */
- // NICKNAME is not supported in vCard 2.1, but some vCard may contain.
+ /** Store the property names available in vCard 2.1 */
private static final HashSet<String> sAvailablePropertyNameV21 =
new HashSet<String>(Arrays.asList(
- "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
+ "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
"VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
- "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER",
- "NICKNAME"));
+ "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...
@@ -76,6 +79,30 @@ public class VCardParser_V21 {
// Should not directly read a line from this. Use getLine() instead.
protected BufferedReader mReader;
+ private boolean mCanceled;
+
+ // 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.
*/
@@ -83,12 +110,35 @@ public class 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 {
- while (parseOneVCard()) {
+ 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;
+ }
}
}
@@ -100,7 +150,13 @@ public class VCardParser_V21 {
* @return true when the propertyName is a valid property name.
*/
protected boolean isValidPropertyName(String propertyName) {
- return sAvailablePropertyNameV21.contains(propertyName.toUpperCase());
+ 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;
}
/**
@@ -129,7 +185,7 @@ public class VCardParser_V21 {
line = getLine();
if (line == null) {
throw new VCardException("Reached end of buffer.");
- } else if (line.trim().length() > 0) {
+ } else if (line.trim().length() > 0) {
return line;
}
}
@@ -140,12 +196,37 @@ public class VCardParser_V21 {
* items *CRLF
* "END" [ws] ":" [ws] "VCARD"
*/
- private boolean parseOneVCard() throws IOException, VCardException {
- if (!readBeginVCard()) {
+ 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();
- readEndVCard();
+ mTimeParseItems += System.currentTimeMillis() - start;
+ readEndVCard(true, false);
+ if (mBuilder != null) {
+ start = System.currentTimeMillis();
+ mBuilder.endRecord();
+ mTimeEndRecord += System.currentTimeMillis() - start;
+ }
return true;
}
@@ -154,46 +235,102 @@ public class VCardParser_V21 {
* @throws IOException
* @throws VCardException
*/
- protected boolean readBeginVCard() throws IOException, VCardException {
+ protected boolean readBeginVCard(boolean allowGarbage)
+ throws IOException, VCardException {
String line;
- while (true) {
- line = getLine();
- if (line == null) {
- return false;
- } else if (line.trim().length() > 0) {
- break;
+ do {
+ while (true) {
+ line = getLine();
+ if (line == null) {
+ return false;
+ } else if (line.trim().length() > 0) {
+ break;
+ }
}
- }
- String[] strArray = line.split(":", 2);
-
- // Though vCard specification does not allow lower cases,
- // some data may have them, so we allow it.
- if (!(strArray.length == 2 &&
- strArray[0].trim().equalsIgnoreCase("BEGIN") &&
- strArray[1].trim().equalsIgnoreCase("VCARD"))) {
- throw new VCardException("BEGIN:VCARD != \"" + line + "\"");
- }
-
- if (mBuilder != null) {
- mBuilder.startRecord("VCARD");
- }
+ String[] strArray = line.split(":", 2);
+ int length = strArray.length;
- return true;
+ // 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.");
}
-
- protected void readEndVCard() throws VCardException {
- // Though vCard specification does not allow lower cases,
- // some data may have them, so we allow it.
- String[] strArray = mPreviousLine.split(":", 2);
- if (!(strArray.length == 2 &&
- strArray[0].trim().equalsIgnoreCase("END") &&
- strArray[1].trim().equalsIgnoreCase("VCARD"))) {
- throw new VCardException("END:VCARD != \"" + mPreviousLine + "\"");
- }
-
- if (mBuilder != null) {
- mBuilder.endRecord();
- }
+
+ /**
+ * 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);
}
/**
@@ -205,32 +342,33 @@ public class VCardParser_V21 {
boolean ended = false;
if (mBuilder != null) {
+ long start = System.currentTimeMillis();
mBuilder.startProperty();
+ mTimeStartProperty += System.currentTimeMillis() - start;
}
-
- try {
- ended = parseItem();
- } finally {
- if (mBuilder != null) {
- mBuilder.endProperty();
- }
+ 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;
}
- try {
- ended = parseItem();
- } finally {
- if (mBuilder != null) {
- mBuilder.endProperty();
- }
+ 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
@@ -241,57 +379,134 @@ public class VCardParser_V21 {
protected boolean parseItem() throws IOException, VCardException {
mEncoding = sDefaultEncoding;
- // params = ";" [ws] paramlist
String line = getNonEmptyLine();
- String[] strArray = line.split(":", 2);
- if (strArray.length < 2) {
- throw new VCardException("Invalid line(\":\" does not exist): " + line);
- }
- String propertyValue = strArray[1];
- String[] groupNameParamsArray = strArray[0].split(";");
- String groupAndName = groupNameParamsArray[0].trim();
- String[] groupNameArray = groupAndName.split("\\.");
- int length = groupNameArray.length;
- String propertyName = groupNameArray[length - 1];
- if (mBuilder != null) {
- mBuilder.propertyName(propertyName);
- for (int i = 0; i < length - 1; i++) {
- mBuilder.propertyGroup(groupNameArray[i]);
- }
- }
- if (propertyName.equalsIgnoreCase("END")) {
- mPreviousLine = line;
+ long start = System.currentTimeMillis();
+
+ String[] propertyNameAndValue = separateLineAndHandleGroup(line);
+ if (propertyNameAndValue == null) {
return true;
}
-
- length = groupNameParamsArray.length;
- for (int i = 1; i < length; i++) {
- handleParams(groupNameParamsArray[i]);
+ if (propertyNameAndValue.length != 2) {
+ throw new VCardException("Invalid line \"" + line + "\"");
}
-
- if (isValidPropertyName(propertyName) ||
- propertyName.startsWith("X-")) {
- if (propertyName.equals("VERSION") &&
- !propertyValue.equals(getVersion())) {
- throw new VCardVersionException("Incompatible version: " +
- propertyValue + " != " + getVersion());
- }
- handlePropertyValue(propertyName, propertyValue);
- return false;
- } else if (propertyName.equals("ADR") ||
+ 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
@@ -330,18 +545,19 @@ public class VCardParser_V21 {
}
/**
- * typeval = knowntype / "X-" word
+ * ptypeval = knowntype / "X-" word
*/
- protected void handleType(String ptypeval) throws VCardException {
- if (sKnownTypeSet.contains(ptypeval.toUpperCase()) ||
- ptypeval.startsWith("X-")) {
- if (mBuilder != null) {
- mBuilder.propertyParamType("TYPE");
- mBuilder.propertyParamValue(ptypeval.toUpperCase());
- }
- } else {
- throw new VCardException("Unknown type: \"" + ptypeval + "\"");
- }
+ 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);
+ }
}
/**
@@ -427,31 +643,48 @@ public class VCardParser_V21 {
protected void handlePropertyValue(
String propertyName, String propertyValue) throws
IOException, VCardException {
- if (mEncoding == null || mEncoding.equalsIgnoreCase("7BIT")
- || mEncoding.equalsIgnoreCase("8BIT")
- || mEncoding.toUpperCase().startsWith("X-")) {
- if (mBuilder != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(maybeUnescapeText(propertyValue));
- mBuilder.propertyValues(v);
- }
- } else if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
+ 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")) {
- String result = getBase64(propertyValue);
+ 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(result);
+ v.add(maybeUnescapeText(propertyValue));
mBuilder.propertyValues(v);
- }
- } else {
- throw new VCardException("Unknown encoding: \"" + mEncoding + "\"");
+ }
+ mTimeHandlePropertyValue1 += System.currentTimeMillis() - start;
}
}
@@ -546,57 +779,51 @@ public class VCardParser_V21 {
if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
propertyValue = getQuotedPrintable(propertyValue);
}
-
- if (propertyValue.endsWith("\\")) {
+
+ if (mBuilder != null) {
+ // TODO: limit should be set in accordance with propertyName?
StringBuilder builder = new StringBuilder();
- // builder.append(propertyValue);
- builder.append(propertyValue.substring(0, propertyValue.length() - 1));
- try {
- String line;
- while (true) {
- line = getNonEmptyLine();
- // builder.append("\r\n");
- // builder.append(line);
- if (!line.endsWith("\\")) {
- builder.append(line);
- break;
+ 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(line.substring(0, line.length() - 1));
+ builder.append(ch);
}
+ } else if (ch == ';') {
+ list.add(builder.toString());
+ builder = new StringBuilder();
+ } else {
+ builder.append(ch);
}
- } catch (IOException e) {
- throw new VCardException(
- "IOException is throw during reading propertyValue" + e);
}
- // Now, propertyValue may contain "\r\n"
- propertyValue = builder.toString();
- }
-
- if (mBuilder != null) {
- // In String#replaceAll() and Pattern class, "\\\\" means single slash.
-
- final String IMPOSSIBLE_STRING = "\0";
- // First replace two backslashes with impossible strings.
- propertyValue = propertyValue.replaceAll("\\\\\\\\", IMPOSSIBLE_STRING);
-
- // Now, split propertyValue with ; whose previous char is not back slash.
- Pattern pattern = Pattern.compile("(?<!\\\\);");
- // TODO: limit should be set in accordance with propertyName?
- String[] strArray = pattern.split(propertyValue, -1);
- ArrayList<String> arrayList = new ArrayList<String>();
- for (String str : strArray) {
- // Replace impossible strings with original two backslashes
- arrayList.add(
- unescapeText(str.replaceAll(IMPOSSIBLE_STRING, "\\\\\\\\")));
- }
- mBuilder.propertyValues(arrayList);
+ 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 IOException, VCardException {
+ protected void handleAgent(String propertyValue) throws VCardException {
+ throw new VCardException("AGENT Property is not supported.");
+ /* 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") &&
@@ -605,6 +832,7 @@ public class VCardParser_V21 {
}
parseItems();
readEndVCard();
+ */
}
/**
@@ -615,17 +843,18 @@ public class VCardParser_V21 {
}
/**
- * Convert escaped text into unescaped 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 unescapeText(String text) {
+ 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.
- // In String#replaceAll(), "\\\\" means single slash.
- return text.replaceAll("\\\\;", ";")
- .replaceAll("\\\\:", ":")
- .replaceAll("\\\\,", ",")
- .replaceAll("\\\\\\\\", "\\\\");
+ if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') {
+ return String.valueOf(ch);
+ } else {
+ return null;
+ }
}
/**
@@ -656,12 +885,15 @@ public class VCardParser_V21 {
*/
public boolean parse(InputStream is, String charset, VBuilder 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().
- mReader = new BufferedReader(new InputStreamReader(is, charset));
+ mReader = new CustomBufferedReader(new InputStreamReader(is, charset));
mBuilder = builder;
+ long start = System.currentTimeMillis();
if (mBuilder != null) {
mBuilder.start();
}
@@ -669,9 +901,50 @@ public class VCardParser_V21 {
if (mBuilder != null) {
mBuilder.end();
}
+ mTimeTotal += System.currentTimeMillis() - start;
+
return true;
}
+ public boolean parse(InputStream is, VBuilder builder) throws IOException, VCardException {
+ return parse(is, DEFAULT_CHARSET, builder);
+ }
+
+ /**
+ * Cancel parsing.
+ * Actual cancel is done after the end of the current one vcard entry parsing.
+ */
+ public void cancel() {
+ mCanceled = true;
+ }
+
+ /**
+ * It is very, very rare case, but there is a case where
+ * canceled may be already true outside this object.
+ * @hide
+ */
+ public void parse(InputStream is, String charset, VBuilder builder, boolean canceled)
+ throws IOException, VCardException {
+ mCanceled = canceled;
+ parse(is, charset, builder);
+ }
+
+ public void showDebugInfo() {
+ 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;
@@ -679,3 +952,24 @@ public class VCardParser_V21 {
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/syncml/pim/vcard/VCardParser_V30.java b/core/java/android/syncml/pim/vcard/VCardParser_V30.java
index 901bd49..e67525e 100644
--- a/core/java/android/syncml/pim/vcard/VCardParser_V30.java
+++ b/core/java/android/syncml/pim/vcard/VCardParser_V30.java
@@ -16,8 +16,9 @@
package android.syncml.pim.vcard;
+import android.util.Log;
+
import java.io.IOException;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -26,9 +27,11 @@ import java.util.HashSet;
* Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426)
*/
public class VCardParser_V30 extends VCardParser_V21 {
+ private static final String LOG_TAG = "VCardParser_V30";
+
private static final HashSet<String> acceptablePropsWithParam = new HashSet<String>(
Arrays.asList(
- "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
+ "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",
@@ -51,8 +54,14 @@ public class VCardParser_V30 extends VCardParser_V21 {
@Override
protected boolean isValidPropertyName(String propertyName) {
- return acceptablePropsWithParam.contains(propertyName) ||
- acceptablePropsWithoutParam.contains(propertyName);
+ if (!(acceptablePropsWithParam.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
@@ -100,7 +109,21 @@ public class VCardParser_V30 extends VCardParser_V21 {
}
} else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
if (builder != null) {
- // TODO: Check whether MIME requires only one whitespace.
+ // 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();
@@ -113,10 +136,13 @@ public class VCardParser_V30 extends VCardParser_V21 {
} else {
if (mPreviousLine == null) {
mPreviousLine = line;
+ if (builder != null) {
+ return builder.toString();
+ }
} else {
String ret = mPreviousLine;
mPreviousLine = line;
- return ret;
+ return ret;
}
}
}
@@ -130,15 +156,16 @@ public class VCardParser_V30 extends VCardParser_V21 {
* [group "."] "END" ":" "VCARD" 1*CRLF
*/
@Override
- protected boolean readBeginVCard() throws IOException, VCardException {
+ protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
// TODO: vCard 3.0 supports group.
- return super.readBeginVCard();
+ return super.readBeginVCard(allowGarbage);
}
@Override
- protected void readEndVCard() throws VCardException {
+ protected void readEndVCard(boolean useCache, boolean allowGarbage)
+ throws IOException, VCardException {
// TODO: vCard 3.0 supports group.
- super.readEndVCard();
+ super.readEndVCard(useCache, allowGarbage);
}
/**
@@ -214,23 +241,6 @@ public class VCardParser_V30 extends VCardParser_V21 {
throw new VCardException("AGENT in vCard 3.0 is not supported yet.");
}
- // vCard 3.0 supports "B" as BASE64 encoding.
- @Override
- protected void handlePropertyValue(
- String propertyName, String propertyValue) throws
- IOException, VCardException {
- if (mEncoding != null && mEncoding.equalsIgnoreCase("B")) {
- String result = getBase64(propertyValue);
- if (mBuilder != null) {
- ArrayList<String> v = new ArrayList<String>();
- v.add(result);
- mBuilder.propertyValues(v);
- }
- }
-
- super.handlePropertyValue(propertyName, propertyValue);
- }
-
/**
* vCard 3.0 does not require two CRLF at the last of BASE64 data.
* It only requires that data should be MIME-encoded.
@@ -259,27 +269,38 @@ public class VCardParser_V30 extends VCardParser_V21 {
}
/**
- * Return unescapeText(text).
- * In vCard 3.0, 8bit text is always encoded.
- */
- @Override
- protected String maybeUnescapeText(String text) {
- return unescapeText(text);
- }
-
- /**
* ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
* ; \\ encodes \, \n or \N encodes newline
* ; \; encodes ;, \, encodes ,
- */
+ *
+ * Note: Apple escape ':' into '\:' while does not escape '\'
+ */
@Override
- protected String unescapeText(String text) {
- // In String#replaceAll(), "\\\\" means single slash.
- return text.replaceAll("\\\\;", ";")
- .replaceAll("\\\\:", ":")
- .replaceAll("\\\\,", ",")
- .replaceAll("\\\\n", "\r\n")
- .replaceAll("\\\\N", "\r\n")
- .replaceAll("\\\\\\\\", "\\\\");
+ 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/syncml/pim/vcard/VCardSourceDetector.java b/core/java/android/syncml/pim/vcard/VCardSourceDetector.java
new file mode 100644
index 0000000..8c48391
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardSourceDetector.java
@@ -0,0 +1,140 @@
+/*
+ * 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.syncml.pim.vcard;
+
+import android.syncml.pim.VBuilder;
+
+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 VBuilder {
+ // 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/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
index 7553029..9d91aca 100644
--- a/core/java/android/util/CharsetUtils.java
+++ b/core/java/android/util/CharsetUtils.java
@@ -142,20 +142,25 @@ public final class CharsetUtils {
/**
* Returns whether the given character set name indicates the Shift-JIS
- * encoding.
+ * encoding. Returns false if the name is null.
*
* @param charsetName the character set name
* @return {@code true} if the name corresponds to Shift-JIS or
* {@code false} if not
*/
private static boolean isShiftJis(String charsetName) {
- if (charsetName.length() != 9) {
- // Bail quickly if the length doesn't match.
+ // Bail quickly if the length doesn't match.
+ if (charsetName == null) {
+ return false;
+ }
+ int length = charsetName.length();
+ if (length != 4 && length != 9) {
return false;
}
return charsetName.equalsIgnoreCase("shift_jis")
- || charsetName.equalsIgnoreCase("shift-jis");
+ || charsetName.equalsIgnoreCase("shift-jis")
+ || charsetName.equalsIgnoreCase("sjis");
}
/**
diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java
index c28210d..32e5504 100644
--- a/core/java/android/widget/ArrayAdapter.java
+++ b/core/java/android/widget/ArrayAdapter.java
@@ -348,7 +348,12 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable {
"ArrayAdapter requires the resource ID to be a TextView", e);
}
- text.setText(getItem(position).toString());
+ T item = getItem(position);
+ if (item instanceof CharSequence) {
+ text.setText((CharSequence)item);
+ } else {
+ text.setText(item.toString());
+ }
return view;
}