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