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