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