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