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 { DecoderException(String pMessage)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 = '='; decodeQuotedPrintable(byte[] bytes)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 { formatNumber(String source, int defaultFormattingType)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 { isPrintableAscii(final char c)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 isPrintableAsciiOnly(final CharSequence str)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 sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR)132 sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR)133 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER)134 sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER)135 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN)136 sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN)137 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); 138 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME)139 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK)140 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE)141 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); 142 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER)143 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK)144 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, 145 Phone.TYPE_CALLBACK); sKnownPhoneTypeMap_StoI.put( VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN)146 sKnownPhoneTypeMap_StoI.put( 147 VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO)148 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD)149 sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, 150 Phone.TYPE_TTY_TDD); sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, Phone.TYPE_ASSISTANT)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). sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_VOICE, Phone.TYPE_OTHER)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>(); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM)163 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN)164 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO)165 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME)166 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, VCardConstants.PROPERTY_X_GOOGLE_TALK)167 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, 168 VCardConstants.PROPERTY_X_GOOGLE_TALK); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ)169 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER)170 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ)171 sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING)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 getPhoneTypeString(Integer type)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 */ getPhoneTypeFromStrings(Collection<String> types, String number)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@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") isMobilePhoneLabel(final String label)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 isValidInV21ButUnknownToContactsPhoteType(final String label)284 public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { 285 return sPhoneTypesUnknownToContactsSet.contains(label); 286 } 287 getPropertyNameForIm(final int protocol)288 public static String getPropertyNameForIm(final int protocol) { 289 return sKnownImPropNameMap_ItoS.get(protocol); 290 } 291 sortNameElements(final int nameOrder, final String familyName, final String middleName, final String givenName)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 getPhoneNumberFormat(final int vcardType)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 constructNameFromElements(final int nameOrder, final String familyName, final String middleName, final String givenName)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 constructNameFromElements(final int nameOrder, final String familyName, final String middleName, final String givenName, final String prefix, final String suffix)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 */ constructListFromValue(final String value, final int vcardType)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 containsOnlyPrintableAscii(final String...values)413 public static boolean containsOnlyPrintableAscii(final String...values) { 414 if (values == null) { 415 return true; 416 } 417 return containsOnlyPrintableAscii(Arrays.asList(values)); 418 } 419 containsOnlyPrintableAscii(final Collection<String> values)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 */ containsOnlyNonCrLfPrintableAscii(final String...values)444 public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { 445 if (values == null) { 446 return true; 447 } 448 return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); 449 } 450 containsOnlyNonCrLfPrintableAscii(final Collection<String> values)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 */ containsOnlyAlphaDigitHyphen(final String...values)487 public static boolean containsOnlyAlphaDigitHyphen(final String...values) { 488 if (values == null) { 489 return true; 490 } 491 return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); 492 } 493 containsOnlyAlphaDigitHyphen(final Collection<String> values)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 containsOnlyWhiteSpaces(final String...values)523 public static boolean containsOnlyWhiteSpaces(final String...values) { 524 if (values == null) { 525 return true; 526 } 527 return containsOnlyWhiteSpaces(Arrays.asList(values)); 528 } 529 containsOnlyWhiteSpaces(final Collection<String> values)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 */ isV21Word(final String value)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 */ toStringAsV30ParamValue(String value)595 public static String toStringAsV30ParamValue(String value) { 596 return toStringAsParamValue(value, sEscapeIndicatorsV30); 597 } 598 toStringAsV40ParamValue(String value)599 public static String toStringAsV40ParamValue(String value) { 600 return toStringAsParamValue(value, sEscapeIndicatorsV40); 601 } 602 toStringAsParamValue(String value, final int[] escapeIndicators)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 toHalfWidthString(final String orgString)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 */ guessImageType(final byte[] input)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 */ areAllEmpty(final String...values)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 */ appearsLikeAndroidVCardQuotedPrintable(String value)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 */ parseQuotedPrintable( final String value, boolean strictLineBreaking, String sourceCharset, String targetCharset)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 getAppropriateParser(int vcardType)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 convertStringCharset( String originalString, String sourceCharset, String targetCharset)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 VCardUtils()853 private VCardUtils() { 854 } 855 } 856