Merge change 6736

* changes:
  Refactor VCard-related code.
This commit is contained in:
Android (Google) Code Review
2009-07-17 14:09:00 -07:00
25 changed files with 4433 additions and 28 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import android.content.AbstractSyncableContentProvider;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.IContentProvider;
import android.provider.Contacts;
import android.util.Log;
/**
* EntryHandler implementation which commits the entry to Contacts Provider
*/
public class EntryCommitter implements EntryHandler {
public static String LOG_TAG = "vcard.EntryComitter";
private ContentResolver mContentResolver;
// Ideally, this should be ContactsProvider but it seems Class loader cannot find it,
// even when it is subclass of ContactsProvider...
private AbstractSyncableContentProvider mProvider;
private long mMyContactsGroupId;
private long mTimeToCommit;
public EntryCommitter(ContentResolver resolver) {
mContentResolver = resolver;
tryGetOriginalProvider();
}
public void onFinal() {
if (VCardConfig.showPerformanceLog()) {
Log.d(LOG_TAG,
String.format("time to commit entries: %ld ms", mTimeToCommit));
}
}
private void tryGetOriginalProvider() {
final ContentResolver resolver = mContentResolver;
if ((mMyContactsGroupId = Contacts.People.tryGetMyContactsGroupId(resolver)) == 0) {
Log.e(LOG_TAG, "Could not get group id of MyContact");
return;
}
IContentProvider iProviderForName = resolver.acquireProvider(Contacts.CONTENT_URI);
ContentProvider contentProvider =
ContentProvider.coerceToLocalContentProvider(iProviderForName);
if (contentProvider == null) {
Log.e(LOG_TAG, "Fail to get ContentProvider object.");
return;
}
if (!(contentProvider instanceof AbstractSyncableContentProvider)) {
Log.e(LOG_TAG,
"Acquired ContentProvider object is not AbstractSyncableContentProvider.");
return;
}
mProvider = (AbstractSyncableContentProvider)contentProvider;
}
public void onEntryCreated(final ContactStruct contactStruct) {
long start = System.currentTimeMillis();
if (mProvider != null) {
contactStruct.pushIntoAbstractSyncableContentProvider(
mProvider, mMyContactsGroupId);
} else {
contactStruct.pushIntoContentResolver(mContentResolver);
}
mTimeToCommit += System.currentTimeMillis() - start;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
/**
* Unlike VCardBuilderBase, this (and VCardDataBuilder) assumes
* "each VCard entry should be correctly parsed and passed to each EntryHandler object",
*/
public interface EntryHandler {
/**
* Able to be use this method for showing performance log, etc.
* TODO: better name?
*/
public void onFinal();
/**
* The method called when one VCard entry is successfully created
*/
public void onEntryCreated(final ContactStruct entry);
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import java.util.List;
public interface VCardBuilder {
void start();
void end();
/**
* BEGIN:VCARD
*/
void startRecord(String type);
/** END:VXX */
void endRecord();
void startProperty();
void endProperty();
/**
* @param group
*/
void propertyGroup(String group);
/**
* @param name
* N <br>
* N
*/
void propertyName(String name);
/**
* @param type
* LANGUAGE \ ENCODING <br>
* ;LANGUage= \ ;ENCODING=
*/
void propertyParamType(String type);
/**
* @param value
* FR-EN \ GBK <br>
* FR-EN \ GBK
*/
void propertyParamValue(String value);
void propertyValues(List<String> values);
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import java.util.Collection;
import java.util.List;
public class VCardBuilderCollection implements VCardBuilder {
private final Collection<VCardBuilder> mVCardBuilderCollection;
public VCardBuilderCollection(Collection<VCardBuilder> vBuilderCollection) {
mVCardBuilderCollection = vBuilderCollection;
}
public Collection<VCardBuilder> getVCardBuilderBaseCollection() {
return mVCardBuilderCollection;
}
public void start() {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.start();
}
}
public void end() {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.end();
}
}
public void startRecord(String type) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.startRecord(type);
}
}
public void endRecord() {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.endRecord();
}
}
public void startProperty() {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.startProperty();
}
}
public void endProperty() {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.endProperty();
}
}
public void propertyGroup(String group) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.propertyGroup(group);
}
}
public void propertyName(String name) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.propertyName(name);
}
}
public void propertyParamType(String type) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.propertyParamType(type);
}
}
public void propertyParamValue(String value) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.propertyParamValue(value);
}
}
public void propertyValues(List<String> values) {
for (VCardBuilder builder : mVCardBuilderCollection) {
builder.propertyValues(values);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
/**
* The class representing VCard related configurations
*/
public class VCardConfig {
static final int LOG_LEVEL_NONE = 0;
static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1;
static final int LOG_LEVEL_SHOW_WARNING = 0x2;
static final int LOG_LEVEL_VERBOSE =
LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING;
// Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and
// decode the unicode to the original charset. If not, this setting will cause some bug.
public static final String DEFAULT_CHARSET = "iso-8859-1";
// TODO: use this flag
public static boolean IGNORE_CASE_EXCEPT_VALUE = true;
protected static final int LOG_LEVEL = LOG_LEVEL_PERFORMANCE_MEASUREMENT;
// Note: phonetic name probably should be "LAST FIRST MIDDLE" for European languages, and
// space should be added between each element while it should not be in Japanese.
// But unfortunately, we currently do not have the data and are not sure whether we should
// support European version of name ordering.
//
// TODO: Implement the logic described above if we really need European version of
// phonetic name handling. Also, adding the appropriate test case of vCard would be
// highly appreciated.
public static final int NAME_ORDER_TYPE_ENGLISH = 0;
public static final int NAME_ORDER_TYPE_JAPANESE = 1;
public static final int NAME_ORDER_TYPE_DEFAULT = NAME_ORDER_TYPE_ENGLISH;
/**
* @hide temporal. may be deleted
*/
public static boolean showPerformanceLog() {
return (LOG_LEVEL & LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0;
}
private VCardConfig() {
}
}

View File

@@ -0,0 +1,319 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import android.util.CharsetUtils;
import android.util.Log;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.net.QuotedPrintableCodec;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* VBuilder for VCard. VCard may contain big photo images encoded by BASE64,
* If we store all VNode entries in memory like VDataBuilder.java,
* OutOfMemoryError may be thrown. Thus, this class push each VCard entry into
* ContentResolver immediately.
*/
public class VCardDataBuilder implements VCardBuilder {
static private String LOG_TAG = "VCardDataBuilder";
/**
* If there's no other information available, this class uses this charset for encoding
* byte arrays.
*/
static public String TARGET_CHARSET = "UTF-8";
private ContactStruct.Property mCurrentProperty = new ContactStruct.Property();
private ContactStruct mCurrentContactStruct;
private String mParamType;
/**
* The charset using which VParser parses the text.
*/
private String mSourceCharset;
/**
* The charset with which byte array is encoded to String.
*/
private String mTargetCharset;
private boolean mStrictLineBreakParsing;
private int mNameOrderType;
// Just for testing.
private long mTimePushIntoContentResolver;
private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>();
public VCardDataBuilder() {
this(null, null, false, VCardConfig.NAME_ORDER_TYPE_DEFAULT);
}
/**
* @hide
*/
public VCardDataBuilder(int nameOrderType) {
this(null, null, false, nameOrderType);
}
/**
* @hide
*/
public VCardDataBuilder(String charset,
boolean strictLineBreakParsing,
int nameOrderType) {
this(null, charset, strictLineBreakParsing, nameOrderType);
}
/**
* @hide
*/
public VCardDataBuilder(String sourceCharset,
String targetCharset,
boolean strictLineBreakParsing,
int nameOrderType) {
if (sourceCharset != null) {
mSourceCharset = sourceCharset;
} else {
mSourceCharset = VCardConfig.DEFAULT_CHARSET;
}
if (targetCharset != null) {
mTargetCharset = targetCharset;
} else {
mTargetCharset = TARGET_CHARSET;
}
mStrictLineBreakParsing = strictLineBreakParsing;
mNameOrderType = nameOrderType;
}
public void addEntryHandler(EntryHandler entryHandler) {
mEntryHandlers.add(entryHandler);
}
public void start() {
}
public void end() {
for (EntryHandler entryHandler : mEntryHandlers) {
entryHandler.onFinal();
}
}
/**
* Assume that VCard is not nested. In other words, this code does not accept
*/
public void startRecord(String type) {
// TODO: add the method clear() instead of using null for reducing GC?
if (mCurrentContactStruct != null) {
// This means startRecord() is called inside startRecord() - endRecord() block.
// TODO: should throw some Exception
Log.e(LOG_TAG, "Nested VCard code is not supported now.");
}
if (!type.equalsIgnoreCase("VCARD")) {
// TODO: add test case for this
Log.e(LOG_TAG, "This is not VCARD!");
}
mCurrentContactStruct = new ContactStruct(mNameOrderType);
}
public void endRecord() {
mCurrentContactStruct.consolidateFields();
for (EntryHandler entryHandler : mEntryHandlers) {
entryHandler.onEntryCreated(mCurrentContactStruct);
}
mCurrentContactStruct = null;
}
public void startProperty() {
mCurrentProperty.clear();
}
public void endProperty() {
mCurrentContactStruct.addProperty(mCurrentProperty);
}
public void propertyName(String name) {
mCurrentProperty.setPropertyName(name);
}
public void propertyGroup(String group) {
// ContactStruct does not support Group.
}
public void propertyParamType(String type) {
if (mParamType != null) {
Log.e(LOG_TAG,
"propertyParamType() is called more than once " +
"before propertyParamValue() is called");
}
mParamType = type;
}
public void propertyParamValue(String value) {
if (mParamType == null) {
mParamType = "TYPE";
}
mCurrentProperty.addParameter(mParamType, value);
mParamType = null;
}
private String encodeString(String originalString, String targetCharset) {
if (mSourceCharset.equalsIgnoreCase(targetCharset)) {
return originalString;
}
Charset charset = Charset.forName(mSourceCharset);
ByteBuffer byteBuffer = charset.encode(originalString);
// byteBuffer.array() "may" return byte array which is larger than
// byteBuffer.remaining(). Here, we keep on the safe side.
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
try {
return new String(bytes, targetCharset);
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
return null;
}
}
private String handleOneValue(String value, String targetCharset, String encoding) {
if (encoding != null) {
if (encoding.equals("BASE64") || encoding.equals("B")) {
mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes()));
return value;
} else if (encoding.equals("QUOTED-PRINTABLE")) {
// "= " -> " ", "=\t" -> "\t".
// Previous code had done this replacement. Keep on the safe side.
StringBuilder builder = new StringBuilder();
int length = value.length();
for (int i = 0; i < length; i++) {
char ch = value.charAt(i);
if (ch == '=' && i < length - 1) {
char nextCh = value.charAt(i + 1);
if (nextCh == ' ' || nextCh == '\t') {
builder.append(nextCh);
i++;
continue;
}
}
builder.append(ch);
}
String quotedPrintable = builder.toString();
String[] lines;
if (mStrictLineBreakParsing) {
lines = quotedPrintable.split("\r\n");
} else {
builder = new StringBuilder();
length = quotedPrintable.length();
ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < length; i++) {
char ch = quotedPrintable.charAt(i);
if (ch == '\n') {
list.add(builder.toString());
builder = new StringBuilder();
} else if (ch == '\r') {
list.add(builder.toString());
builder = new StringBuilder();
if (i < length - 1) {
char nextCh = quotedPrintable.charAt(i + 1);
if (nextCh == '\n') {
i++;
}
}
} else {
builder.append(ch);
}
}
String finalLine = builder.toString();
if (finalLine.length() > 0) {
list.add(finalLine);
}
lines = list.toArray(new String[0]);
}
builder = new StringBuilder();
for (String line : lines) {
if (line.endsWith("=")) {
line = line.substring(0, line.length() - 1);
}
builder.append(line);
}
byte[] bytes;
try {
bytes = builder.toString().getBytes(mSourceCharset);
} catch (UnsupportedEncodingException e1) {
Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset);
bytes = builder.toString().getBytes();
}
try {
bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes);
} catch (DecoderException e) {
Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e);
return "";
}
try {
return new String(bytes, targetCharset);
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
return new String(bytes);
}
}
// Unknown encoding. Fall back to default.
}
return encodeString(value, targetCharset);
}
public void propertyValues(List<String> values) {
if (values == null || values.size() == 0) {
return;
}
final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET");
String charset =
((charsetCollection != null) ? charsetCollection.iterator().next() : null);
String targetCharset = CharsetUtils.nameForDefaultVendor(charset);
final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING");
String encoding =
((encodingCollection != null) ? encodingCollection.iterator().next() : null);
if (targetCharset == null || targetCharset.length() == 0) {
targetCharset = mTargetCharset;
}
for (String value : values) {
mCurrentProperty.addToPropertyValueList(
handleOneValue(value, targetCharset, encoding));
}
}
public void showPerformanceInfo() {
Log.d(LOG_TAG, "time for insert ContactStruct to database: " +
mTimePushIntoContentResolver + " ms");
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import java.util.List;
public class VCardEntryCounter implements VCardBuilder {
private int mCount;
public int getCount() {
return mCount;
}
public void start() {
}
public void end() {
}
public void startRecord(String type) {
}
public void endRecord() {
mCount++;
}
public void startProperty() {
}
public void endProperty() {
}
public void propertyGroup(String group) {
}
public void propertyName(String name) {
}
public void propertyParamType(String type) {
}
public void propertyParamValue(String value) {
}
public void propertyValues(List<String> values) {
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import android.pim.vcard.exception.VCardException;
import java.io.IOException;
import java.io.InputStream;
public abstract class VCardParser {
protected boolean mCanceled;
/**
* Parses the given stream and send the VCard data into VCardBuilderBase object.
*
* Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets
* local encoding to it. For example, Japanese phone career uses Shift_JIS, which is
* formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1,
* In some exreme case, some VCard may have different charsets in one VCard (though
* we do not see any device which emits such kind of malicious data)
*
* In order to avoid "misunderstanding" charset as much as possible, this method
* use "ISO-8859-1" for reading the stream. When charset is specified in some property
* (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to
* the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit
* characters, which is not completely sure. In some cases, this "decoding-encoding"
* scheme may fail. To avoid the case,
*
* We recommend you to use VCardSourceDetector and detect which kind of source the
* VCard comes from and explicitly specify a charset using the result.
*
* @param is The source to parse.
* @param builder The VCardBuilderBase object which used to construct data. If you want to
* include multiple VCardBuilderBase objects in this field, consider using
* {#link VCardBuilderCollection} class.
* @return Returns true for success. Otherwise returns false.
* @throws IOException, VCardException
*/
public abstract boolean parse(InputStream is, VCardBuilder builder)
throws IOException, VCardException;
/**
* The method variants which accept charset.
*
* RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use
* UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese
* phone uses Shift_JIS as a charset (e.g. W61SH), and another uses
* "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification
* (e.g. W53K).
*
* @param is The source to parse.
* @param charset Charset to be used.
* @param builder The VCardBuilderBase object.
* @return Returns true when successful. Otherwise returns false.
* @throws IOException, VCardException
*/
public abstract boolean parse(InputStream is, String charset, VCardBuilder builder)
throws IOException, VCardException;
/**
* The method variants which tells this object the operation is already canceled.
* XXX: Is this really necessary?
* @hide
*/
public abstract void parse(InputStream is, String charset,
VCardBuilder builder, boolean canceled)
throws IOException, VCardException;
/**
* Cancel parsing.
* Actual cancel is done after the end of the current one vcard entry parsing.
*/
public void cancel() {
mCanceled = true;
}
}

View File

@@ -0,0 +1,948 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import android.pim.vcard.exception.VCardException;
import android.pim.vcard.exception.VCardNestedException;
import android.pim.vcard.exception.VCardNotSupportedException;
import android.pim.vcard.exception.VCardVersionException;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
/**
* This class is used to parse vcard. Please refer to vCard Specification 2.1.
*/
public class VCardParser_V21 extends VCardParser {
private static final String LOG_TAG = "VCardParser_V21";
/** Store the known-type */
private static final HashSet<String> sKnownTypeSet = new HashSet<String>(
Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK",
"PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS",
"MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK",
"ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL",
"MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF",
"CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF",
"PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI",
"WAVE", "AIFF", "PCM", "X509", "PGP"));
/** Store the known-value */
private static final HashSet<String> sKnownValueSet = new HashSet<String>(
Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID"));
/** Store the property names available in vCard 2.1 */
private static final HashSet<String> sAvailablePropertyNameV21 =
new HashSet<String>(Arrays.asList(
"BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
"VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
"BDAY", "ROLE", "REV", "UID", "KEY", "MAILER"));
// Though vCard 2.1 specification does not allow "B" encoding, some data may have it.
// We allow it for safety...
private static final HashSet<String> sAvailableEncodingV21 =
new HashSet<String>(Arrays.asList(
"7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B"));
// Used only for parsing END:VCARD.
private String mPreviousLine;
/** The builder to build parsed data */
protected VCardBuilder mBuilder = null;
/** The encoding type */
protected String mEncoding = null;
protected final String sDefaultEncoding = "8BIT";
// Should not directly read a line from this. Use getLine() instead.
protected BufferedReader mReader;
// In some cases, vCard is nested. Currently, we only consider the most interior vCard data.
// See v21_foma_1.vcf in test directory for more information.
private int mNestCount;
// In order to reduce warning message as much as possible, we hold the value which made Logger
// emit a warning message.
protected HashSet<String> mWarningValueMap = new HashSet<String>();
// Just for debugging
private long mTimeTotal;
private long mTimeStartRecord;
private long mTimeEndRecord;
private long mTimeStartProperty;
private long mTimeEndProperty;
private long mTimeParseItems;
private long mTimeParseItem1;
private long mTimeParseItem2;
private long mTimeParseItem3;
private long mTimeHandlePropertyValue1;
private long mTimeHandlePropertyValue2;
private long mTimeHandlePropertyValue3;
/**
* Create a new VCard parser.
*/
public VCardParser_V21() {
super();
}
public VCardParser_V21(VCardSourceDetector detector) {
super();
if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) {
mNestCount = 1;
}
}
/**
* Parse the file at the given position
* vcard_file = [wsls] vcard [wsls]
*/
protected void parseVCardFile() throws IOException, VCardException {
boolean firstReading = true;
while (true) {
if (mCanceled) {
break;
}
if (!parseOneVCard(firstReading)) {
break;
}
firstReading = false;
}
if (mNestCount > 0) {
boolean useCache = true;
for (int i = 0; i < mNestCount; i++) {
readEndVCard(useCache, true);
useCache = false;
}
}
}
protected String getVersion() {
return "2.1";
}
/**
* @return true when the propertyName is a valid property name.
*/
protected boolean isValidPropertyName(String propertyName) {
if (!(sAvailablePropertyNameV21.contains(propertyName.toUpperCase()) ||
propertyName.startsWith("X-")) &&
!mWarningValueMap.contains(propertyName)) {
mWarningValueMap.add(propertyName);
Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName);
}
return true;
}
/**
* @return true when the encoding is a valid encoding.
*/
protected boolean isValidEncoding(String encoding) {
return sAvailableEncodingV21.contains(encoding.toUpperCase());
}
/**
* @return String. It may be null, or its length may be 0
* @throws IOException
*/
protected String getLine() throws IOException {
return mReader.readLine();
}
/**
* @return String with it's length > 0
* @throws IOException
* @throws VCardException when the stream reached end of line
*/
protected String getNonEmptyLine() throws IOException, VCardException {
String line;
while (true) {
line = getLine();
if (line == null) {
throw new VCardException("Reached end of buffer.");
} else if (line.trim().length() > 0) {
return line;
}
}
}
/**
* vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF
* items *CRLF
* "END" [ws] ":" [ws] "VCARD"
*/
private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException {
boolean allowGarbage = false;
if (firstReading) {
if (mNestCount > 0) {
for (int i = 0; i < mNestCount; i++) {
if (!readBeginVCard(allowGarbage)) {
return false;
}
allowGarbage = true;
}
}
}
if (!readBeginVCard(allowGarbage)) {
return false;
}
long start;
if (mBuilder != null) {
start = System.currentTimeMillis();
mBuilder.startRecord("VCARD");
mTimeStartRecord += System.currentTimeMillis() - start;
}
start = System.currentTimeMillis();
parseItems();
mTimeParseItems += System.currentTimeMillis() - start;
readEndVCard(true, false);
if (mBuilder != null) {
start = System.currentTimeMillis();
mBuilder.endRecord();
mTimeEndRecord += System.currentTimeMillis() - start;
}
return true;
}
/**
* @return True when successful. False when reaching the end of line
* @throws IOException
* @throws VCardException
*/
protected boolean readBeginVCard(boolean allowGarbage)
throws IOException, VCardException {
String line;
do {
while (true) {
line = getLine();
if (line == null) {
return false;
} else if (line.trim().length() > 0) {
break;
}
}
String[] strArray = line.split(":", 2);
int length = strArray.length;
// Though vCard 2.1/3.0 specification does not allow lower cases,
// some data may have them, so we allow it (Actually, previous code
// had explicitly allowed "BEGIN:vCard" though there's no example).
//
// TODO: ignore non vCard entry (e.g. vcalendar).
// XXX: Not sure, but according to VDataBuilder.java, vcalendar
// entry
// may be nested. Just seeking "END:SOMETHING" may not be enough.
// e.g.
// BEGIN:VCARD
// ... (Valid. Must parse this)
// END:VCARD
// BEGIN:VSOMETHING
// ... (Must ignore this)
// BEGIN:VSOMETHING2
// ... (Must ignore this)
// END:VSOMETHING2
// ... (Must ignore this!)
// END:VSOMETHING
// BEGIN:VCARD
// ... (Valid. Must parse this)
// END:VCARD
// INVALID_STRING (VCardException should be thrown)
if (length == 2 &&
strArray[0].trim().equalsIgnoreCase("BEGIN") &&
strArray[1].trim().equalsIgnoreCase("VCARD")) {
return true;
} else if (!allowGarbage) {
if (mNestCount > 0) {
mPreviousLine = line;
return false;
} else {
throw new VCardException(
"Expected String \"BEGIN:VCARD\" did not come "
+ "(Instead, \"" + line + "\" came)");
}
}
} while(allowGarbage);
throw new VCardException("Reached where must not be reached.");
}
/**
* The arguments useCache and allowGarbase are usually true and false accordingly when
* this function is called outside this function itself.
*
* @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine()
* is used.
* @param allowGarbage When true, ignore non "END:VCARD" line.
* @throws IOException
* @throws VCardException
*/
protected void readEndVCard(boolean useCache, boolean allowGarbage)
throws IOException, VCardException {
String line;
do {
if (useCache) {
// Though vCard specification does not allow lower cases,
// some data may have them, so we allow it.
line = mPreviousLine;
} else {
while (true) {
line = getLine();
if (line == null) {
throw new VCardException("Expected END:VCARD was not found.");
} else if (line.trim().length() > 0) {
break;
}
}
}
String[] strArray = line.split(":", 2);
if (strArray.length == 2 &&
strArray[0].trim().equalsIgnoreCase("END") &&
strArray[1].trim().equalsIgnoreCase("VCARD")) {
return;
} else if (!allowGarbage) {
throw new VCardException("END:VCARD != \"" + mPreviousLine + "\"");
}
useCache = false;
} while (allowGarbage);
}
/**
* items = *CRLF item
* / item
*/
protected void parseItems() throws IOException, VCardException {
/* items *CRLF item / item */
boolean ended = false;
if (mBuilder != null) {
long start = System.currentTimeMillis();
mBuilder.startProperty();
mTimeStartProperty += System.currentTimeMillis() - start;
}
ended = parseItem();
if (mBuilder != null && !ended) {
long start = System.currentTimeMillis();
mBuilder.endProperty();
mTimeEndProperty += System.currentTimeMillis() - start;
}
while (!ended) {
// follow VCARD ,it wont reach endProperty
if (mBuilder != null) {
long start = System.currentTimeMillis();
mBuilder.startProperty();
mTimeStartProperty += System.currentTimeMillis() - start;
}
ended = parseItem();
if (mBuilder != null && !ended) {
long start = System.currentTimeMillis();
mBuilder.endProperty();
mTimeEndProperty += System.currentTimeMillis() - start;
}
}
}
/**
* item = [groups "."] name [params] ":" value CRLF
* / [groups "."] "ADR" [params] ":" addressparts CRLF
* / [groups "."] "ORG" [params] ":" orgparts CRLF
* / [groups "."] "N" [params] ":" nameparts CRLF
* / [groups "."] "AGENT" [params] ":" vcard CRLF
*/
protected boolean parseItem() throws IOException, VCardException {
mEncoding = sDefaultEncoding;
String line = getNonEmptyLine();
long start = System.currentTimeMillis();
String[] propertyNameAndValue = separateLineAndHandleGroup(line);
if (propertyNameAndValue == null) {
return true;
}
if (propertyNameAndValue.length != 2) {
throw new VCardException("Invalid line \"" + line + "\"");
}
String propertyName = propertyNameAndValue[0].toUpperCase();
String propertyValue = propertyNameAndValue[1];
mTimeParseItem1 += System.currentTimeMillis() - start;
if (propertyName.equals("ADR") ||
propertyName.equals("ORG") ||
propertyName.equals("N")) {
start = System.currentTimeMillis();
handleMultiplePropertyValue(propertyName, propertyValue);
mTimeParseItem3 += System.currentTimeMillis() - start;
return false;
} else if (propertyName.equals("AGENT")) {
handleAgent(propertyValue);
return false;
} else if (isValidPropertyName(propertyName)) {
if (propertyName.equals("BEGIN")) {
if (propertyValue.equals("VCARD")) {
throw new VCardNestedException("This vCard has nested vCard data in it.");
} else {
throw new VCardException("Unknown BEGIN type: " + propertyValue);
}
} else if (propertyName.equals("VERSION") &&
!propertyValue.equals(getVersion())) {
throw new VCardVersionException("Incompatible version: " +
propertyValue + " != " + getVersion());
}
start = System.currentTimeMillis();
handlePropertyValue(propertyName, propertyValue);
mTimeParseItem2 += System.currentTimeMillis() - start;
return false;
}
throw new VCardException("Unknown property name: \"" +
propertyName + "\"");
}
static private final int STATE_GROUP_OR_PROPNAME = 0;
static private final int STATE_PARAMS = 1;
// vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not.
// This is just for safety.
static private final int STATE_PARAMS_IN_DQUOTE = 2;
protected String[] separateLineAndHandleGroup(String line) throws VCardException {
int length = line.length();
int state = STATE_GROUP_OR_PROPNAME;
int nameIndex = 0;
String[] propertyNameAndValue = new String[2];
for (int i = 0; i < length; i++) {
char ch = line.charAt(i);
switch (state) {
case STATE_GROUP_OR_PROPNAME:
if (ch == ':') {
String propertyName = line.substring(nameIndex, i);
if (propertyName.equalsIgnoreCase("END")) {
mPreviousLine = line;
return null;
}
if (mBuilder != null) {
mBuilder.propertyName(propertyName);
}
propertyNameAndValue[0] = propertyName;
if (i < length - 1) {
propertyNameAndValue[1] = line.substring(i + 1);
} else {
propertyNameAndValue[1] = "";
}
return propertyNameAndValue;
} else if (ch == '.') {
String groupName = line.substring(nameIndex, i);
if (mBuilder != null) {
mBuilder.propertyGroup(groupName);
}
nameIndex = i + 1;
} else if (ch == ';') {
String propertyName = line.substring(nameIndex, i);
if (propertyName.equalsIgnoreCase("END")) {
mPreviousLine = line;
return null;
}
if (mBuilder != null) {
mBuilder.propertyName(propertyName);
}
propertyNameAndValue[0] = propertyName;
nameIndex = i + 1;
state = STATE_PARAMS;
}
break;
case STATE_PARAMS:
if (ch == '"') {
state = STATE_PARAMS_IN_DQUOTE;
} else if (ch == ';') {
handleParams(line.substring(nameIndex, i));
nameIndex = i + 1;
} else if (ch == ':') {
handleParams(line.substring(nameIndex, i));
if (i < length - 1) {
propertyNameAndValue[1] = line.substring(i + 1);
} else {
propertyNameAndValue[1] = "";
}
return propertyNameAndValue;
}
break;
case STATE_PARAMS_IN_DQUOTE:
if (ch == '"') {
state = STATE_PARAMS;
}
break;
}
}
throw new VCardException("Invalid line: \"" + line + "\"");
}
/**
* params = ";" [ws] paramlist
* paramlist = paramlist [ws] ";" [ws] param
* / param
* param = "TYPE" [ws] "=" [ws] ptypeval
* / "VALUE" [ws] "=" [ws] pvalueval
* / "ENCODING" [ws] "=" [ws] pencodingval
* / "CHARSET" [ws] "=" [ws] charsetval
* / "LANGUAGE" [ws] "=" [ws] langval
* / "X-" word [ws] "=" [ws] word
* / knowntype
*/
protected void handleParams(String params) throws VCardException {
String[] strArray = params.split("=", 2);
if (strArray.length == 2) {
String paramName = strArray[0].trim();
String paramValue = strArray[1].trim();
if (paramName.equals("TYPE")) {
handleType(paramValue);
} else if (paramName.equals("VALUE")) {
handleValue(paramValue);
} else if (paramName.equals("ENCODING")) {
handleEncoding(paramValue);
} else if (paramName.equals("CHARSET")) {
handleCharset(paramValue);
} else if (paramName.equals("LANGUAGE")) {
handleLanguage(paramValue);
} else if (paramName.startsWith("X-")) {
handleAnyParam(paramName, paramValue);
} else {
throw new VCardException("Unknown type \"" + paramName + "\"");
}
} else {
handleType(strArray[0]);
}
}
/**
* ptypeval = knowntype / "X-" word
*/
protected void handleType(String ptypeval) {
String upperTypeValue = ptypeval;
if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) &&
!mWarningValueMap.contains(ptypeval)) {
mWarningValueMap.add(ptypeval);
Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval);
}
if (mBuilder != null) {
mBuilder.propertyParamType("TYPE");
mBuilder.propertyParamValue(upperTypeValue);
}
}
/**
* pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word
*/
protected void handleValue(String pvalueval) throws VCardException {
if (sKnownValueSet.contains(pvalueval.toUpperCase()) ||
pvalueval.startsWith("X-")) {
if (mBuilder != null) {
mBuilder.propertyParamType("VALUE");
mBuilder.propertyParamValue(pvalueval);
}
} else {
throw new VCardException("Unknown value \"" + pvalueval + "\"");
}
}
/**
* pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word
*/
protected void handleEncoding(String pencodingval) throws VCardException {
if (isValidEncoding(pencodingval) ||
pencodingval.startsWith("X-")) {
if (mBuilder != null) {
mBuilder.propertyParamType("ENCODING");
mBuilder.propertyParamValue(pencodingval);
}
mEncoding = pencodingval;
} else {
throw new VCardException("Unknown encoding \"" + pencodingval + "\"");
}
}
/**
* vCard specification only allows us-ascii and iso-8859-xxx (See RFC 1521),
* but some vCard contains other charset, so we allow them.
*/
protected void handleCharset(String charsetval) {
if (mBuilder != null) {
mBuilder.propertyParamType("CHARSET");
mBuilder.propertyParamValue(charsetval);
}
}
/**
* See also Section 7.1 of RFC 1521
*/
protected void handleLanguage(String langval) throws VCardException {
String[] strArray = langval.split("-");
if (strArray.length != 2) {
throw new VCardException("Invalid Language: \"" + langval + "\"");
}
String tmp = strArray[0];
int length = tmp.length();
for (int i = 0; i < length; i++) {
if (!isLetter(tmp.charAt(i))) {
throw new VCardException("Invalid Language: \"" + langval + "\"");
}
}
tmp = strArray[1];
length = tmp.length();
for (int i = 0; i < length; i++) {
if (!isLetter(tmp.charAt(i))) {
throw new VCardException("Invalid Language: \"" + langval + "\"");
}
}
if (mBuilder != null) {
mBuilder.propertyParamType("LANGUAGE");
mBuilder.propertyParamValue(langval);
}
}
/**
* Mainly for "X-" type. This accepts any kind of type without check.
*/
protected void handleAnyParam(String paramName, String paramValue) {
if (mBuilder != null) {
mBuilder.propertyParamType(paramName);
mBuilder.propertyParamValue(paramValue);
}
}
protected void handlePropertyValue(
String propertyName, String propertyValue) throws
IOException, VCardException {
if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
long start = System.currentTimeMillis();
String result = getQuotedPrintable(propertyValue);
if (mBuilder != null) {
ArrayList<String> v = new ArrayList<String>();
v.add(result);
mBuilder.propertyValues(v);
}
mTimeHandlePropertyValue2 += System.currentTimeMillis() - start;
} else if (mEncoding.equalsIgnoreCase("BASE64") ||
mEncoding.equalsIgnoreCase("B")) {
long start = System.currentTimeMillis();
// It is very rare, but some BASE64 data may be so big that
// OutOfMemoryError occurs. To ignore such cases, use try-catch.
try {
String result = getBase64(propertyValue);
if (mBuilder != null) {
ArrayList<String> v = new ArrayList<String>();
v.add(result);
mBuilder.propertyValues(v);
}
} catch (OutOfMemoryError error) {
Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!");
if (mBuilder != null) {
mBuilder.propertyValues(null);
}
}
mTimeHandlePropertyValue3 += System.currentTimeMillis() - start;
} else {
if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT")
|| mEncoding.equalsIgnoreCase("8BIT")
|| mEncoding.toUpperCase().startsWith("X-"))) {
Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\".");
}
long start = System.currentTimeMillis();
if (mBuilder != null) {
ArrayList<String> v = new ArrayList<String>();
v.add(maybeUnescapeText(propertyValue));
mBuilder.propertyValues(v);
}
mTimeHandlePropertyValue1 += System.currentTimeMillis() - start;
}
}
protected String getQuotedPrintable(String firstString) throws IOException, VCardException {
// Specifically, there may be some padding between = and CRLF.
// See the following:
//
// qp-line := *(qp-segment transport-padding CRLF)
// qp-part transport-padding
// qp-segment := qp-section *(SPACE / TAB) "="
// ; Maximum length of 76 characters
//
// e.g. (from RFC 2045)
// Now's the time =
// for all folk to come=
// to the aid of their country.
if (firstString.trim().endsWith("=")) {
// remove "transport-padding"
int pos = firstString.length() - 1;
while(firstString.charAt(pos) != '=') {
}
StringBuilder builder = new StringBuilder();
builder.append(firstString.substring(0, pos + 1));
builder.append("\r\n");
String line;
while (true) {
line = getLine();
if (line == null) {
throw new VCardException(
"File ended during parsing quoted-printable String");
}
if (line.trim().endsWith("=")) {
// remove "transport-padding"
pos = line.length() - 1;
while(line.charAt(pos) != '=') {
}
builder.append(line.substring(0, pos + 1));
builder.append("\r\n");
} else {
builder.append(line);
break;
}
}
return builder.toString();
} else {
return firstString;
}
}
protected String getBase64(String firstString) throws IOException, VCardException {
StringBuilder builder = new StringBuilder();
builder.append(firstString);
while (true) {
String line = getLine();
if (line == null) {
throw new VCardException(
"File ended during parsing BASE64 binary");
}
if (line.length() == 0) {
break;
}
builder.append(line);
}
return builder.toString();
}
/**
* Mainly for "ADR", "ORG", and "N"
* We do not care the number of strnosemi here.
*
* addressparts = 0*6(strnosemi ";") strnosemi
* ; PO Box, Extended Addr, Street, Locality, Region,
* Postal Code, Country Name
* orgparts = *(strnosemi ";") strnosemi
* ; First is Organization Name,
* remainder are Organization Units.
* nameparts = 0*4(strnosemi ";") strnosemi
* ; Family, Given, Middle, Prefix, Suffix.
* ; Example:Public;John;Q.;Reverend Dr.;III, Esq.
* strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi
* ; To include a semicolon in this string, it must be escaped
* ; with a "\" character.
*
* We are not sure whether we should add "\" CRLF to each value.
* For now, we exclude them.
*/
protected void handleMultiplePropertyValue(
String propertyName, String propertyValue) throws IOException, VCardException {
// vCard 2.1 does not allow QUOTED-PRINTABLE here, but some data have it.
if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
propertyValue = getQuotedPrintable(propertyValue);
}
if (mBuilder != null) {
// TODO: limit should be set in accordance with propertyName?
StringBuilder builder = new StringBuilder();
ArrayList<String> list = new ArrayList<String>();
int length = propertyValue.length();
for (int i = 0; i < length; i++) {
char ch = propertyValue.charAt(i);
if (ch == '\\' && i < length - 1) {
char nextCh = propertyValue.charAt(i + 1);
String unescapedString = maybeUnescape(nextCh);
if (unescapedString != null) {
builder.append(unescapedString);
i++;
} else {
builder.append(ch);
}
} else if (ch == ';') {
list.add(builder.toString());
builder = new StringBuilder();
} else {
builder.append(ch);
}
}
list.add(builder.toString());
mBuilder.propertyValues(list);
}
}
/**
* vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all.
*
* item = ...
* / [groups "."] "AGENT"
* [params] ":" vcard CRLF
* vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF
* items *CRLF "END" [ws] ":" [ws] "VCARD"
*
*/
protected void handleAgent(String propertyValue) throws VCardException {
throw new VCardNotSupportedException("AGENT Property is not supported now.");
/* This is insufficient support. Also, AGENT Property is very rare.
Ignore it for now.
TODO: fix this.
String[] strArray = propertyValue.split(":", 2);
if (!(strArray.length == 2 ||
strArray[0].trim().equalsIgnoreCase("BEGIN") &&
strArray[1].trim().equalsIgnoreCase("VCARD"))) {
throw new VCardException("BEGIN:VCARD != \"" + propertyValue + "\"");
}
parseItems();
readEndVCard();
*/
}
/**
* For vCard 3.0.
*/
protected String maybeUnescapeText(String text) {
return text;
}
/**
* Returns unescaped String if the character should be unescaped. Return null otherwise.
* e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be.
*/
protected String maybeUnescape(char ch) {
// Original vCard 2.1 specification does not allow transformation
// "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of
// this class allowed them, so keep it as is.
if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') {
return String.valueOf(ch);
} else {
return null;
}
}
@Override
public boolean parse(InputStream is, VCardBuilder builder)
throws IOException, VCardException {
return parse(is, VCardConfig.DEFAULT_CHARSET, builder);
}
@Override
public boolean parse(InputStream is, String charset, VCardBuilder builder)
throws IOException, VCardException {
// TODO: make this count error entries instead of just throwing VCardException.
{
// TODO: If we really need to allow only CRLF as line break,
// we will have to develop our own BufferedReader().
final InputStreamReader tmpReader = new InputStreamReader(is, charset);
if (VCardConfig.showPerformanceLog()) {
mReader = new CustomBufferedReader(tmpReader);
} else {
mReader = new BufferedReader(tmpReader);
}
}
mBuilder = builder;
long start = System.currentTimeMillis();
if (mBuilder != null) {
mBuilder.start();
}
parseVCardFile();
if (mBuilder != null) {
mBuilder.end();
}
mTimeTotal += System.currentTimeMillis() - start;
if (VCardConfig.showPerformanceLog()) {
showPerformanceInfo();
}
return true;
}
@Override
public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled)
throws IOException, VCardException {
mCanceled = canceled;
parse(is, charset, builder);
}
private void showPerformanceInfo() {
Log.d(LOG_TAG, "total parsing time: " + mTimeTotal + " ms");
if (mReader instanceof CustomBufferedReader) {
Log.d(LOG_TAG, "total readLine time: " +
((CustomBufferedReader)mReader).getTotalmillisecond() + " ms");
}
Log.d(LOG_TAG, "mTimeStartRecord: " + mTimeStartRecord + " ms");
Log.d(LOG_TAG, "mTimeEndRecord: " + mTimeEndRecord + " ms");
Log.d(LOG_TAG, "mTimeParseItem1: " + mTimeParseItem1 + " ms");
Log.d(LOG_TAG, "mTimeParseItem2: " + mTimeParseItem2 + " ms");
Log.d(LOG_TAG, "mTimeParseItem3: " + mTimeParseItem3 + " ms");
Log.d(LOG_TAG, "mTimeHandlePropertyValue1: " + mTimeHandlePropertyValue1 + " ms");
Log.d(LOG_TAG, "mTimeHandlePropertyValue2: " + mTimeHandlePropertyValue2 + " ms");
Log.d(LOG_TAG, "mTimeHandlePropertyValue3: " + mTimeHandlePropertyValue3 + " ms");
}
private boolean isLetter(char ch) {
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
return true;
}
return false;
}
}
class CustomBufferedReader extends BufferedReader {
private long mTime;
public CustomBufferedReader(Reader in) {
super(in);
}
@Override
public String readLine() throws IOException {
long start = System.currentTimeMillis();
String ret = super.readLine();
long end = System.currentTimeMillis();
mTime += end - start;
return ret;
}
public long getTotalmillisecond() {
return mTime;
}
}

