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