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