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.provider.ContactsContract.Data;
     20 import android.provider.ContactsContract.CommonDataKinds.Im;
     21 import android.provider.ContactsContract.CommonDataKinds.Phone;
     22 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     23 import android.telephony.PhoneNumberUtils;
     24 import android.text.TextUtils;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Arrays;
     28 import java.util.Collection;
     29 import java.util.HashMap;
     30 import java.util.HashSet;
     31 import java.util.List;
     32 import java.util.Map;
     33 import java.util.Set;
     34 
     35 /**
     36  * Utilities for VCard handling codes.
     37  */
     38 public class VCardUtils {
     39     // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is
     40     // converted to two parameter Strings. These only contain some minor fields valid in both
     41     // vCard and current (as of 2009-08-07) Contacts structure.
     42     private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS;
     43     private static final Set<String> sPhoneTypesUnknownToContactsSet;
     44     private static final Map<String, Integer> sKnownPhoneTypeMap_StoI;
     45     private static final Map<Integer, String> sKnownImPropNameMap_ItoS;
     46     private static final Set<String> sMobilePhoneLabelSet;
     47 
     48     static {
     49         sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>();
     50         sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>();
     51 
     52         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR);
     53         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR);
     54         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER);
     55         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER);
     56         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN);
     57         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN);
     58 
     59         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME);
     60         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK);
     61         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE);
     62 
     63         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER);
     64         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK,
     65                 Phone.TYPE_CALLBACK);
     66         sKnownPhoneTypeMap_StoI.put(
     67                 VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN);
     68         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO);
     69         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD,
     70                 Phone.TYPE_TTY_TDD);
     71         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT,
     72                 Phone.TYPE_ASSISTANT);
     73 
     74         sPhoneTypesUnknownToContactsSet = new HashSet<String>();
     75         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM);
     76         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG);
     77         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS);
     78         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO);
     79 
     80         sKnownImPropNameMap_ItoS = new HashMap<Integer, String>();
     81         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
     82         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
     83         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
     84         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
     85         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK,
     86                 VCardConstants.PROPERTY_X_GOOGLE_TALK);
     87         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
     88         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
     89         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ);
     90         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING);
     91 
     92         // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone)
     93         // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone)
     94         // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone)
     95         // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone)
     96         sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList(
     97                 "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4",
     98                 "\uFF79\uFF72\uFF80\uFF72"));
     99     }
    100 
    101     public static String getPhoneTypeString(Integer type) {
    102         return sKnownPhoneTypesMap_ItoS.get(type);
    103     }
    104 
    105     /**
    106      * Returns Interger when the given types can be parsed as known type. Returns String object
    107      * when not, which should be set to label.
    108      */
    109     public static Object getPhoneTypeFromStrings(Collection<String> types,
    110             String number) {
    111         if (number == null) {
    112             number = "";
    113         }
    114         int type = -1;
    115         String label = null;
    116         boolean isFax = false;
    117         boolean hasPref = false;
    118 
    119         if (types != null) {
    120             for (String typeString : types) {
    121                 if (typeString == null) {
    122                     continue;
    123                 }
    124                 typeString = typeString.toUpperCase();
    125                 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
    126                     hasPref = true;
    127                 } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) {
    128                     isFax = true;
    129                 } else {
    130                     if (typeString.startsWith("X-") && type < 0) {
    131                         typeString = typeString.substring(2);
    132                     }
    133                     if (typeString.length() == 0) {
    134                         continue;
    135                     }
    136                     final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString);
    137                     if (tmp != null) {
    138                         final int typeCandidate = tmp;
    139                         // TYPE_PAGER is prefered when the number contains @ surronded by
    140                         // a pager number and a domain name.
    141                         // e.g.
    142                         // o 1111 (at) domain.com
    143                         // x @domain.com
    144                         // x 1111@
    145                         final int indexOfAt = number.indexOf("@");
    146                         if ((typeCandidate == Phone.TYPE_PAGER
    147                                 && 0 < indexOfAt && indexOfAt < number.length() - 1)
    148                                 || type < 0
    149                                 || type == Phone.TYPE_CUSTOM) {
    150                             type = tmp;
    151                         }
    152                     } else if (type < 0) {
    153                         type = Phone.TYPE_CUSTOM;
    154                         label = typeString;
    155                     }
    156                 }
    157             }
    158         }
    159         if (type < 0) {
    160             if (hasPref) {
    161                 type = Phone.TYPE_MAIN;
    162             } else {
    163                 // default to TYPE_HOME
    164                 type = Phone.TYPE_HOME;
    165             }
    166         }
    167         if (isFax) {
    168             if (type == Phone.TYPE_HOME) {
    169                 type = Phone.TYPE_FAX_HOME;
    170             } else if (type == Phone.TYPE_WORK) {
    171                 type = Phone.TYPE_FAX_WORK;
    172             } else if (type == Phone.TYPE_OTHER) {
    173                 type = Phone.TYPE_OTHER_FAX;
    174             }
    175         }
    176         if (type == Phone.TYPE_CUSTOM) {
    177             return label;
    178         } else {
    179             return type;
    180         }
    181     }
    182 
    183     @SuppressWarnings("deprecation")
    184     public static boolean isMobilePhoneLabel(final String label) {
    185         // For backward compatibility.
    186         // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
    187         //         To support mobile type at that time, this custom label had been used.
    188         return (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME.equals(label)
    189                 || sMobilePhoneLabelSet.contains(label));
    190     }
    191 
    192     public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) {
    193         return sPhoneTypesUnknownToContactsSet.contains(label);
    194     }
    195 
    196     public static String getPropertyNameForIm(final int protocol) {
    197         return sKnownImPropNameMap_ItoS.get(protocol);
    198     }
    199 
    200     public static String[] sortNameElements(final int vcardType,
    201             final String familyName, final String middleName, final String givenName) {
    202         final String[] list = new String[3];
    203         final int nameOrderType = VCardConfig.getNameOrderType(vcardType);
    204         switch (nameOrderType) {
    205             case VCardConfig.NAME_ORDER_JAPANESE: {
    206                 if (containsOnlyPrintableAscii(familyName) &&
    207                         containsOnlyPrintableAscii(givenName)) {
    208                     list[0] = givenName;
    209                     list[1] = middleName;
    210                     list[2] = familyName;
    211                 } else {
    212                     list[0] = familyName;
    213                     list[1] = middleName;
    214                     list[2] = givenName;
    215                 }
    216                 break;
    217             }
    218             case VCardConfig.NAME_ORDER_EUROPE: {
    219                 list[0] = middleName;
    220                 list[1] = givenName;
    221                 list[2] = familyName;
    222                 break;
    223             }
    224             default: {
    225                 list[0] = givenName;
    226                 list[1] = middleName;
    227                 list[2] = familyName;
    228                 break;
    229             }
    230         }
    231         return list;
    232     }
    233 
    234     public static int getPhoneNumberFormat(final int vcardType) {
    235         if (VCardConfig.isJapaneseDevice(vcardType)) {
    236             return PhoneNumberUtils.FORMAT_JAPAN;
    237         } else {
    238             return PhoneNumberUtils.FORMAT_NANP;
    239         }
    240     }
    241 
    242     /**
    243      * Inserts postal data into the builder object.
    244      *
    245      * Note that the data structure of ContactsContract is different from that defined in vCard.
    246      * So some conversion may be performed in this method.
    247      */
    248     public static void insertStructuredPostalDataUsingContactsStruct(int vcardType,
    249             final ContentProviderOperation.Builder builder,
    250             final VCardEntry.PostalData postalData) {
    251         builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0);
    252         builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
    253 
    254         builder.withValue(StructuredPostal.TYPE, postalData.type);
    255         if (postalData.type == StructuredPostal.TYPE_CUSTOM) {
    256             builder.withValue(StructuredPostal.LABEL, postalData.label);
    257         }
    258 
    259         final String streetString;
    260         if (TextUtils.isEmpty(postalData.street)) {
    261             if (TextUtils.isEmpty(postalData.extendedAddress)) {
    262                 streetString = null;
    263             } else {
    264                 streetString = postalData.extendedAddress;
    265             }
    266         } else {
    267             if (TextUtils.isEmpty(postalData.extendedAddress)) {
    268                 streetString = postalData.street;
    269             } else {
    270                 streetString = postalData.street + " " + postalData.extendedAddress;
    271             }
    272         }
    273         builder.withValue(StructuredPostal.POBOX, postalData.pobox);
    274         builder.withValue(StructuredPostal.STREET, streetString);
    275         builder.withValue(StructuredPostal.CITY, postalData.localty);
    276         builder.withValue(StructuredPostal.REGION, postalData.region);
    277         builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode);
    278         builder.withValue(StructuredPostal.COUNTRY, postalData.country);
    279 
    280         builder.withValue(StructuredPostal.FORMATTED_ADDRESS,
    281                 postalData.getFormattedAddress(vcardType));
    282         if (postalData.isPrimary) {
    283             builder.withValue(Data.IS_PRIMARY, 1);
    284         }
    285     }
    286 
    287     public static String constructNameFromElements(final int vcardType,
    288             final String familyName, final String middleName, final String givenName) {
    289         return constructNameFromElements(vcardType, familyName, middleName, givenName,
    290                 null, null);
    291     }
    292 
    293     public static String constructNameFromElements(final int vcardType,
    294             final String familyName, final String middleName, final String givenName,
    295             final String prefix, final String suffix) {
    296         final StringBuilder builder = new StringBuilder();
    297         final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName);
    298         boolean first = true;
    299         if (!TextUtils.isEmpty(prefix)) {
    300             first = false;
    301             builder.append(prefix);
    302         }
    303         for (final String namePart : nameList) {
    304             if (!TextUtils.isEmpty(namePart)) {
    305                 if (first) {
    306                     first = false;
    307                 } else {
    308                     builder.append(' ');
    309                 }
    310                 builder.append(namePart);
    311             }
    312         }
    313         if (!TextUtils.isEmpty(suffix)) {
    314             if (!first) {
    315                 builder.append(' ');
    316             }
    317             builder.append(suffix);
    318         }
    319         return builder.toString();
    320     }
    321 
    322     public static List<String> constructListFromValue(final String value,
    323             final boolean isV30) {
    324         final List<String> list = new ArrayList<String>();
    325         StringBuilder builder = new StringBuilder();
    326         int length = value.length();
    327         for (int i = 0; i < length; i++) {
    328             char ch = value.charAt(i);
    329             if (ch == '\\' && i < length - 1) {
    330                 char nextCh = value.charAt(i + 1);
    331                 final String unescapedString =
    332                     (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) :
    333                         VCardParser_V21.unescapeCharacter(nextCh));
    334                 if (unescapedString != null) {
    335                     builder.append(unescapedString);
    336                     i++;
    337                 } else {
    338                     builder.append(ch);
    339                 }
    340             } else if (ch == ';') {
    341                 list.add(builder.toString());
    342                 builder = new StringBuilder();
    343             } else {
    344                 builder.append(ch);
    345             }
    346         }
    347         list.add(builder.toString());
    348         return list;
    349     }
    350 
    351     public static boolean containsOnlyPrintableAscii(final String...values) {
    352         if (values == null) {
    353             return true;
    354         }
    355         return containsOnlyPrintableAscii(Arrays.asList(values));
    356     }
    357 
    358     public static boolean containsOnlyPrintableAscii(final Collection<String> values) {
    359         if (values == null) {
    360             return true;
    361         }
    362         for (final String value : values) {
    363             if (TextUtils.isEmpty(value)) {
    364                 continue;
    365             }
    366             if (!TextUtils.isPrintableAsciiOnly(value)) {
    367                 return false;
    368             }
    369         }
    370         return true;
    371     }
    372 
    373     /**
    374      * This is useful when checking the string should be encoded into quoted-printable
    375      * or not, which is required by vCard 2.1.
    376      * See the definition of "7bit" in vCard 2.1 spec for more information.
    377      */
    378     public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) {
    379         if (values == null) {
    380             return true;
    381         }
    382         return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values));
    383     }
    384 
    385     public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) {
    386         if (values == null) {
    387             return true;
    388         }
    389         final int asciiFirst = 0x20;
    390         final int asciiLast = 0x7E;  // included
    391         for (final String value : values) {
    392             if (TextUtils.isEmpty(value)) {
    393                 continue;
    394             }
    395             final int length = value.length();
    396             for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
    397                 final int c = value.codePointAt(i);
    398                 if (!(asciiFirst <= c && c <= asciiLast)) {
    399                     return false;
    400                 }
    401             }
    402         }
    403         return true;
    404     }
    405 
    406     private static final Set<Character> sUnAcceptableAsciiInV21WordSet =
    407         new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' '));
    408 
    409     /**
    410      * This is useful since vCard 3.0 often requires the ("X-") properties and groups
    411      * should contain only alphabets, digits, and hyphen.
    412      *
    413      * Note: It is already known some devices (wrongly) outputs properties with characters
    414      *       which should not be in the field. One example is "X-GOOGLE TALK". We accept
    415      *       such kind of input but must never output it unless the target is very specific
    416      *       to the device which is able to parse the malformed input.
    417      */
    418     public static boolean containsOnlyAlphaDigitHyphen(final String...values) {
    419         if (values == null) {
    420             return true;
    421         }
    422         return containsOnlyAlphaDigitHyphen(Arrays.asList(values));
    423     }
    424 
    425     public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) {
    426         if (values == null) {
    427             return true;
    428         }
    429         final int upperAlphabetFirst = 0x41;  // A
    430         final int upperAlphabetAfterLast = 0x5b;  // [
    431         final int lowerAlphabetFirst = 0x61;  // a
    432         final int lowerAlphabetAfterLast = 0x7b;  // {
    433         final int digitFirst = 0x30;  // 0
    434         final int digitAfterLast = 0x3A;  // :
    435         final int hyphen = '-';
    436         for (final String str : values) {
    437             if (TextUtils.isEmpty(str)) {
    438                 continue;
    439             }
    440             final int length = str.length();
    441             for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
    442                 int codepoint = str.codePointAt(i);
    443                 if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) ||
    444                     (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) ||
    445                     (digitFirst <= codepoint && codepoint < digitAfterLast) ||
    446                     (codepoint == hyphen))) {
    447                     return false;
    448                 }
    449             }
    450         }
    451         return true;
    452     }
    453 
    454     /**
    455      * <P>
    456      * Returns true when the given String is categorized as "word" specified in vCard spec 2.1.
    457      * </P>
    458      * <P>
    459      * vCard 2.1 specifies:<BR />
    460      * word = &lt;any printable 7bit us-ascii except []=:., &gt;
    461      * </P>
    462      */
    463     public static boolean isV21Word(final String value) {
    464         if (TextUtils.isEmpty(value)) {
    465             return true;
    466         }
    467         final int asciiFirst = 0x20;
    468         final int asciiLast = 0x7E;  // included
    469         final int length = value.length();
    470         for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
    471             final int c = value.codePointAt(i);
    472             if (!(asciiFirst <= c && c <= asciiLast) ||
    473                     sUnAcceptableAsciiInV21WordSet.contains((char)c)) {
    474                 return false;
    475             }
    476         }
    477         return true;
    478     }
    479 
    480     public static String toHalfWidthString(final String orgString) {
    481         if (TextUtils.isEmpty(orgString)) {
    482             return null;
    483         }
    484         final StringBuilder builder = new StringBuilder();
    485         final int length = orgString.length();
    486         for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) {
    487             // All Japanese character is able to be expressed by char.
    488             // Do not need to use String#codepPointAt().
    489             final char ch = orgString.charAt(i);
    490             final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch);
    491             if (halfWidthText != null) {
    492                 builder.append(halfWidthText);
    493             } else {
    494                 builder.append(ch);
    495             }
    496         }
    497         return builder.toString();
    498     }
    499 
    500     /**
    501      * Guesses the format of input image. Currently just the first few bytes are used.
    502      * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when
    503      * the guess failed.
    504      * @param input Image as byte array.
    505      * @return The image type or null when the type cannot be determined.
    506      */
    507     public static String guessImageType(final byte[] input) {
    508         if (input == null) {
    509             return null;
    510         }
    511         if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') {
    512             return "GIF";
    513         } else if (input.length >= 4 && input[0] == (byte) 0x89
    514                 && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') {
    515             // Note: vCard 2.1 officially does not support PNG, but we may have it and
    516             //       using X- word like "X-PNG" may not let importers know it is PNG.
    517             //       So we use the String "PNG" as is...
    518             return "PNG";
    519         } else if (input.length >= 2 && input[0] == (byte) 0xff
    520                 && input[1] == (byte) 0xd8) {
    521             return "JPEG";
    522         } else {
    523             return null;
    524         }
    525     }
    526 
    527     /**
    528      * @return True when all the given values are null or empty Strings.
    529      */
    530     public static boolean areAllEmpty(final String...values) {
    531         if (values == null) {
    532             return true;
    533         }
    534 
    535         for (final String value : values) {
    536             if (!TextUtils.isEmpty(value)) {
    537                 return false;
    538             }
    539         }
    540         return true;
    541     }
    542 
    543     private VCardUtils() {
    544     }
    545 }
    546