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