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