diff options
17 files changed, 2746 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; } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 8ad8a84..058a445 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1095,6 +1095,9 @@ <item>Custom</item> </string-array> + <!-- String which means the type "mobile phone". --> + <string name="mobileEmailTypeName">Mobile</string> + <!-- The order of these is important, don't reorder without changing Contacts.java --> <skip /> <!-- Postal address types from android.provider.Contacts. This could be used when adding a new address for a contact, for example. --> <string-array name="postalAddressTypes"> |