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