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