Hand merge from cupcake_dcm to donut, part 2.
Modify Contacts-related java files and update vCard importer code to the latest.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* <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 (successful) {
|
||||
// setTransactionSuccessful() must be called just once during opening the
|
||||
// transaction.
|
||||
mDb.setTransactionSuccessful();
|
||||
}
|
||||
} finally {
|
||||
mDb.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int update(final Uri url, final ContentValues values,
|
||||
public final int update(final Uri uri, final ContentValues values,
|
||||
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().update(
|
||||
url, values, selection, selectionArgs);
|
||||
mDb.setTransactionSuccessful();
|
||||
return numRows;
|
||||
}
|
||||
|
||||
int result = updateInternal(url, values, selection, selectionArgs);
|
||||
mDb.setTransactionSuccessful();
|
||||
|
||||
if (!isTemporary() && result > 0) {
|
||||
getContext().getContentResolver().notifyChange(url, null /* observer */,
|
||||
changeRequiresLocalSync(url));
|
||||
}
|
||||
|
||||
return result;
|
||||
int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs);
|
||||
successful = true;
|
||||
return ret;
|
||||
} finally {
|
||||
mDb.endTransaction();
|
||||
endTransaction(successful);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Uri insert(final Uri url, final ContentValues values) {
|
||||
mDb = mOpenHelper.getWritableDatabase();
|
||||
mDb.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;
|
||||
} finally {
|
||||
mDb.endTransaction();
|
||||
/**
|
||||
* @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 uri, final ContentValues values) {
|
||||
boolean successful = false;
|
||||
beginTransaction();
|
||||
try {
|
||||
Uri ret = nonTransactionalInsert(uri, values);
|
||||
successful = true;
|
||||
return ret;
|
||||
} finally {
|
||||
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
|
||||
|
||||
@@ -868,6 +868,17 @@ public class Contacts {
|
||||
public static final int TYPE_WORK = 2;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
100
core/java/android/syncml/pim/VBuilderCollection.java
Normal file
100
core/java/android/syncml/pim/VBuilderCollection.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 mCharset;
|
||||
private String mSourceCharset;
|
||||
|
||||
/**
|
||||
* The charset with which byte array is encoded to String.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(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 {
|
||||
builder.append(ch);
|
||||
}
|
||||
}
|
||||
String finalLine = builder.toString();
|
||||
if (finalLine.length() > 0) {
|
||||
list.add(finalLine);
|
||||
}
|
||||
lines = list.toArray(new String[0]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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 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()));
|
||||
}
|
||||
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));
|
||||
}
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
setupParamValues = true;
|
||||
} catch(Exception e) {
|
||||
Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e);
|
||||
}
|
||||
} // QUOTED-PRINTABLE
|
||||
} // ENCODING
|
||||
String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET"));
|
||||
String encoding = paramMap.getAsString("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);
|
||||
}
|
||||
}
|
||||
values = tmpValues;
|
||||
}
|
||||
|
||||
mCurrentPropNode.propValue_vector = values;
|
||||
mCurrentPropNode.propValue = listToString(values);
|
||||
if (targetCharset == null || targetCharset.length() == 0) {
|
||||
targetCharset = mTargetCharset;
|
||||
}
|
||||
mCurrentVNode.propList.add(mCurrentPropNode);
|
||||
|
||||
for (String value : values) {
|
||||
mCurrentPropNode.propValue_vector.add(
|
||||
handleOneValue(value, targetCharset, encoding));
|
||||
}
|
||||
|
||||
mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector);
|
||||
}
|
||||
|
||||
private String listToString(Collection<String> list){
|
||||
StringBuilder typeListB = new StringBuilder();
|
||||
for (String type : list) {
|
||||
typeListB.append(type).append(";");
|
||||
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 "";
|
||||
}
|
||||
int len = typeListB.length();
|
||||
if (len > 0 && typeListB.charAt(len - 1) == ';') {
|
||||
return typeListB.substring(0, len - 1);
|
||||
}
|
||||
return typeListB.toString();
|
||||
}
|
||||
|
||||
public String getResult(){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -95,6 +98,20 @@ abstract public class VParser {
|
||||
return (mBuffer.length() == sum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
442
core/java/android/syncml/pim/vcard/VCardDataBuilder.java
Normal file
442
core/java/android/syncml/pim/vcard/VCardDataBuilder.java
Normal file
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
63
core/java/android/syncml/pim/vcard/VCardEntryCounter.java
Normal file
63
core/java/android/syncml/pim/vcard/VCardEntryCounter.java
Normal file
@@ -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) {
|
||||
}
|
||||
}
|
||||
27
core/java/android/syncml/pim/vcard/VCardNestedException.java
Normal file
27
core/java/android/syncml/pim/vcard/VCardNestedException.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,50 +379,46 @@ 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") &&
|
||||
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);
|
||||
return false;
|
||||
} else if (propertyName.equals("ADR") ||
|
||||
propertyName.equals("ORG") ||
|
||||
propertyName.equals("N")) {
|
||||
handleMultiplePropertyValue(propertyName, propertyValue);
|
||||
return false;
|
||||
} else if (propertyName.equals("AGENT")) {
|
||||
handleAgent(propertyValue);
|
||||
mTimeParseItem2 += System.currentTimeMillis() - start;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -292,6 +426,87 @@ public class VCardParser_V21 {
|
||||
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("\\")) {
|
||||
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;
|
||||
} else {
|
||||
builder.append(line.substring(0, line.length() - 1));
|
||||
}
|
||||
}
|
||||
} 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, "\\\\\\\\")));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
int length = propertyValue.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char ch = propertyValue.charAt(i);
|
||||
if (ch == '\\' && i < length - 1) {
|
||||
char nextCh = propertyValue.charAt(i + 1);
|
||||
String unescapedString = maybeUnescape(nextCh);
|
||||
if (unescapedString != null) {
|
||||
builder.append(unescapedString);
|
||||
i++;
|
||||
} else {
|
||||
builder.append(ch);
|
||||
}
|
||||
} else if (ch == ';') {
|
||||
list.add(builder.toString());
|
||||
builder = new StringBuilder();
|
||||
} else {
|
||||
builder.append(ch);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -258,28 +268,39 @@ public class VCardParser_V30 extends VCardParser_V21 {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
core/java/android/syncml/pim/vcard/VCardSourceDetector.java
Normal file
140
core/java/android/syncml/pim/vcard/VCardSourceDetector.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user