blob: 290ca2bfec14d86512d5b427e11c136687c5f3a5 [file] [log] [blame]
/*
* 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.accounts.Account;
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;
public class VCardEntryConstructor implements VCardInterpreter {
private static String LOG_TAG = "VCardEntryConstructor";
/**
* If there's no other information available, this class uses this charset for encoding
* byte arrays to String.
*/
/* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8";
private VCardEntry.Property mCurrentProperty = new VCardEntry.Property();
private VCardEntry mCurrentContactStruct;
private String mParamType;
/**
* The charset using which {@link VCardInterpreter} parses the text.
*/
private String mInputCharset;
/**
* The charset with which byte array is encoded to String.
*/
final private String mCharsetForDecodedBytes;
final private boolean mStrictLineBreakParsing;
final private int mVCardType;
final private Account mAccount;
/** For measuring performance. */
private long mTimePushIntoContentResolver;
final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>();
public VCardEntryConstructor() {
this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null);
}
public VCardEntryConstructor(final int vcardType) {
this(null, null, false, vcardType, null);
}
public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing,
final int vcardType, final Account account) {
this(null, charset, strictLineBreakParsing, vcardType, account);
}
public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes,
final boolean strictLineBreakParsing, final int vcardType,
final Account account) {
if (inputCharset != null) {
mInputCharset = inputCharset;
} else {
mInputCharset = VCardConfig.DEFAULT_CHARSET;
}
if (charsetForDetodedBytes != null) {
mCharsetForDecodedBytes = charsetForDetodedBytes;
} else {
mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES;
}
mStrictLineBreakParsing = strictLineBreakParsing;
mVCardType = vcardType;
mAccount = account;
}
public void addEntryHandler(VCardEntryHandler entryHandler) {
mEntryHandlers.add(entryHandler);
}
public void start() {
for (VCardEntryHandler entryHandler : mEntryHandlers) {
entryHandler.onStart();
}
}
public void end() {
for (VCardEntryHandler entryHandler : mEntryHandlers) {
entryHandler.onEnd();
}
}
/**
* Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}.
*/
public void clear() {
mCurrentContactStruct = null;
mCurrentProperty = new VCardEntry.Property();
}
/**
* Assume that VCard is not nested. In other words, this code does not accept
*/
public void startEntry() {
if (mCurrentContactStruct != null) {
Log.e(LOG_TAG, "Nested VCard code is not supported now.");
}
mCurrentContactStruct = new VCardEntry(mVCardType, mAccount);
}
public void endEntry() {
mCurrentContactStruct.consolidateFields();
for (VCardEntryHandler 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) {
}
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) {
// From vCard 2.1 specification. vCard 3.0 formally does not allow this case.
mParamType = "TYPE";
}
mCurrentProperty.addParameter(mParamType, value);
mParamType = null;
}
private String encodeString(String originalString, String charsetForDecodedBytes) {
if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) {
return originalString;
}
Charset charset = Charset.forName(mInputCharset);
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, charsetForDecodedBytes);
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes);
return null;
}
}
private String handleOneValue(String value, String charsetForDecodedBytes, 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(mInputCharset);
} catch (UnsupportedEncodingException e1) {
Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset);
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, charsetForDecodedBytes);
} catch (UnsupportedEncodingException e) {
Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes);
return new String(bytes);
}
}
// Unknown encoding. Fall back to default.
}
return encodeString(value, charsetForDecodedBytes);
}
public void propertyValues(List<String> values) {
if (values == null || values.isEmpty()) {
return;
}
final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET");
final String charset =
((charsetCollection != null) ? charsetCollection.iterator().next() : null);
final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING");
final String encoding =
((encodingCollection != null) ? encodingCollection.iterator().next() : null);
String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset);
if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) {
charsetForDecodedBytes = mCharsetForDecodedBytes;
}
for (final String value : values) {
mCurrentProperty.addToPropertyValueList(
handleOneValue(value, charsetForDecodedBytes, encoding));
}
}
public void showPerformanceInfo() {
Log.d(LOG_TAG, "time for insert ContactStruct to database: " +
mTimePushIntoContentResolver + " ms");
}
}