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 = <any printable 7bit us-ascii except []=:., > 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