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