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