View File

@@ -0,0 +1,306 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import android.pim.vcard.exception.VCardException;
import android.util.Log;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
/**
* This class is used to parse vcard3.0. <br>
* Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426)
*/
public class VCardParser_V30 extends VCardParser_V21 {
private static final String LOG_TAG = "VCardParser_V30";
private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>(
Arrays.asList(
"BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
"VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
"BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1
"NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS",
"SORT-STRING", "CATEGORIES", "PRODID")); // 3.0
// Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety.
private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>(
Arrays.asList("7BIT", "8BIT", "BASE64", "B"));
// Although RFC 2426 specifies some property must not have parameters, we allow it,
// since there may be some careers which violates the RFC...
private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>();
private String mPreviousLine;
@Override
protected String getVersion() {
return "3.0";
}
@Override
protected boolean isValidPropertyName(String propertyName) {
if (!(sAcceptablePropsWithParam.contains(propertyName) ||
acceptablePropsWithoutParam.contains(propertyName) ||
propertyName.startsWith("X-")) &&
!mWarningValueMap.contains(propertyName)) {
mWarningValueMap.add(propertyName);
Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName);
}
return true;
}
@Override
protected boolean isValidEncoding(String encoding) {
return sAcceptableEncodingV30.contains(encoding.toUpperCase());
}
@Override
protected String getLine() throws IOException {
if (mPreviousLine != null) {
String ret = mPreviousLine;
mPreviousLine = null;
return ret;
} else {
return mReader.readLine();
}
}
/**
* vCard 3.0 requires that the line with space at the beginning of the line
* must be combined with previous line.
*/
@Override
protected String getNonEmptyLine() throws IOException, VCardException {
String line;
StringBuilder builder = null;
while (true) {
line = mReader.readLine();
if (line == null) {
if (builder != null) {
return builder.toString();
} else if (mPreviousLine != null) {
String ret = mPreviousLine;
mPreviousLine = null;
return ret;
}
throw new VCardException("Reached end of buffer.");
} else if (line.length() == 0) {
if (builder != null) {
return builder.toString();
} else if (mPreviousLine != null) {
String ret = mPreviousLine;
mPreviousLine = null;
return ret;
}
} else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
if (builder != null) {
// See Section 5.8.1 of RFC 2425 (MIME-DIR document).
// Following is the excerpts from it.
//
// DESCRIPTION:This is a long description that exists on a long line.
//
// Can be represented as:
//
// DESCRIPTION:This is a long description
// that exists on a long line.
//
// It could also be represented as:
//
// DESCRIPTION:This is a long descrip
// tion that exists o
// n a long line.
builder.append(line.substring(1));
} else if (mPreviousLine != null) {
builder = new StringBuilder();
builder.append(mPreviousLine);
mPreviousLine = null;
builder.append(line.substring(1));
} else {
throw new VCardException("Space exists at the beginning of the line");
}
} else {
if (mPreviousLine == null) {
mPreviousLine = line;
if (builder != null) {
return builder.toString();
}
} else {
String ret = mPreviousLine;
mPreviousLine = line;
return ret;
}
}
}
}
/**
* vcard = [group "."] "BEGIN" ":" "VCARD" 1*CRLF
* 1*(contentline)
* ;A vCard object MUST include the VERSION, FN and N types.
* [group "."] "END" ":" "VCARD" 1*CRLF
*/
@Override
protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
// TODO: vCard 3.0 supports group.
return super.readBeginVCard(allowGarbage);
}
@Override
protected void readEndVCard(boolean useCache, boolean allowGarbage)
throws IOException, VCardException {
// TODO: vCard 3.0 supports group.
super.readEndVCard(useCache, allowGarbage);
}
/**
* vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
*/
@Override
protected void handleParams(String params) throws VCardException {
try {
super.handleParams(params);
} catch (VCardException e) {
// maybe IANA type
String[] strArray = params.split("=", 2);
if (strArray.length == 2) {
handleAnyParam(strArray[0], strArray[1]);
} else {
// Must not come here in the current implementation.
throw new VCardException(
"Unknown params value: " + params);
}
}
}
@Override
protected void handleAnyParam(String paramName, String paramValue) {
// vCard 3.0 accept comma-separated multiple values, but
// current PropertyNode does not accept it.
// For now, we do not split the values.
//
// TODO: fix this.
super.handleAnyParam(paramName, paramValue);
}
/**
* vCard 3.0 defines
*
* param = param-name "=" param-value *("," param-value)
* param-name = iana-token / x-name
* param-value = ptext / quoted-string
* quoted-string = DQUOTE QSAFE-CHAR DQUOTE
*/
@Override
protected void handleType(String ptypevalues) {
String[] ptypeArray = ptypevalues.split(",");
mBuilder.propertyParamType("TYPE");
for (String value : ptypeArray) {
int length = value.length();
if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
mBuilder.propertyParamValue(value.substring(1, value.length() - 1));
} else {
mBuilder.propertyParamValue(value);
}
}
}
@Override
protected void handleAgent(String propertyValue) throws VCardException {
// The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.0.
//
// e.g.
// AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
// TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
// ET:jfriday@host.com\nEND:VCARD\n
//
// TODO: fix this.
//
// issue:
// vCard 3.0 also allows this as an example.
//
// AGENT;VALUE=uri:
// CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com
//
// This is not VCARD. Should we support this?
throw new VCardException("AGENT in vCard 3.0 is not supported yet.");
}
/**
* vCard 3.0 does not require two CRLF at the last of BASE64 data.
* It only requires that data should be MIME-encoded.
*/
@Override
protected String getBase64(String firstString) throws IOException, VCardException {
StringBuilder builder = new StringBuilder();
builder.append(firstString);
while (true) {
String line = getLine();
if (line == null) {
throw new VCardException(
"File ended during parsing BASE64 binary");
}
if (line.length() == 0) {
break;
} else if (!line.startsWith(" ") && !line.startsWith("\t")) {
mPreviousLine = line;
break;
}
builder.append(line);
}
return builder.toString();
}
/**
* ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
* ; \\ encodes \, \n or \N encodes newline
* ; \; encodes ;, \, encodes ,
*
* Note: Apple escape ':' into '\:' while does not escape '\'
*/
@Override
protected String maybeUnescapeText(String text) {
StringBuilder builder = new StringBuilder();
int length = text.length();
for (int i = 0; i < length; i++) {
char ch = text.charAt(i);
if (ch == '\\' && i < length - 1) {
char next_ch = text.charAt(++i);
if (next_ch == 'n' || next_ch == 'N') {
builder.append("\r\n");
} else {
builder.append(next_ch);
}
} else {
builder.append(ch);
}
}
return builder.toString();
}
@Override
protected String maybeUnescape(char ch) {
if (ch == 'n' || ch == 'N') {
return "\r\n";
} else {
return String.valueOf(ch);
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Class which tries to detects the source of the vCard from its properties.
* Currently this implementation is very premature.
* @hide
*/
public class VCardSourceDetector implements VCardBuilder {
// Should only be used in package.
static final int TYPE_UNKNOWN = 0;
static final int TYPE_APPLE = 1;
static final int TYPE_JAPANESE_MOBILE_PHONE = 2; // Used in Japanese mobile phones.
static final int TYPE_FOMA = 3; // Used in some Japanese FOMA mobile phones.
static final int TYPE_WINDOWS_MOBILE_JP = 4;
// TODO: Excel, etc.
private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList(
"X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME",
"X-ABADR", "X-ABUID"));
private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList(
"X-GNO", "X-GN", "X-REDUCTION"));
private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList(
"X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC"));
// Note: these signes appears before the signs of the other type (e.g. "X-GN").
// In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES.
private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList(
"X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED",
"X-SD-DESCRIPTION"));
private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE";
private int mType = TYPE_UNKNOWN;
// Some mobile phones (like FOMA) tells us the charset of the data.
private boolean mNeedParseSpecifiedCharset;
private String mSpecifiedCharset;
public void start() {
}
public void end() {
}
public void startRecord(String type) {
}
public void startProperty() {
mNeedParseSpecifiedCharset = false;
}
public void endProperty() {
}
public void endRecord() {
}
public void propertyGroup(String group) {
}
public void propertyName(String name) {
if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) {
mType = TYPE_FOMA;
mNeedParseSpecifiedCharset = true;
return;
}
if (mType != TYPE_UNKNOWN) {
return;
}
if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) {
mType = TYPE_WINDOWS_MOBILE_JP;
} else if (FOMA_SIGNS.contains(name)) {
mType = TYPE_FOMA;
} else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) {
mType = TYPE_JAPANESE_MOBILE_PHONE;
} else if (APPLE_SIGNS.contains(name)) {
mType = TYPE_APPLE;
}
}
public void propertyParamType(String type) {
}
public void propertyParamValue(String value) {
}
public void propertyValues(List<String> values) {
if (mNeedParseSpecifiedCharset && values.size() > 0) {
mSpecifiedCharset = values.get(0);
}
}
int getType() {
return mType;
}
/**
* Return charset String guessed from the source's properties.
* This method must be called after parsing target file(s).
* @return Charset String. Null is returned if guessing the source fails.
*/
public String getEstimatedCharset() {
if (mSpecifiedCharset != null) {
return mSpecifiedCharset;
}
switch (mType) {
case TYPE_WINDOWS_MOBILE_JP:
case TYPE_FOMA:
case TYPE_JAPANESE_MOBILE_PHONE:
return "SHIFT_JIS";
case TYPE_APPLE:
return "UTF-8";
default:
return null;
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard.exception;
public class VCardException extends java.lang.Exception {
/**
* Constructs a VCardException object
*/
public VCardException() {
super();
}
/**
* Constructs a VCardException object
*
* @param message the error message
*/
public VCardException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard.exception;
/**
* VCardException thrown when VCard is nested without VCardParser's being notified.
*/
public class VCardNestedException extends VCardNotSupportedException {
public VCardNestedException() {
super();
}
public VCardNestedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard.exception;
/**
* The exception which tells that the input VCard is probably valid from the view of
* specification but not supported in the current framework for now.
*
* This is a kind of a good news from the view of development.
* It may be good to ask users to send a report with the VCard example
* for the future development.
*/
public class VCardNotSupportedException extends VCardException {
public VCardNotSupportedException() {
super();
}
public VCardNotSupportedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.pim.vcard.exception;
/**
* VCardException used only when the version of the vCard is different.
*/
public class VCardVersionException extends VCardException {
public VCardVersionException() {
super();
}
public VCardVersionException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,5 @@
<HTML>
<BODY>
{@hide}
</BODY>
</HTML>

View File

@@ -0,0 +1,5 @@
<HTML>
<BODY>
{@hide}
</BODY>
</HTML>

View File

@@ -0,0 +1,3 @@
BEGIN:VCARD
N:Ando;Roid;
END:VCARD

View File

@@ -0,0 +1,3 @@
BEGIN:VCARD
FN:Ando Roid
END:VCARD

View File

@@ -0,0 +1,322 @@
/*
* 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 com.android.unit_tests.vcard;
import android.content.ContentValues;
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;
/**
* @hide old class just for test
*/
public class PropertyNode {
public String propName;
public String propValue;
public List<String> propValue_vector;
/** Store value as byte[],after decode.
* Used when propValue is encoded by something like BASE64, QUOTED-PRINTABLE, etc.
*/
public byte[] propValue_bytes;
/** param store: key=paramType, value=paramValue
* Note that currently PropertyNode class does not support multiple param-values
* defined in vCard 3.0 (See also RFC 2426). multiple-values are stored as
* one String value like "A,B", not ["A", "B"]...
* TODO: fix this.
*/
public ContentValues paramMap;
/** Only for TYPE=??? param store. */
public Set<String> paramMap_TYPE;
/** Store group values. Used only in VCard. */
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>();
}
public PropertyNode(
String propName, String propValue, List<String> propValue_vector,
byte[] propValue_bytes, ContentValues paramMap, Set<String> paramMap_TYPE,
Set<String> propGroupSet) {
if (propName != null) {
this.propName = propName;
} else {
this.propName = "";
}
if (propValue != null) {
this.propValue = propValue;
} else {
this.propValue = "";
}
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;
} else {
this.paramMap = new ContentValues();
}
if (paramMap_TYPE != null) {
this.paramMap_TYPE = paramMap_TYPE;
} else {
this.paramMap_TYPE = new HashSet<String>();
}
if (propGroupSet != null) {
this.propGroupSet = propGroupSet;
} else {
this.propGroupSet = new HashSet<String>();
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PropertyNode)) {
return false;
}
PropertyNode node = (PropertyNode)obj;
if (propName == null || !propName.equals(node.propName)) {
return false;
} else if (!paramMap.equals(node.paramMap)) {
return false;
} else if (!paramMap_TYPE.equals(node.paramMap_TYPE)) {
return false;
} else if (!propGroupSet.equals(node.propGroupSet)) {
return false;
}
if (propValue_bytes != null && Arrays.equals(propValue_bytes, node.propValue_bytes)) {
return true;
} else {
// Log.d("@@@", propValue + ", " + node.propValue);
if (!propValue.equals(node.propValue)) {
return false;
}
// The value in propValue_vector is not decoded even if it should be
// 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.
return (propValue_vector.equals(node.propValue_vector) ||
propValue_vector.size() == 1 ||
node.propValue_vector.size() == 1);
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("propName: ");
builder.append(propName);
builder.append(", paramMap: ");
builder.append(paramMap.toString());
builder.append(", propmMap_TYPE: ");
builder.append(paramMap_TYPE.toString());
builder.append(", propGroupSet: ");
builder.append(propGroupSet.toString());
if (propValue_vector != null && propValue_vector.size() > 1) {
builder.append(", propValue_vector size: ");
builder.append(propValue_vector.size());
}
if (propValue_bytes != null) {
builder.append(", propValue_bytes size: ");
builder.append(propValue_bytes.length);
}
builder.append(", propValue: ");
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;
}
}

View File

@@ -14,36 +14,130 @@
* limitations under the License.
*/
package com.android.unit_tests;
package com.android.unit_tests.vcard;
import android.content.ContentValues;
import android.syncml.pim.PropertyNode;
import android.syncml.pim.VDataBuilder;
import android.syncml.pim.VNode;
import android.syncml.pim.vcard.VCardException;
import android.syncml.pim.vcard.VCardParser_V21;
import android.syncml.pim.vcard.VCardParser_V30;
import android.pim.vcard.ContactStruct;
import android.pim.vcard.EntryHandler;
import android.pim.vcard.VCardBuilder;
import android.pim.vcard.VCardBuilderCollection;
import android.pim.vcard.VCardConfig;
import android.pim.vcard.VCardDataBuilder;
import android.pim.vcard.VCardParser;
import android.pim.vcard.VCardParser_V21;
import android.pim.vcard.VCardParser_V30;
import android.pim.vcard.exception.VCardException;
import android.test.AndroidTestCase;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Vector;
import java.util.List;
import java.util.Map;
public class VCardTests extends AndroidTestCase {
// TODO: Use EntityIterator, which is added in Eclair.
private static class EntryHolder implements EntryHandler {
public List<ContactStruct> contacts = new ArrayList<ContactStruct>();
public void onEntryCreated(ContactStruct contactStruct) {
contacts.add(contactStruct);
}
public void onFinal() {
}
}
static void verify(ContactStruct expected, ContactStruct actual) {
if (!equalsString(expected.getName(), actual.getName())) {
fail(String.format("Names do not equal: \"%s\" != \"%s\"",
expected.getName(), actual.getName()));
}
if (!equalsString(
expected.getPhoneticName(), actual.getPhoneticName())) {
fail(String.format("Phonetic names do not equal: \"%s\" != \"%s\"",
expected.getPhoneticName(), actual.getPhoneticName()));
}
{
final byte[] expectedPhotoBytes = expected.getPhotoBytes();
final byte[] actualPhotoBytes = actual.getPhotoBytes();
if (!((expectedPhotoBytes == null && actualPhotoBytes == null) ||
Arrays.equals(expectedPhotoBytes, actualPhotoBytes))) {
fail("photoBytes is not equal.");
}
}
verifyInternal(expected.getNotes(), actual.getNotes(), "notes");
verifyInternal(expected.getPhoneList(), actual.getPhoneList(), "phones");
verifyInternal(expected.getContactMethodList(), actual.getContactMethodList(),
"contact lists");
verifyInternal(expected.getOrganizationList(), actual.getOrganizationList(),
"organizations");
{
final Map<String, List<String>> expectedMap =
expected.getExtensionMap();
final Map<String, List<String>> actualMap =
actual.getExtensionMap();
if (verifySize((expectedMap == null ? 0 : expectedMap.size()),
(actualMap == null ? 0 : actualMap.size()), "extensions") > 0) {
for (String key : expectedMap.keySet()) {
if (!actualMap.containsKey(key)) {
fail(String.format(
"Actual does not have %s extension while expected has",
key));
}
final List<String> expectedList = expectedMap.get(key);
final List<String> actualList = actualMap.get(key);
verifyInternal(expectedList, actualList,
String.format("extension \"%s\"", key));
}
}
}
}
private static boolean equalsString(String a, String b) {
if (a == null || a.length() == 0) {
return b == null || b.length() == 0;
} else {
return a.equals(b);
}
}
private static int verifySize(int expectedSize, int actualSize, String name) {
if (expectedSize != actualSize) {
fail(String.format("Size of %s is different: %d != %d",
name, expectedSize, actualSize));
}
return expectedSize;
}
private static <T> void verifyInternal(final List<T> expected, final List<T> actual,
String name) {
if(verifySize((expected == null ? 0 : expected.size()),
(actual == null ? 0 : actual.size()), name) > 0) {
int size = expected.size();
for (int i = 0; i < size; i++) {
final T expectedObj = expected.get(i);
final T actualObj = actual.get(i);
if (!expected.equals(actual)) {
fail(String.format("The %i %s are different: %s != %s",
i, name, expectedObj, actualObj));
}
}
}
}
private class PropertyNodesVerifier {
private HashMap<String, Vector<PropertyNode>> mPropertyNodeMap;
private HashMap<String, ArrayList<PropertyNode>> mPropertyNodeMap;
public PropertyNodesVerifier(PropertyNode... nodes) {
mPropertyNodeMap = new HashMap<String, Vector<PropertyNode>>();
mPropertyNodeMap = new HashMap<String, ArrayList<PropertyNode>>();
for (PropertyNode propertyNode : nodes) {
String propName = propertyNode.propName;
Vector<PropertyNode> expectedNodes =
ArrayList<PropertyNode> expectedNodes =
mPropertyNodeMap.get(propName);
if (expectedNodes == null) {
expectedNodes = new Vector<PropertyNode>();
expectedNodes = new ArrayList<PropertyNode>();
mPropertyNodeMap.put(propName, expectedNodes);
}
expectedNodes.add(propertyNode);
@@ -53,7 +147,7 @@ public class VCardTests extends AndroidTestCase {
public void verify(VNode vnode) {
for (PropertyNode propertyNode : vnode.propList) {
String propName = propertyNode.propName;
Vector<PropertyNode> nodes = mPropertyNodeMap.get(propName);
ArrayList<PropertyNode> nodes = mPropertyNodeMap.get(propName);
if (nodes == null) {
fail("Unexpected propName \"" + propName + "\" exists.");
}
@@ -81,8 +175,8 @@ public class VCardTests extends AndroidTestCase {
}
}
if (mPropertyNodeMap.size() != 0) {
Vector<String> expectedProps = new Vector<String>();
for (Vector<PropertyNode> nodes : mPropertyNodeMap.values()) {
ArrayList<String> expectedProps = new ArrayList<String>();
for (ArrayList<PropertyNode> nodes : mPropertyNodeMap.values()) {
for (PropertyNode node : nodes) {
expectedProps.add(node.propName);
}
@@ -93,25 +187,82 @@ public class VCardTests extends AndroidTestCase {
}
}
public void testV21SimpleCase() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple);
public void testV21SimpleCase1_1() throws IOException, VCardException {
VCardParser parser = new VCardParser_V21();
VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH);
EntryHolder holder = new EntryHolder();
builder.addEntryHandler(holder);
InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_1);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
assertEquals(1, builder.vNodeList.size());
assertEquals(1, holder.contacts.size());
verify(new ContactStruct("Roid Ando", null,
null, null, null, null, null, null),
holder.contacts.get(0));
}
public void testV21SimpleCase1_2() throws IOException, VCardException {
VCardParser parser = new VCardParser_V21();
VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_JAPANESE);
EntryHolder holder = new EntryHolder();
builder.addEntryHandler(holder);
InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_1);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
assertEquals(1, holder.contacts.size());
verify(new ContactStruct("Ando Roid", null,
null, null, null, null, null, null),
holder.contacts.get(0));
}
public void testV21SimpleCase2() throws IOException, VCardException {
VCardParser parser = new VCardParser_V21();
VCardDataBuilder builder = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH);
EntryHolder holder = new EntryHolder();
builder.addEntryHandler(holder);
InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_2);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
assertEquals(1, holder.contacts.size());
verify(new ContactStruct("Ando Roid", null,
null, null, null, null, null, null),
holder.contacts.get(0));
}
public void testV21SimpleCase3() throws IOException, VCardException {
VCardParser parser = new VCardParser_V21();
VCardDataBuilder builder1 = new VCardDataBuilder(VCardConfig.NAME_ORDER_TYPE_ENGLISH);
EntryHolder holder = new EntryHolder();
builder1.addEntryHandler(holder);
VNodeBuilder builder2 = new VNodeBuilder();
VCardBuilderCollection collection =
new VCardBuilderCollection(
new ArrayList<VCardBuilder>(Arrays.asList(builder1, builder2)));
InputStream is = getContext().getResources().openRawResource(R.raw.v21_simple_3);
assertEquals(true, parser.parse(is,"ISO-8859-1", collection));
is.close();
assertEquals(1, builder2.vNodeList.size());
VNode vnode = builder2.vNodeList.get(0);
PropertyNodesVerifier verifier = new PropertyNodesVerifier(
new PropertyNode("N", "Ando;Roid;",
Arrays.asList("Ando", "Roid", ""),
null, null, null, null),
new PropertyNode("FN", "Ando Roid",
null, null, null, null, null));
verifier.verify(builder.vNodeList.get(0));
verifier.verify(vnode);
// FN is prefered.
assertEquals(1, holder.contacts.size());
ContactStruct actual = holder.contacts.get(0);
verify(new ContactStruct("Ando Roid", null,
null, null, null, null, null, null),
actual);
}
public void testV21BackslashCase() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_backslash);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
@@ -129,7 +280,7 @@ public class VCardTests extends AndroidTestCase {
public void testV21ComplicatedCase() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_complicated);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
@@ -585,7 +736,7 @@ public class VCardTests extends AndroidTestCase {
public void testV21Japanese1() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_japanese_1);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
@@ -616,7 +767,7 @@ public class VCardTests extends AndroidTestCase {
public void testV21Japanese2() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_japanese_2);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
@@ -660,7 +811,7 @@ public class VCardTests extends AndroidTestCase {
public void testV21MultipleEntryCase() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V21();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v21_multiple_entry);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();
@@ -741,7 +892,7 @@ public class VCardTests extends AndroidTestCase {
public void testV30SimpleCase() throws IOException, VCardException {
VCardParser_V21 parser = new VCardParser_V30();
VDataBuilder builder = new VDataBuilder();
VNodeBuilder builder = new VNodeBuilder();
InputStream is = getContext().getResources().openRawResource(R.raw.v30_simple);
assertEquals(true, parser.parse(is,"ISO-8859-1", builder));
is.close();

View File

@@ -0,0 +1,30 @@
/*
* 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 com.android.unit_tests.vcard;
import java.util.ArrayList;
/**
* @hide old class. Just for testing
*/
public class VNode {
public String VName;
public ArrayList<PropertyNode> propList = new ArrayList<PropertyNode>();
/** 0:parse over. 1:parsing. */
public int parseStatus = 1;
}

View File

@@ -0,0 +1,313 @@
/*
* 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 com.android.unit_tests.vcard;
import android.content.ContentValues;
import android.pim.vcard.VCardBuilder;
import android.pim.vcard.VCardConfig;
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;
/**
* Store the parse result to custom datastruct: VNode, PropertyNode
* Maybe several vcard instance, so use vNodeList to store.
* VNode: standy by a vcard instance.
* PropertyNode: standy by a property line of a card.
* @hide old class, just for testing use
*/
public class VNodeBuilder implements VCardBuilder {
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 TARGET_CHARSET = "UTF-8";
/** type=VNode */
public List<VNode> vNodeList = new ArrayList<VNode>();
private int mNodeListPos = 0;
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;
public VNodeBuilder() {
this(VCardConfig.DEFAULT_CHARSET, TARGET_CHARSET, false);
}
public VNodeBuilder(String charset, boolean strictLineBreakParsing) {
this(null, charset, strictLineBreakParsing);
}
/**
* @hide sourceCharset is temporal.
*/
public VNodeBuilder(String sourceCharset, String targetCharset,
boolean strictLineBreakParsing) {
if (sourceCharset != null) {
mSourceCharset = sourceCharset;
} else {
mSourceCharset = VCardConfig.DEFAULT_CHARSET;
}
if (targetCharset != null) {
mTargetCharset = targetCharset;
} else {
mTargetCharset = TARGET_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;
mCurrentVNode = vNodeList.get(mNodeListPos);
}
public void endRecord() {
VNode endNode = vNodeList.get(mNodeListPos);
endNode.parseStatus = 0;
while(mNodeListPos > 0){
mNodeListPos--;
if((vNodeList.get(mNodeListPos)).parseStatus == 1)
break;
}
mCurrentVNode = vNodeList.get(mNodeListPos);
}
public void startProperty() {
mCurrentPropNode = new PropertyNode();
}
public void endProperty() {
mCurrentVNode.propList.add(mCurrentPropNode);
}
public void propertyName(String name) {
mCurrentPropNode.propName = name;
}
// Used only in VCard.
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 null;
}
}
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 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);
}
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 "";
}
}
public String getResult(){
return null;
}
}