| /* |
| * 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"); |
| } |
| } |