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