VCardEntryConstructor.java revision 58610106ce61adad9b1caa1fe9f7925c3e938bab
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.vcard;
17
18import android.accounts.Account;
19import android.text.TextUtils;
20import android.util.Base64;
21import android.util.CharsetUtils;
22import android.util.Log;
23
24import java.io.UnsupportedEncodingException;
25import java.nio.ByteBuffer;
26import java.nio.charset.Charset;
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.List;
30
31/**
32 * <p>
33 * The {@link VCardInterpreter} implementation which enables {@link VCardEntryHandler} objects
34 * to easily handle each vCard entry.
35 * </p>
36 * <p>
37 * This class understand details inside vCard and translates it to {@link VCardEntry}.
38 * Then the class throw it to {@link VCardEntryHandler} registered via
39 * {@link #addEntryHandler(VCardEntryHandler)}, so that all those registered objects
40 * are able to handle the {@link VCardEntry} object.
41 * </p>
42 * <p>
43 * If you want to know the detail inside vCard, it would be better to implement
44 * {@link VCardInterpreter} directly, instead of relying on this class and
45 * {@link VCardEntry} created by the object.
46 * </p>
47 */
48public class VCardEntryConstructor implements VCardInterpreter {
49    private static String LOG_TAG = "VCardEntryConstructor";
50
51    private VCardEntry.Property mCurrentProperty = new VCardEntry.Property();
52    private VCardEntry mCurrentVCardEntry;
53    private String mParamType;
54
55    // The charset using which {@link VCardInterpreter} parses the text.
56    // Each String is first decoded into binary stream with this charset, and encoded back
57    // to "target charset", which may be explicitly specified by the vCard with "CHARSET"
58    // property or implicitly mentioned by its version (e.g. vCard 3.0 recommends UTF-8).
59    private final String mSourceCharset;
60
61    private final boolean mStrictLineBreaking;
62    private final int mVCardType;
63    private final Account mAccount;
64
65    // For measuring performance.
66    private long mTimePushIntoContentResolver;
67
68    private final List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>();
69
70    public VCardEntryConstructor() {
71        this(VCardConfig.VCARD_TYPE_V21_GENERIC, null);
72    }
73
74    public VCardEntryConstructor(final int vcardType) {
75        this(vcardType, null, null, false);
76    }
77
78    public VCardEntryConstructor(final int vcardType, final Account account) {
79        this(vcardType, account, null, false);
80    }
81
82    public VCardEntryConstructor(final int vcardType, final Account account,
83            final String inputCharset) {
84        this(vcardType, account, inputCharset, false);
85    }
86
87    /**
88     * @hide Just for testing.
89     */
90    public VCardEntryConstructor(final int vcardType, final Account account,
91            final String inputCharset, final boolean strictLineBreakParsing) {
92        if (inputCharset != null) {
93            mSourceCharset = inputCharset;
94        } else {
95            mSourceCharset = VCardConfig.DEFAULT_INTERMEDIATE_CHARSET;
96        }
97        mStrictLineBreaking = strictLineBreakParsing;
98        mVCardType = vcardType;
99        mAccount = account;
100    }
101
102    public void addEntryHandler(VCardEntryHandler entryHandler) {
103        mEntryHandlers.add(entryHandler);
104    }
105
106    public void start() {
107        for (VCardEntryHandler entryHandler : mEntryHandlers) {
108            entryHandler.onStart();
109        }
110    }
111
112    public void end() {
113        for (VCardEntryHandler entryHandler : mEntryHandlers) {
114            entryHandler.onEnd();
115        }
116    }
117
118    public void clear() {
119        mCurrentVCardEntry = null;
120        mCurrentProperty = new VCardEntry.Property();
121    }
122
123    public void startEntry() {
124        if (mCurrentVCardEntry != null) {
125            Log.e(LOG_TAG, "Nested VCard code is not supported now.");
126        }
127        mCurrentVCardEntry = new VCardEntry(mVCardType, mAccount);
128    }
129
130    public void endEntry() {
131        mCurrentVCardEntry.consolidateFields();
132        for (VCardEntryHandler entryHandler : mEntryHandlers) {
133            entryHandler.onEntryCreated(mCurrentVCardEntry);
134        }
135        mCurrentVCardEntry = null;
136    }
137
138    public void startProperty() {
139        mCurrentProperty.clear();
140    }
141
142    public void endProperty() {
143        mCurrentVCardEntry.addProperty(mCurrentProperty);
144    }
145
146    public void propertyName(String name) {
147        mCurrentProperty.setPropertyName(name);
148    }
149
150    public void propertyGroup(String group) {
151    }
152
153    public void propertyParamType(String type) {
154        if (mParamType != null) {
155            Log.e(LOG_TAG, "propertyParamType() is called more than once " +
156                    "before propertyParamValue() is called");
157        }
158        mParamType = type;
159    }
160
161    public void propertyParamValue(String value) {
162        if (mParamType == null) {
163            // From vCard 2.1 specification. vCard 3.0 formally does not allow this case.
164            mParamType = "TYPE";
165        }
166        mCurrentProperty.addParameter(mParamType, value);
167        mParamType = null;
168    }
169
170    private static String encodeToSystemCharset(String originalString,
171            String sourceCharset, String targetCharset) {
172        if (sourceCharset.equalsIgnoreCase(targetCharset)) {
173            return originalString;
174        }
175        final Charset charset = Charset.forName(sourceCharset);
176        final ByteBuffer byteBuffer = charset.encode(originalString);
177        // byteBuffer.array() "may" return byte array which is larger than
178        // byteBuffer.remaining(). Here, we keep on the safe side.
179        final byte[] bytes = new byte[byteBuffer.remaining()];
180        byteBuffer.get(bytes);
181        try {
182            String ret = new String(bytes, targetCharset);
183            return ret;
184        } catch (UnsupportedEncodingException e) {
185            Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
186            return null;
187        }
188    }
189
190    private String handleOneValue(String value,
191            String sourceCharset, String targetCharset, String encoding) {
192        // It is possible when some of multiple values are empty.
193        // e.g. N:;a;;; -> values are "", "a", "", "", and "".
194        if (TextUtils.isEmpty(value)) {
195            return "";
196        }
197
198        if (encoding != null) {
199            if (encoding.equals("BASE64") || encoding.equals("B")) {
200                mCurrentProperty.setPropertyBytes(Base64.decode(value.getBytes(), Base64.DEFAULT));
201                return value;
202            } else if (encoding.equals("QUOTED-PRINTABLE")) {
203                return VCardUtils.parseQuotedPrintable(
204                        value, mStrictLineBreaking, sourceCharset, targetCharset);
205            }
206            Log.w(LOG_TAG, "Unknown encoding. Fall back to default.");
207        }
208
209        // Just translate the charset of a given String from inputCharset to a system one.
210        return encodeToSystemCharset(value, sourceCharset, targetCharset);
211    }
212
213    public void propertyValues(List<String> values) {
214        if (values == null || values.isEmpty()) {
215            return;
216        }
217
218        final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET");
219        final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING");
220        final String encoding =
221            ((encodingCollection != null) ? encodingCollection.iterator().next() : null);
222        String targetCharset = CharsetUtils.nameForDefaultVendor(
223                ((charsetCollection != null) ? charsetCollection.iterator().next() : null));
224        if (TextUtils.isEmpty(targetCharset)) {
225            targetCharset = VCardConfig.DEFAULT_IMPORT_CHARSET;
226        }
227
228        for (final String value : values) {
229            mCurrentProperty.addToPropertyValueList(
230                    handleOneValue(value, mSourceCharset, targetCharset, encoding));
231        }
232    }
233
234    /**
235     * @hide
236     */
237    public void showPerformanceInfo() {
238        Log.d(LOG_TAG, "time for insert ContactStruct to database: " +
239                mTimePushIntoContentResolver + " ms");
240    }
241}
242