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