Home | History | Annotate | Download | only in vcard
      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  */
     16 package com.android.vcard;
     17 
     18 import android.provider.ContactsContract.CommonDataKinds.Im;
     19 import android.provider.ContactsContract.CommonDataKinds.Phone;
     20 import android.telephony.PhoneNumberUtils;
     21 import android.text.SpannableStringBuilder;
     22 import android.text.TextUtils;
     23 import android.util.Log;
     24 
     25 import com.android.vcard.exception.VCardException;
     26 
     27 import java.io.ByteArrayOutputStream;
     28 import java.io.UnsupportedEncodingException;
     29 import java.nio.ByteBuffer;
     30 import java.nio.charset.Charset;
     31 import java.util.ArrayList;
     32 import java.util.Arrays;
     33 import java.util.Collection;
     34 import java.util.HashMap;
     35 import java.util.HashSet;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.Set;
     39 
     40 /**
     41  * Utilities for VCard handling codes.
     42  */
     43 public 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 (at) 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