VCardUtils.java revision be378d5b188f51cf717e5309e3c39180e85833a8
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 com.android.vcard.exception.VCardException;
19
20import org.apache.commons.codec.DecoderException;
21import org.apache.commons.codec.net.QuotedPrintableCodec;
22
23import android.content.ContentProviderOperation;
24import android.provider.ContactsContract.Data;
25import android.provider.ContactsContract.CommonDataKinds.Im;
26import android.provider.ContactsContract.CommonDataKinds.Phone;
27import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
28import android.telephony.PhoneNumberUtils;
29import android.text.TextUtils;
30import android.util.Log;
31
32import java.io.UnsupportedEncodingException;
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.HashMap;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Map;
40import java.util.Set;
41
42/**
43 * Utilities for VCard handling codes.
44 */
45public class VCardUtils {
46    private static final String LOG_TAG = "VCardUtils";
47
48    // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is
49    // converted to two parameter Strings. These only contain some minor fields valid in both
50    // vCard and current (as of 2009-08-07) Contacts structure.
51    private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS;
52    private static final Set<String> sPhoneTypesUnknownToContactsSet;
53    private static final Map<String, Integer> sKnownPhoneTypeMap_StoI;
54    private static final Map<Integer, String> sKnownImPropNameMap_ItoS;
55    private static final Set<String> sMobilePhoneLabelSet;
56
57    static {
58        sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>();
59        sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>();
60
61        sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR);
62        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR);
63        sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER);
64        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER);
65        sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN);
66        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN);
67
68        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME);
69        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK);
70        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE);
71
72        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER);
73        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK,
74                Phone.TYPE_CALLBACK);
75        sKnownPhoneTypeMap_StoI.put(
76                VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN);
77        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO);
78        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD,
79                Phone.TYPE_TTY_TDD);
80        sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT,
81                Phone.TYPE_ASSISTANT);
82
83        sPhoneTypesUnknownToContactsSet = new HashSet<String>();
84        sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM);
85        sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG);
86        sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS);
87        sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO);
88
89        sKnownImPropNameMap_ItoS = new HashMap<Integer, String>();
90        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
91        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
92        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
93        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
94        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK,
95                VCardConstants.PROPERTY_X_GOOGLE_TALK);
96        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
97        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
98        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ);
99        sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING);
100
101        // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone)
102        // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone)
103        // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone)
104        // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone)
105        sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList(
106                "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4",
107                "\uFF79\uFF72\uFF80\uFF72"));
108    }
109
110    public static String getPhoneTypeString(Integer type) {
111        return sKnownPhoneTypesMap_ItoS.get(type);
112    }
113
114    /**
115     * Returns Interger when the given types can be parsed as known type. Returns String object
116     * when not, which should be set to label.
117     */
118    public static Object getPhoneTypeFromStrings(Collection<String> types,
119            String number) {
120        if (number == null) {
121            number = "";
122        }
123        int type = -1;
124        String label = null;
125        boolean isFax = false;
126        boolean hasPref = false;
127
128        if (types != null) {
129            for (String typeString : types) {
130                if (typeString == null) {
131                    continue;
132                }
133                typeString = typeString.toUpperCase();
134                if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
135                    hasPref = true;
136                } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) {
137                    isFax = true;
138                } else {
139                    if (typeString.startsWith("X-") && type < 0) {
140                        typeString = typeString.substring(2);
141                    }
142                    if (typeString.length() == 0) {
143                        continue;
144                    }
145                    final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString);
146                    if (tmp != null) {
147                        final int typeCandidate = tmp;
148                        // TYPE_PAGER is prefered when the number contains @ surronded by
149                        // a pager number and a domain name.
150                        // e.g.
151                        // o 1111@domain.com
152                        // x @domain.com
153                        // x 1111@
154                        final int indexOfAt = number.indexOf("@");
155                        if ((typeCandidate == Phone.TYPE_PAGER
156                                && 0 < indexOfAt && indexOfAt < number.length() - 1)
157                                || type < 0
158                                || type == Phone.TYPE_CUSTOM) {
159                            type = tmp;
160                        }
161                    } else if (type < 0) {
162                        type = Phone.TYPE_CUSTOM;
163                        label = typeString;
164                    }
165                }
166            }
167        }
168        if (type < 0) {
169            if (hasPref) {
170                type = Phone.TYPE_MAIN;
171            } else {
172                // default to TYPE_HOME
173                type = Phone.TYPE_HOME;
174            }
175        }
176        if (isFax) {
177            if (type == Phone.TYPE_HOME) {
178                type = Phone.TYPE_FAX_HOME;
179            } else if (type == Phone.TYPE_WORK) {
180                type = Phone.TYPE_FAX_WORK;
181            } else if (type == Phone.TYPE_OTHER) {
182                type = Phone.TYPE_OTHER_FAX;
183            }
184        }
185        if (type == Phone.TYPE_CUSTOM) {
186            return label;
187        } else {
188            return type;
189        }
190    }
191
192    @SuppressWarnings("deprecation")
193    public static boolean isMobilePhoneLabel(final String label) {
194        // For backward compatibility.
195        // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
196        //         To support mobile type at that time, this custom label had been used.
197        return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label));
198    }
199
200    public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) {
201        return sPhoneTypesUnknownToContactsSet.contains(label);
202    }
203
204    public static String getPropertyNameForIm(final int protocol) {
205        return sKnownImPropNameMap_ItoS.get(protocol);
206    }
207
208    public static String[] sortNameElements(final int vcardType,
209            final String familyName, final String middleName, final String givenName) {
210        final String[] list = new String[3];
211        final int nameOrderType = VCardConfig.getNameOrderType(vcardType);
212        switch (nameOrderType) {
213            case VCardConfig.NAME_ORDER_JAPANESE: {
214                if (containsOnlyPrintableAscii(familyName) &&
215                        containsOnlyPrintableAscii(givenName)) {
216                    list[0] = givenName;
217                    list[1] = middleName;
218                    list[2] = familyName;
219                } else {
220                    list[0] = familyName;
221                    list[1] = middleName;
222                    list[2] = givenName;
223                }
224                break;
225            }
226            case VCardConfig.NAME_ORDER_EUROPE: {
227                list[0] = middleName;
228                list[1] = givenName;
229                list[2] = familyName;
230                break;
231            }
232            default: {
233                list[0] = givenName;
234                list[1] = middleName;
235                list[2] = familyName;
236                break;
237            }
238        }
239        return list;
240    }
241
242    public static int getPhoneNumberFormat(final int vcardType) {
243        if (VCardConfig.isJapaneseDevice(vcardType)) {
244            return PhoneNumberUtils.FORMAT_JAPAN;
245        } else {
246            return PhoneNumberUtils.FORMAT_NANP;
247        }
248    }
249
250    /**
251     * <p>
252     * Inserts postal data into the builder object.
253     * </p>
254     * <p>
255     * Note that the data structure of ContactsContract is different from that defined in vCard.
256     * So some conversion may be performed in this method.
257     * </p>
258     */
259    public static void insertStructuredPostalDataUsingContactsStruct(int vcardType,
260            final ContentProviderOperation.Builder builder,
261            final VCardEntry.PostalData postalData) {
262        builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0);
263        builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
264
265        builder.withValue(StructuredPostal.TYPE, postalData.type);
266        if (postalData.type == StructuredPostal.TYPE_CUSTOM) {
267            builder.withValue(StructuredPostal.LABEL, postalData.label);
268        }
269
270        final String streetString;
271        if (TextUtils.isEmpty(postalData.street)) {
272            if (TextUtils.isEmpty(postalData.extendedAddress)) {
273                streetString = null;
274            } else {
275                streetString = postalData.extendedAddress;
276            }
277        } else {
278            if (TextUtils.isEmpty(postalData.extendedAddress)) {
279                streetString = postalData.street;
280            } else {
281                streetString = postalData.street + " " + postalData.extendedAddress;
282            }
283        }
284        builder.withValue(StructuredPostal.POBOX, postalData.pobox);
285        builder.withValue(StructuredPostal.STREET, streetString);
286        builder.withValue(StructuredPostal.CITY, postalData.localty);
287        builder.withValue(StructuredPostal.REGION, postalData.region);
288        builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode);
289        builder.withValue(StructuredPostal.COUNTRY, postalData.country);
290
291        builder.withValue(StructuredPostal.FORMATTED_ADDRESS,
292                postalData.getFormattedAddress(vcardType));
293        if (postalData.isPrimary) {
294            builder.withValue(Data.IS_PRIMARY, 1);
295        }
296    }
297
298    public static String constructNameFromElements(final int vcardType,
299            final String familyName, final String middleName, final String givenName) {
300        return constructNameFromElements(vcardType, familyName, middleName, givenName,
301                null, null);
302    }
303
304    public static String constructNameFromElements(final int vcardType,
305            final String familyName, final String middleName, final String givenName,
306            final String prefix, final String suffix) {
307        final StringBuilder builder = new StringBuilder();
308        final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName);
309        boolean first = true;
310        if (!TextUtils.isEmpty(prefix)) {
311            first = false;
312            builder.append(prefix);
313        }
314        for (final String namePart : nameList) {
315            if (!TextUtils.isEmpty(namePart)) {
316                if (first) {
317                    first = false;
318                } else {
319                    builder.append(' ');
320                }
321                builder.append(namePart);
322            }
323        }
324        if (!TextUtils.isEmpty(suffix)) {
325            if (!first) {
326                builder.append(' ');
327            }
328            builder.append(suffix);
329        }
330        return builder.toString();
331    }
332
333    public static List<String> constructListFromValue(final String value,
334            final boolean isV30) {
335        final List<String> list = new ArrayList<String>();
336        StringBuilder builder = new StringBuilder();
337        int length = value.length();
338        for (int i = 0; i < length; i++) {
339            char ch = value.charAt(i);
340            if (ch == '\\' && i < length - 1) {
341                char nextCh = value.charAt(i + 1);
342                final String unescapedString =
343                    (isV30 ? VCardParserImpl_V30.escapeCharacter(nextCh) :
344                        VCardParserImpl_V21.unescapeCharacter(nextCh));
345                if (unescapedString != null) {
346                    builder.append(unescapedString);
347                    i++;
348                } else {
349                    builder.append(ch);
350                }
351            } else if (ch == ';') {
352                list.add(builder.toString());
353                builder = new StringBuilder();
354            } else {
355                builder.append(ch);
356            }
357        }
358        list.add(builder.toString());
359        return list;
360    }
361
362    public static boolean containsOnlyPrintableAscii(final String...values) {
363        if (values == null) {
364            return true;
365        }
366        return containsOnlyPrintableAscii(Arrays.asList(values));
367    }
368
369    public static boolean containsOnlyPrintableAscii(final Collection<String> values) {
370        if (values == null) {
371            return true;
372        }
373        for (final String value : values) {
374            if (TextUtils.isEmpty(value)) {
375                continue;
376            }
377            if (!TextUtils.isPrintableAsciiOnly(value)) {
378                return false;
379            }
380        }
381        return true;
382    }
383
384    /**
385     * <p>
386     * This is useful when checking the string should be encoded into quoted-printable
387     * or not, which is required by vCard 2.1.
388     * </p>
389     * <p>
390     * See the definition of "7bit" in vCard 2.1 spec for more information.
391     * </p>
392     */
393    public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) {
394        if (values == null) {
395            return true;
396        }
397        return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values));
398    }
399
400    public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) {
401        if (values == null) {
402            return true;
403        }
404        final int asciiFirst = 0x20;
405        final int asciiLast = 0x7E;  // included
406        for (final String value : values) {
407            if (TextUtils.isEmpty(value)) {
408                continue;
409            }
410            final int length = value.length();
411            for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
412                final int c = value.codePointAt(i);
413                if (!(asciiFirst <= c && c <= asciiLast)) {
414                    return false;
415                }
416            }
417        }
418        return true;
419    }
420
421    private static final Set<Character> sUnAcceptableAsciiInV21WordSet =
422        new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' '));
423
424    /**
425     * <p>
426     * This is useful since vCard 3.0 often requires the ("X-") properties and groups
427     * should contain only alphabets, digits, and hyphen.
428     * </p>
429     * <p>
430     * Note: It is already known some devices (wrongly) outputs properties with characters
431     *       which should not be in the field. One example is "X-GOOGLE TALK". We accept
432     *       such kind of input but must never output it unless the target is very specific
433     *       to the device which is able to parse the malformed input.
434     * </p>
435     */
436    public static boolean containsOnlyAlphaDigitHyphen(final String...values) {
437        if (values == null) {
438            return true;
439        }
440        return containsOnlyAlphaDigitHyphen(Arrays.asList(values));
441    }
442
443    public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) {
444        if (values == null) {
445            return true;
446        }
447        final int upperAlphabetFirst = 0x41;  // A
448        final int upperAlphabetAfterLast = 0x5b;  // [
449        final int lowerAlphabetFirst = 0x61;  // a
450        final int lowerAlphabetAfterLast = 0x7b;  // {
451        final int digitFirst = 0x30;  // 0
452        final int digitAfterLast = 0x3A;  // :
453        final int hyphen = '-';
454        for (final String str : values) {
455            if (TextUtils.isEmpty(str)) {
456                continue;
457            }
458            final int length = str.length();
459            for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
460                int codepoint = str.codePointAt(i);
461                if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) ||
462                    (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) ||
463                    (digitFirst <= codepoint && codepoint < digitAfterLast) ||
464                    (codepoint == hyphen))) {
465                    return false;
466                }
467            }
468        }
469        return true;
470    }
471
472    /**
473     * <p>
474     * Returns true when the given String is categorized as "word" specified in vCard spec 2.1.
475     * </p>
476     * <p>
477     * vCard 2.1 specifies:<br />
478     * word = &lt;any printable 7bit us-ascii except []=:., &gt;
479     * </p>
480     */
481    public static boolean isV21Word(final String value) {
482        if (TextUtils.isEmpty(value)) {
483            return true;
484        }
485        final int asciiFirst = 0x20;
486        final int asciiLast = 0x7E;  // included
487        final int length = value.length();
488        for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
489            final int c = value.codePointAt(i);
490            if (!(asciiFirst <= c && c <= asciiLast) ||
491                    sUnAcceptableAsciiInV21WordSet.contains((char)c)) {
492                return false;
493            }
494        }
495        return true;
496    }
497
498    public static String toHalfWidthString(final String orgString) {
499        if (TextUtils.isEmpty(orgString)) {
500            return null;
501        }
502        final StringBuilder builder = new StringBuilder();
503        final int length = orgString.length();
504        for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) {
505            // All Japanese character is able to be expressed by char.
506            // Do not need to use String#codepPointAt().
507            final char ch = orgString.charAt(i);
508            final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch);
509            if (halfWidthText != null) {
510                builder.append(halfWidthText);
511            } else {
512                builder.append(ch);
513            }
514        }
515        return builder.toString();
516    }
517
518    /**
519     * Guesses the format of input image. Currently just the first few bytes are used.
520     * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when
521     * the guess failed.
522     * @param input Image as byte array.
523     * @return The image type or null when the type cannot be determined.
524     */
525    public static String guessImageType(final byte[] input) {
526        if (input == null) {
527            return null;
528        }
529        if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') {
530            return "GIF";
531        } else if (input.length >= 4 && input[0] == (byte) 0x89
532                && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') {
533            // Note: vCard 2.1 officially does not support PNG, but we may have it and
534            //       using X- word like "X-PNG" may not let importers know it is PNG.
535            //       So we use the String "PNG" as is...
536            return "PNG";
537        } else if (input.length >= 2 && input[0] == (byte) 0xff
538                && input[1] == (byte) 0xd8) {
539            return "JPEG";
540        } else {
541            return null;
542        }
543    }
544
545    /**
546     * @return True when all the given values are null or empty Strings.
547     */
548    public static boolean areAllEmpty(final String...values) {
549        if (values == null) {
550            return true;
551        }
552
553        for (final String value : values) {
554            if (!TextUtils.isEmpty(value)) {
555                return false;
556            }
557        }
558        return true;
559    }
560
561    //// The methods bellow may be used by unit test.
562
563    /**
564     * Unquotes given Quoted-Printable value. value must not be null.
565     */
566    public static String parseQuotedPrintable(
567            final String value, boolean strictLineBreaking,
568            String sourceCharset, String targetCharset) {
569        // "= " -> " ", "=\t" -> "\t".
570        // Previous code had done this replacement. Keep on the safe side.
571        final String quotedPrintable;
572        {
573            final StringBuilder builder = new StringBuilder();
574            final int length = value.length();
575            for (int i = 0; i < length; i++) {
576                char ch = value.charAt(i);
577                if (ch == '=' && i < length - 1) {
578                    char nextCh = value.charAt(i + 1);
579                    if (nextCh == ' ' || nextCh == '\t') {
580                        builder.append(nextCh);
581                        i++;
582                        continue;
583                    }
584                }
585                builder.append(ch);
586            }
587            quotedPrintable = builder.toString();
588        }
589
590        String[] lines;
591        if (strictLineBreaking) {
592            lines = quotedPrintable.split("\r\n");
593        } else {
594            StringBuilder builder = new StringBuilder();
595            final int length = quotedPrintable.length();
596            ArrayList<String> list = new ArrayList<String>();
597            for (int i = 0; i < length; i++) {
598                char ch = quotedPrintable.charAt(i);
599                if (ch == '\n') {
600                    list.add(builder.toString());
601                    builder = new StringBuilder();
602                } else if (ch == '\r') {
603                    list.add(builder.toString());
604                    builder = new StringBuilder();
605                    if (i < length - 1) {
606                        char nextCh = quotedPrintable.charAt(i + 1);
607                        if (nextCh == '\n') {
608                            i++;
609                        }
610                    }
611                } else {
612                    builder.append(ch);
613                }
614            }
615            final String lastLine = builder.toString();
616            if (lastLine.length() > 0) {
617                list.add(lastLine);
618            }
619            lines = list.toArray(new String[0]);
620        }
621
622        final StringBuilder builder = new StringBuilder();
623        for (String line : lines) {
624            if (line.endsWith("=")) {
625                line = line.substring(0, line.length() - 1);
626            }
627            builder.append(line);
628        }
629
630        final String rawString = builder.toString();
631        if (TextUtils.isEmpty(rawString)) {
632            Log.w(LOG_TAG, "Given raw string is empty.");
633        }
634
635        byte[] rawBytes = null;
636        try {
637            rawBytes = rawString.getBytes(sourceCharset);
638        } catch (UnsupportedEncodingException e) {
639            Log.w(LOG_TAG, "Failed to decode: " + sourceCharset);
640            rawBytes = rawString.getBytes();
641        }
642
643        byte[] decodedBytes = null;
644        try {
645            decodedBytes = QuotedPrintableCodec.decodeQuotedPrintable(rawBytes);
646        } catch (DecoderException e) {
647            Log.e(LOG_TAG, "DecoderException is thrown.");
648            decodedBytes = rawBytes;
649        }
650
651        try {
652            return new String(decodedBytes, targetCharset);
653        } catch (UnsupportedEncodingException e) {
654            Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
655            return new String(decodedBytes);
656        }
657    }
658
659    public static final VCardParser getAppropriateParser(int vcardType)
660            throws VCardException {
661        if (VCardConfig.isVersion21(vcardType)) {
662            return new VCardParser_V21();
663        } else if (VCardConfig.isVersion30(vcardType)) {
664            return new VCardParser_V30();
665        } else if (VCardConfig.isVersion40(vcardType)) {
666            return new VCardParser_V40();
667        } else {
668            throw new VCardException("Version is not specified");
669        }
670    }
671
672    // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean
673
674    private VCardUtils() {
675    }
676}
677