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