VCardUtils.java revision 02117b3d19787ff65486b9f9db8abd338ae4c9f9
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 157 sPhoneTypesUnknownToContactsSet = new HashSet<String>(); 158 sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); 159 sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG); 160 sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); 161 sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); 162 163 sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); 164 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); 165 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); 166 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); 167 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 168 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, 169 VCardConstants.PROPERTY_X_GOOGLE_TALK); 170 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); 171 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); 172 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); 173 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); 174 175 // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) 176 // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) 177 // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone) 178 // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone) 179 sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList( 180 "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4", 181 "\uFF79\uFF72\uFF80\uFF72")); 182 } 183 184 public static String getPhoneTypeString(Integer type) { 185 return sKnownPhoneTypesMap_ItoS.get(type); 186 } 187 188 /** 189 * Returns Interger when the given types can be parsed as known type. Returns String object 190 * when not, which should be set to label. 191 */ 192 public static Object getPhoneTypeFromStrings(Collection<String> types, 193 String number) { 194 if (number == null) { 195 number = ""; 196 } 197 int type = -1; 198 String label = null; 199 boolean isFax = false; 200 boolean hasPref = false; 201 202 if (types != null) { 203 for (final String typeStringOrg : types) { 204 if (typeStringOrg == null) { 205 continue; 206 } 207 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 208 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 209 hasPref = true; 210 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_FAX)) { 211 isFax = true; 212 } else { 213 final String labelCandidate; 214 if (typeStringUpperCase.startsWith("X-") && type < 0) { 215 labelCandidate = typeStringOrg.substring(2); 216 } else { 217 labelCandidate = typeStringOrg; 218 } 219 if (labelCandidate.length() == 0) { 220 continue; 221 } 222 // e.g. "home" -> TYPE_HOME 223 final Integer tmp = sKnownPhoneTypeMap_StoI.get(labelCandidate.toUpperCase()); 224 if (tmp != null) { 225 final int typeCandidate = tmp; 226 // TYPE_PAGER is prefered when the number contains @ surronded by 227 // a pager number and a domain name. 228 // e.g. 229 // o 1111@domain.com 230 // x @domain.com 231 // x 1111@ 232 final int indexOfAt = number.indexOf("@"); 233 if ((typeCandidate == Phone.TYPE_PAGER 234 && 0 < indexOfAt && indexOfAt < number.length() - 1) 235 || type < 0 236 || type == Phone.TYPE_CUSTOM) { 237 type = tmp; 238 } 239 } else if (type < 0) { 240 type = Phone.TYPE_CUSTOM; 241 label = labelCandidate; 242 } 243 } 244 } 245 } 246 if (type < 0) { 247 if (hasPref) { 248 type = Phone.TYPE_MAIN; 249 } else { 250 // default to TYPE_HOME 251 type = Phone.TYPE_HOME; 252 } 253 } 254 if (isFax) { 255 if (type == Phone.TYPE_HOME) { 256 type = Phone.TYPE_FAX_HOME; 257 } else if (type == Phone.TYPE_WORK) { 258 type = Phone.TYPE_FAX_WORK; 259 } else if (type == Phone.TYPE_OTHER) { 260 type = Phone.TYPE_OTHER_FAX; 261 } 262 } 263 if (type == Phone.TYPE_CUSTOM) { 264 return label; 265 } else { 266 return type; 267 } 268 } 269 270 @SuppressWarnings("deprecation") 271 public static boolean isMobilePhoneLabel(final String label) { 272 // For backward compatibility. 273 // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. 274 // To support mobile type at that time, this custom label had been used. 275 return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label)); 276 } 277 278 public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { 279 return sPhoneTypesUnknownToContactsSet.contains(label); 280 } 281 282 public static String getPropertyNameForIm(final int protocol) { 283 return sKnownImPropNameMap_ItoS.get(protocol); 284 } 285 286 public static String[] sortNameElements(final int vcardType, 287 final String familyName, final String middleName, final String givenName) { 288 final String[] list = new String[3]; 289 final int nameOrderType = VCardConfig.getNameOrderType(vcardType); 290 switch (nameOrderType) { 291 case VCardConfig.NAME_ORDER_JAPANESE: { 292 if (containsOnlyPrintableAscii(familyName) && 293 containsOnlyPrintableAscii(givenName)) { 294 list[0] = givenName; 295 list[1] = middleName; 296 list[2] = familyName; 297 } else { 298 list[0] = familyName; 299 list[1] = middleName; 300 list[2] = givenName; 301 } 302 break; 303 } 304 case VCardConfig.NAME_ORDER_EUROPE: { 305 list[0] = middleName; 306 list[1] = givenName; 307 list[2] = familyName; 308 break; 309 } 310 default: { 311 list[0] = givenName; 312 list[1] = middleName; 313 list[2] = familyName; 314 break; 315 } 316 } 317 return list; 318 } 319 320 public static int getPhoneNumberFormat(final int vcardType) { 321 if (VCardConfig.isJapaneseDevice(vcardType)) { 322 return PhoneNumberUtils.FORMAT_JAPAN; 323 } else { 324 return PhoneNumberUtils.FORMAT_NANP; 325 } 326 } 327 328 /** 329 * <p> 330 * Inserts postal data into the builder object. 331 * </p> 332 * <p> 333 * Note that the data structure of ContactsContract is different from that defined in vCard. 334 * So some conversion may be performed in this method. 335 * </p> 336 */ 337 public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, 338 final ContentProviderOperation.Builder builder, 339 final VCardEntry.PostalData postalData) { 340 builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); 341 builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); 342 343 builder.withValue(StructuredPostal.TYPE, postalData.type); 344 if (postalData.type == StructuredPostal.TYPE_CUSTOM) { 345 builder.withValue(StructuredPostal.LABEL, postalData.label); 346 } 347 348 final String streetString; 349 if (TextUtils.isEmpty(postalData.street)) { 350 if (TextUtils.isEmpty(postalData.extendedAddress)) { 351 streetString = null; 352 } else { 353 streetString = postalData.extendedAddress; 354 } 355 } else { 356 if (TextUtils.isEmpty(postalData.extendedAddress)) { 357 streetString = postalData.street; 358 } else { 359 streetString = postalData.street + " " + postalData.extendedAddress; 360 } 361 } 362 builder.withValue(StructuredPostal.POBOX, postalData.pobox); 363 builder.withValue(StructuredPostal.STREET, streetString); 364 builder.withValue(StructuredPostal.CITY, postalData.localty); 365 builder.withValue(StructuredPostal.REGION, postalData.region); 366 builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); 367 builder.withValue(StructuredPostal.COUNTRY, postalData.country); 368 369 builder.withValue(StructuredPostal.FORMATTED_ADDRESS, 370 postalData.getFormattedAddress(vcardType)); 371 if (postalData.isPrimary) { 372 builder.withValue(Data.IS_PRIMARY, 1); 373 } 374 } 375 376 public static String constructNameFromElements(final int vcardType, 377 final String familyName, final String middleName, final String givenName) { 378 return constructNameFromElements(vcardType, familyName, middleName, givenName, 379 null, null); 380 } 381 382 public static String constructNameFromElements(final int vcardType, 383 final String familyName, final String middleName, final String givenName, 384 final String prefix, final String suffix) { 385 final StringBuilder builder = new StringBuilder(); 386 final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName); 387 boolean first = true; 388 if (!TextUtils.isEmpty(prefix)) { 389 first = false; 390 builder.append(prefix); 391 } 392 for (final String namePart : nameList) { 393 if (!TextUtils.isEmpty(namePart)) { 394 if (first) { 395 first = false; 396 } else { 397 builder.append(' '); 398 } 399 builder.append(namePart); 400 } 401 } 402 if (!TextUtils.isEmpty(suffix)) { 403 if (!first) { 404 builder.append(' '); 405 } 406 builder.append(suffix); 407 } 408 return builder.toString(); 409 } 410 411 /** 412 * Splits the given value into pieces using the delimiter ';' inside it. 413 * 414 * Escaped characters in those values are automatically unescaped into original form. 415 */ 416 public static List<String> constructListFromValue(final String value, 417 final int vcardType) { 418 final List<String> list = new ArrayList<String>(); 419 StringBuilder builder = new StringBuilder(); 420 final int length = value.length(); 421 for (int i = 0; i < length; i++) { 422 char ch = value.charAt(i); 423 if (ch == '\\' && i < length - 1) { 424 char nextCh = value.charAt(i + 1); 425 final String unescapedString; 426 if (VCardConfig.isVersion40(vcardType)) { 427 unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh); 428 } else if (VCardConfig.isVersion30(vcardType)) { 429 unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh); 430 } else { 431 if (!VCardConfig.isVersion21(vcardType)) { 432 // Unknown vCard type 433 Log.w(LOG_TAG, "Unknown vCard type"); 434 } 435 unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh); 436 } 437 438 if (unescapedString != null) { 439 builder.append(unescapedString); 440 i++; 441 } else { 442 builder.append(ch); 443 } 444 } else if (ch == ';') { 445 list.add(builder.toString()); 446 builder = new StringBuilder(); 447 } else { 448 builder.append(ch); 449 } 450 } 451 list.add(builder.toString()); 452 return list; 453 } 454 455 public static boolean containsOnlyPrintableAscii(final String...values) { 456 if (values == null) { 457 return true; 458 } 459 return containsOnlyPrintableAscii(Arrays.asList(values)); 460 } 461 462 public static boolean containsOnlyPrintableAscii(final Collection<String> values) { 463 if (values == null) { 464 return true; 465 } 466 for (final String value : values) { 467 if (TextUtils.isEmpty(value)) { 468 continue; 469 } 470 if (!TextUtilsPort.isPrintableAsciiOnly(value)) { 471 return false; 472 } 473 } 474 return true; 475 } 476 477 /** 478 * <p> 479 * This is useful when checking the string should be encoded into quoted-printable 480 * or not, which is required by vCard 2.1. 481 * </p> 482 * <p> 483 * See the definition of "7bit" in vCard 2.1 spec for more information. 484 * </p> 485 */ 486 public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { 487 if (values == null) { 488 return true; 489 } 490 return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); 491 } 492 493 public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) { 494 if (values == null) { 495 return true; 496 } 497 final int asciiFirst = 0x20; 498 final int asciiLast = 0x7E; // included 499 for (final String value : values) { 500 if (TextUtils.isEmpty(value)) { 501 continue; 502 } 503 final int length = value.length(); 504 for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { 505 final int c = value.codePointAt(i); 506 if (!(asciiFirst <= c && c <= asciiLast)) { 507 return false; 508 } 509 } 510 } 511 return true; 512 } 513 514 private static final Set<Character> sUnAcceptableAsciiInV21WordSet = 515 new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); 516 517 /** 518 * <p> 519 * This is useful since vCard 3.0 often requires the ("X-") properties and groups 520 * should contain only alphabets, digits, and hyphen. 521 * </p> 522 * <p> 523 * Note: It is already known some devices (wrongly) outputs properties with characters 524 * which should not be in the field. One example is "X-GOOGLE TALK". We accept 525 * such kind of input but must never output it unless the target is very specific 526 * to the device which is able to parse the malformed input. 527 * </p> 528 */ 529 public static boolean containsOnlyAlphaDigitHyphen(final String...values) { 530 if (values == null) { 531 return true; 532 } 533 return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); 534 } 535 536 public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) { 537 if (values == null) { 538 return true; 539 } 540 final int upperAlphabetFirst = 0x41; // A 541 final int upperAlphabetAfterLast = 0x5b; // [ 542 final int lowerAlphabetFirst = 0x61; // a 543 final int lowerAlphabetAfterLast = 0x7b; // { 544 final int digitFirst = 0x30; // 0 545 final int digitAfterLast = 0x3A; // : 546 final int hyphen = '-'; 547 for (final String str : values) { 548 if (TextUtils.isEmpty(str)) { 549 continue; 550 } 551 final int length = str.length(); 552 for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { 553 int codepoint = str.codePointAt(i); 554 if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) || 555 (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) || 556 (digitFirst <= codepoint && codepoint < digitAfterLast) || 557 (codepoint == hyphen))) { 558 return false; 559 } 560 } 561 } 562 return true; 563 } 564 565 public static boolean containsOnlyWhiteSpaces(final String...values) { 566 if (values == null) { 567 return true; 568 } 569 return containsOnlyWhiteSpaces(Arrays.asList(values)); 570 } 571 572 public static boolean containsOnlyWhiteSpaces(final Collection<String> values) { 573 if (values == null) { 574 return true; 575 } 576 for (final String str : values) { 577 if (TextUtils.isEmpty(str)) { 578 continue; 579 } 580 final int length = str.length(); 581 for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { 582 if (!Character.isWhitespace(str.codePointAt(i))) { 583 return false; 584 } 585 } 586 } 587 return true; 588 } 589 590 /** 591 * <p> 592 * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. 593 * </p> 594 * <p> 595 * vCard 2.1 specifies:<br /> 596 * word = <any printable 7bit us-ascii except []=:., > 597 * </p> 598 */ 599 public static boolean isV21Word(final String value) { 600 if (TextUtils.isEmpty(value)) { 601 return true; 602 } 603 final int asciiFirst = 0x20; 604 final int asciiLast = 0x7E; // included 605 final int length = value.length(); 606 for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { 607 final int c = value.codePointAt(i); 608 if (!(asciiFirst <= c && c <= asciiLast) || 609 sUnAcceptableAsciiInV21WordSet.contains((char)c)) { 610 return false; 611 } 612 } 613 return true; 614 } 615 616 private static final int[] sEscapeIndicatorsV30 = new int[]{ 617 ':', ';', ',', ' ' 618 }; 619 620 private static final int[] sEscapeIndicatorsV40 = new int[]{ 621 ';', ':' 622 }; 623 624 /** 625 * <P> 626 * Returns String available as parameter value in vCard 3.0. 627 * </P> 628 * <P> 629 * RFC 2426 requires vCard composer to quote parameter values when it contains 630 * semi-colon, for example (See RFC 2426 for more information). 631 * This method checks whether the given String can be used without quotes. 632 * </P> 633 * <P> 634 * Note: We remove DQUOTE inside the given value silently for now. 635 * </P> 636 */ 637 public static String toStringAsV30ParamValue(String value) { 638 return toStringAsParamValue(value, sEscapeIndicatorsV30); 639 } 640 641 public static String toStringAsV40ParamValue(String value) { 642 return toStringAsParamValue(value, sEscapeIndicatorsV40); 643 } 644 645 private static String toStringAsParamValue(String value, final int[] escapeIndicators) { 646 if (TextUtils.isEmpty(value)) { 647 value = ""; 648 } 649 final int asciiFirst = 0x20; 650 final int asciiLast = 0x7E; // included 651 final StringBuilder builder = new StringBuilder(); 652 final int length = value.length(); 653 boolean needQuote = false; 654 for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { 655 final int codePoint = value.codePointAt(i); 656 if (codePoint < asciiFirst || codePoint == '"') { 657 // CTL characters and DQUOTE are never accepted. Remove them. 658 continue; 659 } 660 builder.appendCodePoint(codePoint); 661 for (int indicator : escapeIndicators) { 662 if (codePoint == indicator) { 663 needQuote = true; 664 break; 665 } 666 } 667 } 668 669 final String result = builder.toString(); 670 return ((result.isEmpty() || VCardUtils.containsOnlyWhiteSpaces(result)) 671 ? "" 672 : (needQuote ? ('"' + result + '"') 673 : result)); 674 } 675 676 public static String toHalfWidthString(final String orgString) { 677 if (TextUtils.isEmpty(orgString)) { 678 return null; 679 } 680 final StringBuilder builder = new StringBuilder(); 681 final int length = orgString.length(); 682 for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) { 683 // All Japanese character is able to be expressed by char. 684 // Do not need to use String#codepPointAt(). 685 final char ch = orgString.charAt(i); 686 final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); 687 if (halfWidthText != null) { 688 builder.append(halfWidthText); 689 } else { 690 builder.append(ch); 691 } 692 } 693 return builder.toString(); 694 } 695 696 /** 697 * Guesses the format of input image. Currently just the first few bytes are used. 698 * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when 699 * the guess failed. 700 * @param input Image as byte array. 701 * @return The image type or null when the type cannot be determined. 702 */ 703 public static String guessImageType(final byte[] input) { 704 if (input == null) { 705 return null; 706 } 707 if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') { 708 return "GIF"; 709 } else if (input.length >= 4 && input[0] == (byte) 0x89 710 && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') { 711 // Note: vCard 2.1 officially does not support PNG, but we may have it and 712 // using X- word like "X-PNG" may not let importers know it is PNG. 713 // So we use the String "PNG" as is... 714 return "PNG"; 715 } else if (input.length >= 2 && input[0] == (byte) 0xff 716 && input[1] == (byte) 0xd8) { 717 return "JPEG"; 718 } else { 719 return null; 720 } 721 } 722 723 /** 724 * @return True when all the given values are null or empty Strings. 725 */ 726 public static boolean areAllEmpty(final String...values) { 727 if (values == null) { 728 return true; 729 } 730 731 for (final String value : values) { 732 if (!TextUtils.isEmpty(value)) { 733 return false; 734 } 735 } 736 return true; 737 } 738 739 //// The methods bellow may be used by unit test. 740 741 /** 742 * Unquotes given Quoted-Printable value. value must not be null. 743 */ 744 public static String parseQuotedPrintable( 745 final String value, boolean strictLineBreaking, 746 String sourceCharset, String targetCharset) { 747 // "= " -> " ", "=\t" -> "\t". 748 // Previous code had done this replacement. Keep on the safe side. 749 final String quotedPrintable; 750 { 751 final StringBuilder builder = new StringBuilder(); 752 final int length = value.length(); 753 for (int i = 0; i < length; i++) { 754 char ch = value.charAt(i); 755 if (ch == '=' && i < length - 1) { 756 char nextCh = value.charAt(i + 1); 757 if (nextCh == ' ' || nextCh == '\t') { 758 builder.append(nextCh); 759 i++; 760 continue; 761 } 762 } 763 builder.append(ch); 764 } 765 quotedPrintable = builder.toString(); 766 } 767 768 String[] lines; 769 if (strictLineBreaking) { 770 lines = quotedPrintable.split("\r\n"); 771 } else { 772 StringBuilder builder = new StringBuilder(); 773 final int length = quotedPrintable.length(); 774 ArrayList<String> list = new ArrayList<String>(); 775 for (int i = 0; i < length; i++) { 776 char ch = quotedPrintable.charAt(i); 777 if (ch == '\n') { 778 list.add(builder.toString()); 779 builder = new StringBuilder(); 780 } else if (ch == '\r') { 781 list.add(builder.toString()); 782 builder = new StringBuilder(); 783 if (i < length - 1) { 784 char nextCh = quotedPrintable.charAt(i + 1); 785 if (nextCh == '\n') { 786 i++; 787 } 788 } 789 } else { 790 builder.append(ch); 791 } 792 } 793 final String lastLine = builder.toString(); 794 if (lastLine.length() > 0) { 795 list.add(lastLine); 796 } 797 lines = list.toArray(new String[0]); 798 } 799 800 final StringBuilder builder = new StringBuilder(); 801 for (String line : lines) { 802 if (line.endsWith("=")) { 803 line = line.substring(0, line.length() - 1); 804 } 805 builder.append(line); 806 } 807 808 final String rawString = builder.toString(); 809 if (TextUtils.isEmpty(rawString)) { 810 Log.w(LOG_TAG, "Given raw string is empty."); 811 } 812 813 byte[] rawBytes = null; 814 try { 815 rawBytes = rawString.getBytes(sourceCharset); 816 } catch (UnsupportedEncodingException e) { 817 Log.w(LOG_TAG, "Failed to decode: " + sourceCharset); 818 rawBytes = rawString.getBytes(); 819 } 820 821 byte[] decodedBytes = null; 822 try { 823 decodedBytes = QuotedPrintableCodecPort.decodeQuotedPrintable(rawBytes); 824 } catch (DecoderException e) { 825 Log.e(LOG_TAG, "DecoderException is thrown."); 826 decodedBytes = rawBytes; 827 } 828 829 try { 830 return new String(decodedBytes, targetCharset); 831 } catch (UnsupportedEncodingException e) { 832 Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); 833 return new String(decodedBytes); 834 } 835 } 836 837 public static final VCardParser getAppropriateParser(int vcardType) 838 throws VCardException { 839 if (VCardConfig.isVersion21(vcardType)) { 840 return new VCardParser_V21(); 841 } else if (VCardConfig.isVersion30(vcardType)) { 842 return new VCardParser_V30(); 843 } else if (VCardConfig.isVersion40(vcardType)) { 844 return new VCardParser_V40(); 845 } else { 846 throw new VCardException("Version is not specified"); 847 } 848 } 849 850 public static final String convertStringCharset( 851 String originalString, String sourceCharset, String targetCharset) { 852 if (sourceCharset.equalsIgnoreCase(targetCharset)) { 853 return originalString; 854 } 855 final Charset charset = Charset.forName(sourceCharset); 856 final ByteBuffer byteBuffer = charset.encode(originalString); 857 // byteBuffer.array() "may" return byte array which is larger than 858 // byteBuffer.remaining(). Here, we keep on the safe side. 859 final byte[] bytes = new byte[byteBuffer.remaining()]; 860 byteBuffer.get(bytes); 861 try { 862 return new String(bytes, targetCharset); 863 } catch (UnsupportedEncodingException e) { 864 Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); 865 return null; 866 } 867 } 868 869 // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean 870 871 private VCardUtils() { 872 } 873} 874