VCardParserImpl_V21.java revision 4199c54c527330ac01699b176e7bca186a3aa3a4
1/*
2 * Copyright (C) 2010 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.util.Log;
19
20import com.android.vcard.exception.VCardAgentNotSupportedException;
21import com.android.vcard.exception.VCardException;
22import com.android.vcard.exception.VCardInvalidCommentLineException;
23import com.android.vcard.exception.VCardInvalidLineException;
24import com.android.vcard.exception.VCardNestedException;
25import com.android.vcard.exception.VCardVersionException;
26
27import java.io.BufferedReader;
28import java.io.IOException;
29import java.io.InputStream;
30import java.io.InputStreamReader;
31import java.io.Reader;
32import java.util.ArrayList;
33import java.util.HashSet;
34import java.util.Set;
35
36/**
37 * <p>
38 * Basic implementation achieving vCard parsing. Based on vCard 2.1,
39 * </p>
40 * @hide
41 */
42/* package */ class VCardParserImpl_V21 {
43    private static final String LOG_TAG = "VCardParserImpl_V21";
44
45    private static final class CustomBufferedReader extends BufferedReader {
46        private long mTime;
47
48        public CustomBufferedReader(Reader in) {
49            super(in);
50        }
51
52        @Override
53        public String readLine() throws IOException {
54            long start = System.currentTimeMillis();
55            String ret = super.readLine();
56            long end = System.currentTimeMillis();
57            mTime += end - start;
58            return ret;
59        }
60
61        public long getTotalmillisecond() {
62            return mTime;
63        }
64    }
65
66    private static final String DEFAULT_ENCODING = "8BIT";
67
68    protected boolean mCanceled;
69    protected VCardInterpreter mInterpreter;
70
71    protected final String mIntermediateCharset;
72
73    /**
74     * <p>
75     * The encoding type for deconding byte streams. This member variable is
76     * reset to a default encoding every time when a new item comes.
77     * </p>
78     * <p>
79     * "Encoding" in vCard is different from "Charset". It is mainly used for
80     * addresses, notes, images. "7BIT", "8BIT", "BASE64", and
81     * "QUOTED-PRINTABLE" are known examples.
82     * </p>
83     */
84    protected String mCurrentEncoding;
85
86    /**
87     * <p>
88     * The reader object to be used internally.
89     * </p>
90     * <p>
91     * Developers should not directly read a line from this object. Use
92     * getLine() unless there some reason.
93     * </p>
94     */
95    protected BufferedReader mReader;
96
97    /**
98     * <p>
99     * Set for storing unkonwn TYPE attributes, which is not acceptable in vCard
100     * specification, but happens to be seen in real world vCard.
101     * </p>
102     */
103    protected final Set<String> mUnknownTypeSet = new HashSet<String>();
104
105    /**
106     * <p>
107     * Set for storing unkonwn VALUE attributes, which is not acceptable in
108     * vCard specification, but happens to be seen in real world vCard.
109     * </p>
110     */
111    protected final Set<String> mUnknownValueSet = new HashSet<String>();
112
113
114    // In some cases, vCard is nested. Currently, we only consider the most
115    // interior vCard data.
116    // See v21_foma_1.vcf in test directory for more information.
117    // TODO: Don't ignore by using count, but read all of information outside vCard.
118    private int mNestCount;
119
120    // Used only for parsing END:VCARD.
121    private String mPreviousLine;
122
123    // For measuring performance.
124    private long mTimeTotal;
125    private long mTimeReadStartRecord;
126    private long mTimeReadEndRecord;
127    private long mTimeStartProperty;
128    private long mTimeEndProperty;
129    private long mTimeParseItems;
130    private long mTimeParseLineAndHandleGroup;
131    private long mTimeParsePropertyValues;
132    private long mTimeParseAdrOrgN;
133    private long mTimeHandleMiscPropertyValue;
134    private long mTimeHandleQuotedPrintable;
135    private long mTimeHandleBase64;
136
137    public VCardParserImpl_V21() {
138        this(VCardConfig.VCARD_TYPE_DEFAULT);
139    }
140
141    public VCardParserImpl_V21(int vcardType) {
142        if ((vcardType & VCardConfig.FLAG_TORELATE_NEST) != 0) {
143            mNestCount = 1;
144        }
145
146        mIntermediateCharset =  VCardConfig.DEFAULT_INTERMEDIATE_CHARSET;
147    }
148
149    /**
150     * <p>
151     * Parses the file at the given position.
152     * </p>
153     */
154    // <pre class="prettyprint">vcard_file = [wsls] vcard [wsls]</pre>
155    protected void parseVCardFile() throws IOException, VCardException {
156        boolean readingFirstFile = true;
157        while (true) {
158            if (mCanceled) {
159                break;
160            }
161            if (!parseOneVCard(readingFirstFile)) {
162                break;
163            }
164            readingFirstFile = false;
165        }
166
167        if (mNestCount > 0) {
168            boolean useCache = true;
169            for (int i = 0; i < mNestCount; i++) {
170                readEndVCard(useCache, true);
171                useCache = false;
172            }
173        }
174    }
175
176    /**
177     * @return true when a given property name is a valid property name.
178     */
179    protected boolean isValidPropertyName(final String propertyName) {
180        if (!(getKnownPropertyNameSet().contains(propertyName.toUpperCase()) ||
181                propertyName.startsWith("X-"))
182                && !mUnknownTypeSet.contains(propertyName)) {
183            mUnknownTypeSet.add(propertyName);
184            Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName);
185        }
186        return true;
187    }
188
189    /**
190     * @return String. It may be null, or its length may be 0
191     * @throws IOException
192     */
193    protected String getLine() throws IOException {
194        return mReader.readLine();
195    }
196
197    /**
198     * @return String with it's length > 0
199     * @throws IOException
200     * @throws VCardException when the stream reached end of line
201     */
202    protected String getNonEmptyLine() throws IOException, VCardException {
203        String line;
204        while (true) {
205            line = getLine();
206            if (line == null) {
207                throw new VCardException("Reached end of buffer.");
208            } else if (line.trim().length() > 0) {
209                return line;
210            }
211        }
212    }
213
214    /*
215     * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF
216     *         items *CRLF
217     *         "END" [ws] ":" [ws] "VCARD"
218     */
219    private boolean parseOneVCard(boolean firstRead) throws IOException, VCardException {
220        boolean allowGarbage = false;
221        if (firstRead) {
222            if (mNestCount > 0) {
223                for (int i = 0; i < mNestCount; i++) {
224                    if (!readBeginVCard(allowGarbage)) {
225                        return false;
226                    }
227                    allowGarbage = true;
228                }
229            }
230        }
231
232        if (!readBeginVCard(allowGarbage)) {
233            return false;
234        }
235        long start;
236        if (mInterpreter != null) {
237            start = System.currentTimeMillis();
238            mInterpreter.startEntry();
239            mTimeReadStartRecord += System.currentTimeMillis() - start;
240        }
241        start = System.currentTimeMillis();
242        parseItems();
243        mTimeParseItems += System.currentTimeMillis() - start;
244        readEndVCard(true, false);
245        if (mInterpreter != null) {
246            start = System.currentTimeMillis();
247            mInterpreter.endEntry();
248            mTimeReadEndRecord += System.currentTimeMillis() - start;
249        }
250        return true;
251    }
252
253    /**
254     * @return True when successful. False when reaching the end of line
255     * @throws IOException
256     * @throws VCardException
257     */
258    protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
259        String line;
260        do {
261            while (true) {
262                line = getLine();
263                if (line == null) {
264                    return false;
265                } else if (line.trim().length() > 0) {
266                    break;
267                }
268            }
269            String[] strArray = line.split(":", 2);
270            int length = strArray.length;
271
272            // Though vCard 2.1/3.0 specification does not allow lower cases,
273            // vCard file emitted by some external vCard expoter have such
274            // invalid Strings.
275            // So we allow it.
276            // e.g. BEGIN:vCard
277            if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN")
278                    && strArray[1].trim().equalsIgnoreCase("VCARD")) {
279                return true;
280            } else if (!allowGarbage) {
281                if (mNestCount > 0) {
282                    mPreviousLine = line;
283                    return false;
284                } else {
285                    throw new VCardException("Expected String \"BEGIN:VCARD\" did not come "
286                            + "(Instead, \"" + line + "\" came)");
287                }
288            }
289        } while (allowGarbage);
290
291        throw new VCardException("Reached where must not be reached.");
292    }
293
294    /**
295     * <p>
296     * The arguments useCache and allowGarbase are usually true and false
297     * accordingly when this function is called outside this function itself.
298     * </p>
299     *
300     * @param useCache When true, line is obtained from mPreviousline.
301     *            Otherwise, getLine() is used.
302     * @param allowGarbage When true, ignore non "END:VCARD" line.
303     * @throws IOException
304     * @throws VCardException
305     */
306    protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException,
307            VCardException {
308        String line;
309        do {
310            if (useCache) {
311                // Though vCard specification does not allow lower cases,
312                // some data may have them, so we allow it.
313                line = mPreviousLine;
314            } else {
315                while (true) {
316                    line = getLine();
317                    if (line == null) {
318                        throw new VCardException("Expected END:VCARD was not found.");
319                    } else if (line.trim().length() > 0) {
320                        break;
321                    }
322                }
323            }
324
325            String[] strArray = line.split(":", 2);
326            if (strArray.length == 2 && strArray[0].trim().equalsIgnoreCase("END")
327                    && strArray[1].trim().equalsIgnoreCase("VCARD")) {
328                return;
329            } else if (!allowGarbage) {
330                throw new VCardException("END:VCARD != \"" + mPreviousLine + "\"");
331            }
332            useCache = false;
333        } while (allowGarbage);
334    }
335
336    /*
337     * items = *CRLF item / item
338     */
339    protected void parseItems() throws IOException, VCardException {
340        boolean ended = false;
341
342        if (mInterpreter != null) {
343            long start = System.currentTimeMillis();
344            mInterpreter.startProperty();
345            mTimeStartProperty += System.currentTimeMillis() - start;
346        }
347        ended = parseItem();
348        if (mInterpreter != null && !ended) {
349            long start = System.currentTimeMillis();
350            mInterpreter.endProperty();
351            mTimeEndProperty += System.currentTimeMillis() - start;
352        }
353
354        while (!ended) {
355            // follow VCARD ,it wont reach endProperty
356            if (mInterpreter != null) {
357                long start = System.currentTimeMillis();
358                mInterpreter.startProperty();
359                mTimeStartProperty += System.currentTimeMillis() - start;
360            }
361            try {
362                ended = parseItem();
363            } catch (VCardInvalidCommentLineException e) {
364                Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored.");
365                ended = false;
366            }
367            if (mInterpreter != null && !ended) {
368                long start = System.currentTimeMillis();
369                mInterpreter.endProperty();
370                mTimeEndProperty += System.currentTimeMillis() - start;
371            }
372        }
373    }
374
375    /*
376     * item = [groups "."] name [params] ":" value CRLF / [groups "."] "ADR"
377     * [params] ":" addressparts CRLF / [groups "."] "ORG" [params] ":" orgparts
378     * CRLF / [groups "."] "N" [params] ":" nameparts CRLF / [groups "."]
379     * "AGENT" [params] ":" vcard CRLF
380     */
381    protected boolean parseItem() throws IOException, VCardException {
382        mCurrentEncoding = DEFAULT_ENCODING;
383
384        final String line = getNonEmptyLine();
385        long start = System.currentTimeMillis();
386
387        String[] propertyNameAndValue = separateLineAndHandleGroup(line);
388        if (propertyNameAndValue == null) {
389            return true;
390        }
391        if (propertyNameAndValue.length != 2) {
392            throw new VCardInvalidLineException("Invalid line \"" + line + "\"");
393        }
394        String propertyName = propertyNameAndValue[0].toUpperCase();
395        String propertyValue = propertyNameAndValue[1];
396
397        mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start;
398
399        if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) {
400            start = System.currentTimeMillis();
401            handleMultiplePropertyValue(propertyName, propertyValue);
402            mTimeParseAdrOrgN += System.currentTimeMillis() - start;
403            return false;
404        } else if (propertyName.equals("AGENT")) {
405            handleAgent(propertyValue);
406            return false;
407        } else if (isValidPropertyName(propertyName)) {
408            if (propertyName.equals("BEGIN")) {
409                if (propertyValue.equals("VCARD")) {
410                    throw new VCardNestedException("This vCard has nested vCard data in it.");
411                } else {
412                    throw new VCardException("Unknown BEGIN type: " + propertyValue);
413                }
414            } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersionString())) {
415                throw new VCardVersionException("Incompatible version: " + propertyValue + " != "
416                        + getVersionString());
417            }
418            start = System.currentTimeMillis();
419            handlePropertyValue(propertyName, propertyValue);
420            mTimeParsePropertyValues += System.currentTimeMillis() - start;
421            return false;
422        }
423
424        throw new VCardException("Unknown property name: \"" + propertyName + "\"");
425    }
426
427    // For performance reason, the states for group and property name are merged into one.
428    static private final int STATE_GROUP_OR_PROPERTY_NAME = 0;
429    static private final int STATE_PARAMS = 1;
430    // vCard 3.0 specification allows double-quoted parameters, while vCard 2.1 does not.
431    static private final int STATE_PARAMS_IN_DQUOTE = 2;
432
433    protected String[] separateLineAndHandleGroup(String line) throws VCardException {
434        final String[] propertyNameAndValue = new String[2];
435        final int length = line.length();
436        if (length > 0 && line.charAt(0) == '#') {
437            throw new VCardInvalidCommentLineException();
438        }
439
440        int state = STATE_GROUP_OR_PROPERTY_NAME;
441        int nameIndex = 0;
442
443        // This loop is developed so that we don't have to take care of bottle neck here.
444        // Refactor carefully when you need to do so.
445        for (int i = 0; i < length; i++) {
446            final char ch = line.charAt(i);
447            switch (state) {
448                case STATE_GROUP_OR_PROPERTY_NAME: {
449                    if (ch == ':') {  // End of a property name.
450                        final String propertyName = line.substring(nameIndex, i);
451                        if (propertyName.equalsIgnoreCase("END")) {
452                            mPreviousLine = line;
453                            return null;
454                        }
455                        if (mInterpreter != null) {
456                            mInterpreter.propertyName(propertyName);
457                        }
458                        propertyNameAndValue[0] = propertyName;
459                        if (i < length - 1) {
460                            propertyNameAndValue[1] = line.substring(i + 1);
461                        } else {
462                            propertyNameAndValue[1] = "";
463                        }
464                        return propertyNameAndValue;
465                    } else if (ch == '.') {  // Each group is followed by the dot.
466                        final String groupName = line.substring(nameIndex, i);
467                        if (groupName.length() == 0) {
468                            Log.w(LOG_TAG, "Empty group found. Ignoring.");
469                        } else if (mInterpreter != null) {
470                            mInterpreter.propertyGroup(groupName);
471                        }
472                        nameIndex = i + 1;  // Next should be another group or a property name.
473                    } else if (ch == ';') {  // End of property name and beginneng of parameters.
474                        final String propertyName = line.substring(nameIndex, i);
475                        if (propertyName.equalsIgnoreCase("END")) {
476                            mPreviousLine = line;
477                            return null;
478                        }
479                        if (mInterpreter != null) {
480                            mInterpreter.propertyName(propertyName);
481                        }
482                        propertyNameAndValue[0] = propertyName;
483                        nameIndex = i + 1;
484                        state = STATE_PARAMS;  // Start parameter parsing.
485                    }
486                    break;
487                }
488                case STATE_PARAMS: {
489                    if (ch == '"') {
490                        if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) {
491                            Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " +
492                                    "Silently allow it");
493                        }
494                        state = STATE_PARAMS_IN_DQUOTE;
495                    } else if (ch == ';') {  // Starts another param.
496                        handleParams(line.substring(nameIndex, i));
497                        nameIndex = i + 1;
498                    } else if (ch == ':') {  // End of param and beginenning of values.
499                        handleParams(line.substring(nameIndex, i));
500                        if (i < length - 1) {
501                            propertyNameAndValue[1] = line.substring(i + 1);
502                        } else {
503                            propertyNameAndValue[1] = "";
504                        }
505                        return propertyNameAndValue;
506                    }
507                    break;
508                }
509                case STATE_PARAMS_IN_DQUOTE: {
510                    if (ch == '"') {
511                        if (VCardConstants.VERSION_V21.equalsIgnoreCase(getVersionString())) {
512                            Log.w(LOG_TAG, "Double-quoted params found in vCard 2.1. " +
513                                    "Silently allow it");
514                        }
515                        state = STATE_PARAMS;
516                    }
517                    break;
518                }
519            }
520        }
521
522        throw new VCardInvalidLineException("Invalid line: \"" + line + "\"");
523    }
524
525    /*
526     * params = ";" [ws] paramlist paramlist = paramlist [ws] ";" [ws] param /
527     * param param = "TYPE" [ws] "=" [ws] ptypeval / "VALUE" [ws] "=" [ws]
528     * pvalueval / "ENCODING" [ws] "=" [ws] pencodingval / "CHARSET" [ws] "="
529     * [ws] charsetval / "LANGUAGE" [ws] "=" [ws] langval / "X-" word [ws] "="
530     * [ws] word / knowntype
531     */
532    protected void handleParams(String params) throws VCardException {
533        final String[] strArray = params.split("=", 2);
534        if (strArray.length == 2) {
535            final String paramName = strArray[0].trim().toUpperCase();
536            String paramValue = strArray[1].trim();
537            if (paramName.equals("TYPE")) {
538                handleType(paramValue);
539            } else if (paramName.equals("VALUE")) {
540                handleValue(paramValue);
541            } else if (paramName.equals("ENCODING")) {
542                handleEncoding(paramValue);
543            } else if (paramName.equals("CHARSET")) {
544                handleCharset(paramValue);
545            } else if (paramName.equals("LANGUAGE")) {
546                handleLanguage(paramValue);
547            } else if (paramName.startsWith("X-")) {
548                handleAnyParam(paramName, paramValue);
549            } else {
550                throw new VCardException("Unknown type \"" + paramName + "\"");
551            }
552        } else {
553            handleParamWithoutName(strArray[0]);
554        }
555    }
556
557    /**
558     * vCard 3.0 parser implementation may throw VCardException.
559     */
560    @SuppressWarnings("unused")
561    protected void handleParamWithoutName(final String paramValue) throws VCardException {
562        handleType(paramValue);
563    }
564
565    /*
566     * ptypeval = knowntype / "X-" word
567     */
568    protected void handleType(final String ptypeval) {
569        if (!(getKnownTypeSet().contains(ptypeval.toUpperCase())
570                || ptypeval.startsWith("X-"))
571                && !mUnknownTypeSet.contains(ptypeval)) {
572            mUnknownTypeSet.add(ptypeval);
573            Log.w(LOG_TAG, String.format("TYPE unsupported by %s: ", getVersion(), ptypeval));
574        }
575        if (mInterpreter != null) {
576            mInterpreter.propertyParamType("TYPE");
577            mInterpreter.propertyParamValue(ptypeval);
578        }
579    }
580
581    /*
582     * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word
583     */
584    protected void handleValue(final String pvalueval) {
585        if (!(getKnownValueSet().contains(pvalueval.toUpperCase())
586                || pvalueval.startsWith("X-")
587                || mUnknownValueSet.contains(pvalueval))) {
588            mUnknownValueSet.add(pvalueval);
589            Log.w(LOG_TAG, String.format(
590                    "The value unsupported by TYPE of %s: ", getVersion(), pvalueval));
591        }
592        if (mInterpreter != null) {
593            mInterpreter.propertyParamType("VALUE");
594            mInterpreter.propertyParamValue(pvalueval);
595        }
596    }
597
598    /*
599     * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word
600     */
601    protected void handleEncoding(String pencodingval) throws VCardException {
602        if (getAvailableEncodingSet().contains(pencodingval) ||
603                pencodingval.startsWith("X-")) {
604            if (mInterpreter != null) {
605                mInterpreter.propertyParamType("ENCODING");
606                mInterpreter.propertyParamValue(pencodingval);
607            }
608            mCurrentEncoding = pencodingval;
609        } else {
610            throw new VCardException("Unknown encoding \"" + pencodingval + "\"");
611        }
612    }
613
614    /**
615     * <p>
616     * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521),
617     * but recent vCard files often contain other charset like UTF-8, SHIFT_JIS, etc.
618     * We allow any charset.
619     * </p>
620     */
621    protected void handleCharset(String charsetval) {
622        if (mInterpreter != null) {
623            mInterpreter.propertyParamType("CHARSET");
624            mInterpreter.propertyParamValue(charsetval);
625        }
626    }
627
628    /**
629     * See also Section 7.1 of RFC 1521
630     */
631    protected void handleLanguage(String langval) throws VCardException {
632        String[] strArray = langval.split("-");
633        if (strArray.length != 2) {
634            throw new VCardException("Invalid Language: \"" + langval + "\"");
635        }
636        String tmp = strArray[0];
637        int length = tmp.length();
638        for (int i = 0; i < length; i++) {
639            if (!isAsciiLetter(tmp.charAt(i))) {
640                throw new VCardException("Invalid Language: \"" + langval + "\"");
641            }
642        }
643        tmp = strArray[1];
644        length = tmp.length();
645        for (int i = 0; i < length; i++) {
646            if (!isAsciiLetter(tmp.charAt(i))) {
647                throw new VCardException("Invalid Language: \"" + langval + "\"");
648            }
649        }
650        if (mInterpreter != null) {
651            mInterpreter.propertyParamType("LANGUAGE");
652            mInterpreter.propertyParamValue(langval);
653        }
654    }
655
656    private boolean isAsciiLetter(char ch) {
657        if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
658            return true;
659        }
660        return false;
661    }
662
663    /**
664     * Mainly for "X-" type. This accepts any kind of type without check.
665     */
666    protected void handleAnyParam(String paramName, String paramValue) {
667        if (mInterpreter != null) {
668            mInterpreter.propertyParamType(paramName);
669            mInterpreter.propertyParamValue(paramValue);
670        }
671    }
672
673    protected void handlePropertyValue(String propertyName, String propertyValue)
674            throws IOException, VCardException {
675        final String upperEncoding = mCurrentEncoding.toUpperCase();
676        if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_QP)) {
677            final long start = System.currentTimeMillis();
678            final String result = getQuotedPrintable(propertyValue);
679            if (mInterpreter != null) {
680                ArrayList<String> v = new ArrayList<String>();
681                v.add(result);
682                mInterpreter.propertyValues(v);
683            }
684            mTimeHandleQuotedPrintable += System.currentTimeMillis() - start;
685        } else if (upperEncoding.equals(VCardConstants.PARAM_ENCODING_BASE64)
686                || upperEncoding.equals(VCardConstants.PARAM_ENCODING_B)) {
687            final long start = System.currentTimeMillis();
688            // It is very rare, but some BASE64 data may be so big that
689            // OutOfMemoryError occurs. To ignore such cases, use try-catch.
690            try {
691                final String result = getBase64(propertyValue);
692                if (mInterpreter != null) {
693                    ArrayList<String> arrayList = new ArrayList<String>();
694                    arrayList.add(result);
695                    mInterpreter.propertyValues(arrayList);
696                }
697            } catch (OutOfMemoryError error) {
698                Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!");
699                if (mInterpreter != null) {
700                    mInterpreter.propertyValues(null);
701                }
702            }
703            mTimeHandleBase64 += System.currentTimeMillis() - start;
704        } else {
705            if (!(upperEncoding.equals("7BIT") || upperEncoding.equals("8BIT") ||
706                    upperEncoding.startsWith("X-"))) {
707                Log.w(LOG_TAG,
708                        String.format("The encoding \"%s\" is unsupported by vCard %s",
709                                mCurrentEncoding, getVersionString()));
710            }
711
712            final long start = System.currentTimeMillis();
713            if (mInterpreter != null) {
714                ArrayList<String> v = new ArrayList<String>();
715                v.add(maybeUnescapeText(propertyValue));
716                mInterpreter.propertyValues(v);
717            }
718            mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start;
719        }
720    }
721
722    /**
723     * <p>
724     * Parses and returns Quoted-Printable.
725     * </p>
726     *
727     * @param firstString The string following a parameter name and attributes.
728     *            Example: "string" in
729     *            "ADR:ENCODING=QUOTED-PRINTABLE:string\n\r".
730     * @return whole Quoted-Printable string, including a given argument and
731     *         following lines. Excludes the last empty line following to Quoted
732     *         Printable lines.
733     * @throws IOException
734     * @throws VCardException
735     */
736    private String getQuotedPrintable(String firstString) throws IOException, VCardException {
737        // Specifically, there may be some padding between = and CRLF.
738        // See the following:
739        //
740        // qp-line := *(qp-segment transport-padding CRLF)
741        // qp-part transport-padding
742        // qp-segment := qp-section *(SPACE / TAB) "="
743        // ; Maximum length of 76 characters
744        //
745        // e.g. (from RFC 2045)
746        // Now's the time =
747        // for all folk to come=
748        // to the aid of their country.
749        if (firstString.trim().endsWith("=")) {
750            // remove "transport-padding"
751            int pos = firstString.length() - 1;
752            while (firstString.charAt(pos) != '=') {
753            }
754            StringBuilder builder = new StringBuilder();
755            builder.append(firstString.substring(0, pos + 1));
756            builder.append("\r\n");
757            String line;
758            while (true) {
759                line = getLine();
760                if (line == null) {
761                    throw new VCardException("File ended during parsing a Quoted-Printable String");
762                }
763                if (line.trim().endsWith("=")) {
764                    // remove "transport-padding"
765                    pos = line.length() - 1;
766                    while (line.charAt(pos) != '=') {
767                    }
768                    builder.append(line.substring(0, pos + 1));
769                    builder.append("\r\n");
770                } else {
771                    builder.append(line);
772                    break;
773                }
774            }
775            return builder.toString();
776        } else {
777            return firstString;
778        }
779    }
780
781    protected String getBase64(String firstString) throws IOException, VCardException {
782        StringBuilder builder = new StringBuilder();
783        builder.append(firstString);
784
785        while (true) {
786            String line = getLine();
787            if (line == null) {
788                throw new VCardException("File ended during parsing BASE64 binary");
789            }
790            if (line.length() == 0) {
791                break;
792            }
793            builder.append(line);
794        }
795
796        return builder.toString();
797    }
798
799    /**
800     * <p>
801     * Mainly for "ADR", "ORG", and "N"
802     * </p>
803     */
804    /*
805     * addressparts = 0*6(strnosemi ";") strnosemi ; PO Box, Extended Addr,
806     * Street, Locality, Region, Postal Code, Country Name orgparts =
807     * *(strnosemi ";") strnosemi ; First is Organization Name, remainder are
808     * Organization Units. nameparts = 0*4(strnosemi ";") strnosemi ; Family,
809     * Given, Middle, Prefix, Suffix. ; Example:Public;John;Q.;Reverend Dr.;III,
810     * Esq. strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi ; To include a
811     * semicolon in this string, it must be escaped ; with a "\" character. We
812     * do not care the number of "strnosemi" here. We are not sure whether we
813     * should add "\" CRLF to each value. We exclude them for now.
814     */
815    protected void handleMultiplePropertyValue(String propertyName, String propertyValue)
816            throws IOException, VCardException {
817        // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some
818        // softwares/devices
819        // emit such data.
820        if (mCurrentEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
821            propertyValue = getQuotedPrintable(propertyValue);
822        }
823
824        if (mInterpreter != null) {
825            mInterpreter.propertyValues(VCardUtils.constructListFromValue(propertyValue,
826                    (getVersion() == VCardConfig.FLAG_V30)));
827        }
828    }
829
830    /*
831     * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an
832     * error toward the AGENT property.
833     * // TODO: Support AGENT property.
834     * item =
835     * ... / [groups "."] "AGENT" [params] ":" vcard CRLF vcard = "BEGIN" [ws]
836     * ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":" [ws] "VCARD"
837     */
838    protected void handleAgent(final String propertyValue) throws VCardException {
839        if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) {
840            // Apparently invalid line seen in Windows Mobile 6.5. Ignore them.
841            return;
842        } else {
843            throw new VCardAgentNotSupportedException("AGENT Property is not supported now.");
844        }
845    }
846
847    /**
848     * For vCard 3.0.
849     */
850    protected String maybeUnescapeText(final String text) {
851        return text;
852    }
853
854    /**
855     * Returns unescaped String if the character should be unescaped. Return
856     * null otherwise. e.g. In vCard 2.1, "\;" should be unescaped into ";"
857     * while "\x" should not be.
858     */
859    protected String maybeUnescapeCharacter(final char ch) {
860        return unescapeCharacter(ch);
861    }
862
863    /* package */ static String unescapeCharacter(final char ch) {
864        // Original vCard 2.1 specification does not allow transformation
865        // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous
866        // implementation of
867        // this class allowed them, so keep it as is.
868        if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') {
869            return String.valueOf(ch);
870        } else {
871            return null;
872        }
873    }
874
875    private void showPerformanceInfo() {
876        Log.d(LOG_TAG, "Total parsing time:  " + mTimeTotal + " ms");
877        if (mReader instanceof CustomBufferedReader) {
878            Log.d(LOG_TAG, "Total readLine time: "
879                    + ((CustomBufferedReader) mReader).getTotalmillisecond() + " ms");
880        }
881        Log.d(LOG_TAG, "Time for handling the beggining of the record: " + mTimeReadStartRecord
882                + " ms");
883        Log.d(LOG_TAG, "Time for handling the end of the record: " + mTimeReadEndRecord + " ms");
884        Log.d(LOG_TAG, "Time for parsing line, and handling group: " + mTimeParseLineAndHandleGroup
885                + " ms");
886        Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms");
887        Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms");
888        Log.d(LOG_TAG, "Time for handling normal property values: " + mTimeHandleMiscPropertyValue
889                + " ms");
890        Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + mTimeHandleQuotedPrintable + " ms");
891        Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms");
892    }
893
894    /**
895     * @return {@link VCardConfig#FLAG_V21}
896     */
897    protected int getVersion() {
898        return VCardConfig.FLAG_V21;
899    }
900
901    /**
902     * @return {@link VCardConfig#FLAG_V30}
903     */
904    protected String getVersionString() {
905        return VCardConstants.VERSION_V21;
906    }
907
908    protected Set<String> getKnownPropertyNameSet() {
909        return VCardParser_V21.sKnownPropertyNameSet;
910    }
911
912    protected Set<String> getKnownTypeSet() {
913        return VCardParser_V21.sKnownTypeSet;
914    }
915
916    protected Set<String> getKnownValueSet() {
917        return VCardParser_V21.sKnownValueSet;
918    }
919
920    protected Set<String> getAvailableEncodingSet() {
921        return VCardParser_V21.sAvailableEncoding;
922    }
923
924    protected String getDefaultEncoding() {
925        return DEFAULT_ENCODING;
926    }
927
928
929    public void parse(InputStream is, VCardInterpreter interpreter)
930            throws IOException, VCardException {
931        if (is == null) {
932            throw new NullPointerException("InputStream must not be null.");
933        }
934
935        final InputStreamReader tmpReader = new InputStreamReader(is, mIntermediateCharset);
936        if (VCardConfig.showPerformanceLog()) {
937            mReader = new CustomBufferedReader(tmpReader);
938        } else {
939            mReader = new BufferedReader(tmpReader);
940        }
941
942        mInterpreter = interpreter;
943
944        final long start = System.currentTimeMillis();
945        if (mInterpreter != null) {
946            mInterpreter.start();
947        }
948        parseVCardFile();
949        if (mInterpreter != null) {
950            mInterpreter.end();
951        }
952        mTimeTotal += System.currentTimeMillis() - start;
953
954        if (VCardConfig.showPerformanceLog()) {
955            showPerformanceInfo();
956        }
957    }
958
959    public final void cancel() {
960        mCanceled = true;
961    }
962}
963