content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.android.vcard;
  
  import android.provider.ContactsContract.CommonDataKinds.Im;
  import android.provider.ContactsContract.CommonDataKinds.Phone;
  import android.telephony.PhoneNumberUtils;
  import android.text.SpannableStringBuilder;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.android.vcard.exception.VCardException;
  
  import java.io.ByteArrayOutputStream;
  import java.io.UnsupportedEncodingException;
  import java.nio.ByteBuffer;
  import java.nio.charset.Charset;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * Utilities for VCard handling codes.
   */
  public class VCardUtils {
      private static final String LOG_TAG = VCardConstants.LOG_TAG;
  
      /**
       * See org.apache.commons.codec.DecoderException
       */
      private static class DecoderException extends Exception {
          public DecoderException(String pMessage) {
              super(pMessage);
          }
      }
  
      /**
       * See org.apache.commons.codec.net.QuotedPrintableCodec
       */
      private static class QuotedPrintableCodecPort {
          private static byte ESCAPE_CHAR = '=';
          public static final byte[] decodeQuotedPrintable(byte[] bytes)
                  throws DecoderException {
              if (bytes == null) {
                  return null;
              }
              ByteArrayOutputStream buffer = new ByteArrayOutputStream();
              for (int i = 0; i < bytes.length; i++) {
                  int b = bytes[i];
                  if (b == ESCAPE_CHAR) {
                      try {
                          int u = Character.digit((char) bytes[++i], 16);
                          int l = Character.digit((char) bytes[++i], 16);
                          if (u == -1 || l == -1) {
                              throw new DecoderException("Invalid quoted-printable encoding");
                          }
                          buffer.write((char) ((u << 4) + l));
                      } catch (ArrayIndexOutOfBoundsException e) {
                          throw new DecoderException("Invalid quoted-printable encoding");
                      }
                  } else {
                      buffer.write(b);
                  }
              }
              return buffer.toByteArray();
          }
      }
  
      /**
       * Ported methods which are hidden in {@link PhoneNumberUtils}.
       */
      public static class PhoneNumberUtilsPort {
          public static String formatNumber(String source, int defaultFormattingType) {
              final SpannableStringBuilder text = new SpannableStringBuilder(source);
              PhoneNumberUtils.formatNumber(text, defaultFormattingType);
              return text.toString();
          }
      }
  
      /**
       * Ported methods which are hidden in {@link TextUtils}.
       */
      public static class TextUtilsPort {
          public static boolean isPrintableAscii(final char c) {
              final int asciiFirst = 0x20;
              final int asciiLast = 0x7E;  // included
              return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
          }
  
          public static boolean isPrintableAsciiOnly(final CharSequence str) {
              final int len = str.length();
              for (int i = 0; i < len; i++) {
                  if (!isPrintableAscii(str.charAt(i))) {
                      return false;
                  }
              }
              return true;
          }
      }
  
      // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is
      // converted to two parameter Strings. These only contain some minor fields valid in both
      // vCard and current (as of 2009-08-07) Contacts structure.
      private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS;
      private static final Set<String> sPhoneTypesUnknownToContactsSet;
      private static final Map<String, Integer> sKnownPhoneTypeMap_StoI;
      private static final Map<Integer, String> sKnownImPropNameMap_ItoS;
      private static final Set<String> sMobilePhoneLabelSet;
  
      static {
          sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>();
          sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>();
  
          sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR);
          sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER);
          sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN);
  
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE);
  
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK,
                  Phone.TYPE_CALLBACK);
          sKnownPhoneTypeMap_StoI.put(
                  VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN);
          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);
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT,
                  Phone.TYPE_ASSISTANT);
          // OTHER (default in Android) should correspond to VOICE (default in vCard).
          sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_VOICE, Phone.TYPE_OTHER);
  
          sPhoneTypesUnknownToContactsSet = new HashSet<String>();
          sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM);
          sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG);
          sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS);
          sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO);
  
          sKnownImPropNameMap_ItoS = new HashMap<Integer, String>();
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK,
                  VCardConstants.PROPERTY_X_GOOGLE_TALK);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ);
          sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING);
  
          // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone)
          // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone)
          // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone)
          // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone)
          sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList(
                  "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4",
                  "\uFF79\uFF72\uFF80\uFF72"));
      }
  
      public static String getPhoneTypeString(Integer type) {
          return sKnownPhoneTypesMap_ItoS.get(type);
      }
  
      /**
       * Returns Interger when the given types can be parsed as known type. Returns String object
       * when not, which should be set to label.
       */
      public static Object getPhoneTypeFromStrings(Collection<String> types,
              String number) {
          if (number == null) {
              number = "";
          }
          int type = -1;
          String label = null;
          boolean isFax = false;
          boolean hasPref = false;
  
          if (types != null) {
              for (final String typeStringOrg : types) {
                  if (typeStringOrg == null) {
                      continue;
                  }
                  final String typeStringUpperCase = typeStringOrg.toUpperCase();
                  if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
                      hasPref = true;
                  } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_FAX)) {
                      isFax = true;
                  } else {
                      final String labelCandidate;
                      if (typeStringUpperCase.startsWith("X-") && type < 0) {
                          labelCandidate = typeStringOrg.substring(2);
                      } else {
                          labelCandidate = typeStringOrg;
                      }
                      if (labelCandidate.length() == 0) {
                          continue;
                      }
                      // e.g. "home" -> TYPE_HOME
                      final Integer tmp = sKnownPhoneTypeMap_StoI.get(labelCandidate.toUpperCase());
                      if (tmp != null) {
                          final int typeCandidate = tmp;
                          // 1. If a type isn't specified yet, we'll choose the new type candidate.
                          // 2. If the current type is default one (OTHER) or custom one, we'll
                          // prefer more specific types specified in the vCard. Note that OTHER and
                          // the other different types may appear simultaneously here, since vCard
                          // allow to have VOICE and HOME/WORK in one line.
                          // e.g. "TEL;WORK;VOICE:1" -> WORK + OTHER -> Type should be WORK
                          // 3. TYPE_PAGER is prefered when the number contains @ surronded by
                          // a pager number and a domain name.
                          // e.g.
                          // o 1111@domain.com
                          // x @domain.com
                          // x 1111@
                          final int indexOfAt = number.indexOf("@");
                          if ((typeCandidate == Phone.TYPE_PAGER
                                  && 0 < indexOfAt && indexOfAt < number.length() - 1)
                                  || type < 0
                                  || type == Phone.TYPE_CUSTOM
                                  || type == Phone.TYPE_OTHER) {
                              type = tmp;
                          }
                      } else if (type < 0) {
                          type = Phone.TYPE_CUSTOM;
                          label = labelCandidate;
                      }
                  }
              }
          }
          if (type < 0) {
              if (hasPref) {
                  type = Phone.TYPE_MAIN;
              } else {
                  // default to TYPE_HOME
                  type = Phone.TYPE_HOME;
              }
          }
          if (isFax) {
              if (type == Phone.TYPE_HOME) {
                  type = Phone.TYPE_FAX_HOME;
              } else if (type == Phone.TYPE_WORK) {
                  type = Phone.TYPE_FAX_WORK;
              } else if (type == Phone.TYPE_OTHER) {
                  type = Phone.TYPE_OTHER_FAX;
              }
          }
          if (type == Phone.TYPE_CUSTOM) {
              return label;
          } else {
              return type;
          }
      }
  
      @SuppressWarnings("deprecation")
      public static boolean isMobilePhoneLabel(final String label) {
          // For backward compatibility.
          // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
          //         To support mobile type at that time, this custom label had been used.
          return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label));
      }
  
      public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) {
          return sPhoneTypesUnknownToContactsSet.contains(label);
      }
  
      public static String getPropertyNameForIm(final int protocol) {
          return sKnownImPropNameMap_ItoS.get(protocol);
      }
  
      public static String[] sortNameElements(final int nameOrder,
              final String familyName, final String middleName, final String givenName) {
          final String[] list = new String[3];
          final int nameOrderType = VCardConfig.getNameOrderType(nameOrder);
          switch (nameOrderType) {
              case VCardConfig.NAME_ORDER_JAPANESE: {
                  if (containsOnlyPrintableAscii(familyName) &&
                          containsOnlyPrintableAscii(givenName)) {
                      list[0] = givenName;
                      list[1] = middleName;
                      list[2] = familyName;
                  } else {
                      list[0] = familyName;
                      list[1] = middleName;
                      list[2] = givenName;
                  }
                  break;
              }
              case VCardConfig.NAME_ORDER_EUROPE: {
                  list[0] = middleName;
                  list[1] = givenName;
                  list[2] = familyName;
                  break;
              }
              default: {
                  list[0] = givenName;
                  list[1] = middleName;
                  list[2] = familyName;
                  break;
              }
          }
          return list;
      }
  
      public static int getPhoneNumberFormat(final int vcardType) {
          if (VCardConfig.isJapaneseDevice(vcardType)) {
              return PhoneNumberUtils.FORMAT_JAPAN;
          } else {
              return PhoneNumberUtils.FORMAT_NANP;
          }
      }
  
      public static String constructNameFromElements(final int nameOrder,
              final String familyName, final String middleName, final String givenName) {
          return constructNameFromElements(nameOrder, familyName, middleName, givenName,
                  null, null);
      }
  
      public static String constructNameFromElements(final int nameOrder,
              final String familyName, final String middleName, final String givenName,
              final String prefix, final String suffix) {
          final StringBuilder builder = new StringBuilder();
          final String[] nameList = sortNameElements(nameOrder, familyName, middleName, givenName);
          boolean first = true;
          if (!TextUtils.isEmpty(prefix)) {
              first = false;
              builder.append(prefix);
          }
          for (final String namePart : nameList) {
              if (!TextUtils.isEmpty(namePart)) {
                  if (first) {
                      first = false;
                  } else {
                      builder.append(' ');
                  }
                  builder.append(namePart);
              }
          }
          if (!TextUtils.isEmpty(suffix)) {
              if (!first) {
                  builder.append(' ');
              }
              builder.append(suffix);
          }
          return builder.toString();
      }
  
      /**
       * Splits the given value into pieces using the delimiter ';' inside it.
       *
       * Escaped characters in those values are automatically unescaped into original form.
       */
      public static List<String> constructListFromValue(final String value,
              final int vcardType) {
          final List<String> list = new ArrayList<String>();
          StringBuilder builder = new StringBuilder();
          final int length = value.length();
          for (int i = 0; i < length; i++) {
              char ch = value.charAt(i);
              if (ch == '\\' && i < length - 1) {
                  char nextCh = value.charAt(i + 1);
                  final String unescapedString;
                  if (VCardConfig.isVersion40(vcardType)) {
                      unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh);
                  } else if (VCardConfig.isVersion30(vcardType)) {
                      unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh);
                  } else {
                      if (!VCardConfig.isVersion21(vcardType)) {
                          // Unknown vCard type
                          Log.w(LOG_TAG, "Unknown vCard type");
                      }
                      unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh);
                  }
  
                  if (unescapedString != null) {
                      builder.append(unescapedString);
                      i++;
                  } else {
                      builder.append(ch);
                  }
              } else if (ch == ';') {
                  list.add(builder.toString());
                  builder = new StringBuilder();
              } else {
                  builder.append(ch);
              }
          }
          list.add(builder.toString());
          return list;
      }
  
      public static boolean containsOnlyPrintableAscii(final String...values) {
          if (values == null) {
              return true;
          }
          return containsOnlyPrintableAscii(Arrays.asList(values));
      }
  
      public static boolean containsOnlyPrintableAscii(final Collection<String> values) {
          if (values == null) {
              return true;
          }
          for (final String value : values) {
              if (TextUtils.isEmpty(value)) {
                  continue;
              }
              if (!TextUtilsPort.isPrintableAsciiOnly(value)) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * <p>
       * This is useful when checking the string should be encoded into quoted-printable
       * or not, which is required by vCard 2.1.
       * </p>
       * <p>
       * See the definition of "7bit" in vCard 2.1 spec for more information.
       * </p>
       */
      public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) {
          if (values == null) {
              return true;
          }
          return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values));
      }
  
      public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) {
          if (values == null) {
              return true;
          }
          final int asciiFirst = 0x20;
          final int asciiLast = 0x7E;  // included
          for (final String value : values) {
              if (TextUtils.isEmpty(value)) {
                  continue;
              }
              final int length = value.length();
              for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
                  final int c = value.codePointAt(i);
                  if (!(asciiFirst <= c && c <= asciiLast)) {
                      return false;
                  }
              }
          }
          return true;
      }
  
      private static final Set<Character> sUnAcceptableAsciiInV21WordSet =
          new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' '));
  
      /**
       * <p>
       * This is useful since vCard 3.0 often requires the ("X-") properties and groups
       * should contain only alphabets, digits, and hyphen.
       * </p>
       * <p>
       * Note: It is already known some devices (wrongly) outputs properties with characters
       *       which should not be in the field. One example is "X-GOOGLE TALK". We accept
       *       such kind of input but must never output it unless the target is very specific
       *       to the device which is able to parse the malformed input.
       * </p>
       */
      public static boolean containsOnlyAlphaDigitHyphen(final String...values) {
          if (values == null) {
              return true;
          }
          return containsOnlyAlphaDigitHyphen(Arrays.asList(values));
      }
  
      public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) {
          if (values == null) {
              return true;
          }
          final int upperAlphabetFirst = 0x41;  // A
          final int upperAlphabetAfterLast = 0x5b;  // [
          final int lowerAlphabetFirst = 0x61;  // a
          final int lowerAlphabetAfterLast = 0x7b;  // {
          final int digitFirst = 0x30;  // 0
          final int digitAfterLast = 0x3A;  // :
          final int hyphen = '-';
          for (final String str : values) {
              if (TextUtils.isEmpty(str)) {
                  continue;
              }
              final int length = str.length();
              for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
                  int codepoint = str.codePointAt(i);
                  if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) ||
                      (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) ||
                      (digitFirst <= codepoint && codepoint < digitAfterLast) ||
                      (codepoint == hyphen))) {
                      return false;
                  }
              }
          }
          return true;
      }
  
      public static boolean containsOnlyWhiteSpaces(final String...values) {
          if (values == null) {
              return true;
          }
          return containsOnlyWhiteSpaces(Arrays.asList(values));
      }
  
      public static boolean containsOnlyWhiteSpaces(final Collection<String> values) {
          if (values == null) {
              return true;
          }
          for (final String str : values) {
              if (TextUtils.isEmpty(str)) {
                  continue;
              }
              final int length = str.length();
              for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
                  if (!Character.isWhitespace(str.codePointAt(i))) {
                      return false;
                  }
              }
          }
          return true;
      }
  
      /**
       * <p>
       * Returns true when the given String is categorized as "word" specified in vCard spec 2.1.
       * </p>
       * <p>
       * vCard 2.1 specifies:<br />
       * word = &lt;any printable 7bit us-ascii except []=:., &gt;
       * </p>
       */
      public static boolean isV21Word(final String value) {
          if (TextUtils.isEmpty(value)) {
              return true;
          }
          final int asciiFirst = 0x20;
          final int asciiLast = 0x7E;  // included
          final int length = value.length();
          for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
              final int c = value.codePointAt(i);
              if (!(asciiFirst <= c && c <= asciiLast) ||
                      sUnAcceptableAsciiInV21WordSet.contains((char)c)) {
                  return false;
              }
          }
          return true;
      }
  
      private static final int[] sEscapeIndicatorsV30 = new int[]{
          ':', ';', ',', ' '
      };
  
      private static final int[] sEscapeIndicatorsV40 = new int[]{
          ';', ':'
      };
  
      /**
       * <P>
       * Returns String available as parameter value in vCard 3.0.
       * </P>
       * <P>
       * RFC 2426 requires vCard composer to quote parameter values when it contains
       * semi-colon, for example (See RFC 2426 for more information).
       * This method checks whether the given String can be used without quotes.
       * </P>
       * <P>
       * Note: We remove DQUOTE inside the given value silently for now.
       * </P>
       */
      public static String toStringAsV30ParamValue(String value) {
          return toStringAsParamValue(value, sEscapeIndicatorsV30);
      }
  
      public static String toStringAsV40ParamValue(String value) {
          return toStringAsParamValue(value, sEscapeIndicatorsV40);
      }
  
      private static String toStringAsParamValue(String value, final int[] escapeIndicators) {
          if (TextUtils.isEmpty(value)) {
              value = "";
          }
          final int asciiFirst = 0x20;
          final int asciiLast = 0x7E;  // included
          final StringBuilder builder = new StringBuilder();
          final int length = value.length();
          boolean needQuote = false;
          for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
              final int codePoint = value.codePointAt(i);
              if (codePoint < asciiFirst || codePoint == '"') {
                  // CTL characters and DQUOTE are never accepted. Remove them.
                  continue;
              }
              builder.appendCodePoint(codePoint);
              for (int indicator : escapeIndicators) {
                  if (codePoint == indicator) {
                      needQuote = true;
                      break;
                  }
              }
          }
  
          final String result = builder.toString();
          return ((result.isEmpty() || VCardUtils.containsOnlyWhiteSpaces(result))
                  ? ""
                  : (needQuote ? ('"' + result + '"')
                  : result));
      }
  
      public static String toHalfWidthString(final String orgString) {
          if (TextUtils.isEmpty(orgString)) {
              return null;
          }
          final StringBuilder builder = new StringBuilder();
          final int length = orgString.length();
          for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) {
              // All Japanese character is able to be expressed by char.
              // Do not need to use String#codepPointAt().
              final char ch = orgString.charAt(i);
              final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch);
              if (halfWidthText != null) {
                  builder.append(halfWidthText);
              } else {
                  builder.append(ch);
              }
          }
          return builder.toString();
      }
  
      /**
       * Guesses the format of input image. Currently just the first few bytes are used.
       * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when
       * the guess failed.
       * @param input Image as byte array.
       * @return The image type or null when the type cannot be determined.
       */
      public static String guessImageType(final byte[] input) {
          if (input == null) {
              return null;
          }
          if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') {
              return "GIF";
          } else if (input.length >= 4 && input[0] == (byte) 0x89
                  && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') {
              // Note: vCard 2.1 officially does not support PNG, but we may have it and
              //       using X- word like "X-PNG" may not let importers know it is PNG.
              //       So we use the String "PNG" as is...
              return "PNG";
          } else if (input.length >= 2 && input[0] == (byte) 0xff
                  && input[1] == (byte) 0xd8) {
              return "JPEG";
          } else {
              return null;
          }
      }
  
      /**
       * @return True when all the given values are null or empty Strings.
       */
      public static boolean areAllEmpty(final String...values) {
          if (values == null) {
              return true;
          }
  
          for (final String value : values) {
              if (!TextUtils.isEmpty(value)) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Checks to see if a string looks like it could be an android generated quoted printable.
       *
       * Identification of quoted printable is not 100% reliable since it's just ascii.  But given
       * the high number and exact location of generated = signs, there is a high likely-hood that
       * it would be.
       *
       * @return True if it appears like android quoted printable.  False otherwise.
       */
      public static boolean appearsLikeAndroidVCardQuotedPrintable(String value) {
  
          // Quoted printable is always in multiple of 3s. With optional 1 '=' at end.
          final int remainder = (value.length() % 3);
          if (value.length() < 2 || (remainder != 1 && remainder != 0)) {
              return false;
          }
          for (int i = 0; i < value.length(); i += 3) {
              if (value.charAt(i) != '=') {
                  return false;
              }
          }
          return true;
      }
  
      //// The methods bellow may be used by unit test.
  
      /**
       * Unquotes given Quoted-Printable value. value must not be null.
       */
      public static String parseQuotedPrintable(
              final String value, boolean strictLineBreaking,
              String sourceCharset, String targetCharset) {
          // "= " -> " ", "=\t" -> "\t".
          // Previous code had done this replacement. Keep on the safe side.
          final String quotedPrintable;
          {
              final StringBuilder builder = new StringBuilder();
              final int length = value.length();
              for (int i = 0; i < length; i++) {
                  char ch = value.charAt(i);
                  if (ch == '=' && i < length - 1) {
                      char nextCh = value.charAt(i + 1);
                      if (nextCh == ' ' || nextCh == '\t') {
                          builder.append(nextCh);
                          i++;
                          continue;
                      }
                  }
                  builder.append(ch);
              }
              quotedPrintable = builder.toString();
          }
  
          String[] lines;
          if (strictLineBreaking) {
              lines = quotedPrintable.split("\r\n");
          } else {
              StringBuilder builder = new StringBuilder();
              final int length = quotedPrintable.length();
              ArrayList<String> list = new ArrayList<String>();
              for (int i = 0; i < length; i++) {
                  char ch = quotedPrintable.charAt(i);
                  if (ch == '\n') {
                      list.add(builder.toString());
                      builder = new StringBuilder();
                  } else if (ch == '\r') {
                      list.add(builder.toString());
                      builder = new StringBuilder();
                      if (i < length - 1) {
                          char nextCh = quotedPrintable.charAt(i + 1);
                          if (nextCh == '\n') {
                              i++;
                          }
                      }
                  } else {
                      builder.append(ch);
                  }
              }
              final String lastLine = builder.toString();
              if (lastLine.length() > 0) {
                  list.add(lastLine);
              }
              lines = list.toArray(new String[0]);
          }
  
          final StringBuilder builder = new StringBuilder();
          for (String line : lines) {
              if (line.endsWith("=")) {
                  line = line.substring(0, line.length() - 1);
              }
              builder.append(line);
          }
  
          final String rawString = builder.toString();
          if (TextUtils.isEmpty(rawString)) {
              Log.w(LOG_TAG, "Given raw string is empty.");
          }
  
          byte[] rawBytes = null;
          try {
              rawBytes = rawString.getBytes(sourceCharset);
          } catch (UnsupportedEncodingException e) {
              Log.w(LOG_TAG, "Failed to decode: " + sourceCharset);
              rawBytes = rawString.getBytes();
          }
  
          byte[] decodedBytes = null;
          try {
              decodedBytes = QuotedPrintableCodecPort.decodeQuotedPrintable(rawBytes);
          } catch (DecoderException e) {
              Log.e(LOG_TAG, "DecoderException is thrown.");
              decodedBytes = rawBytes;
          }
  
          try {
              return new String(decodedBytes, targetCharset);
          } catch (UnsupportedEncodingException e) {
              Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
              return new String(decodedBytes);
          }
      }
  
      public static final VCardParser getAppropriateParser(int vcardType)
              throws VCardException {
          if (VCardConfig.isVersion21(vcardType)) {
              return new VCardParser_V21();
          } else if (VCardConfig.isVersion30(vcardType)) {
              return new VCardParser_V30();
          } else if (VCardConfig.isVersion40(vcardType)) {
              return new VCardParser_V40();
          } else {
              throw new VCardException("Version is not specified");
          }
      }
  
      public static final String convertStringCharset(
              String originalString, String sourceCharset, String targetCharset) {
          if (sourceCharset.equalsIgnoreCase(targetCharset)) {
              return originalString;
          }
          final Charset charset = Charset.forName(sourceCharset);
          final ByteBuffer byteBuffer = charset.encode(originalString);
          // byteBuffer.array() "may" return byte array which is larger than
          // byteBuffer.remaining(). Here, we keep on the safe side.
          final byte[] bytes = new byte[byteBuffer.remaining()];
          byteBuffer.get(bytes);
          try {
              return new String(bytes, targetCharset);
          } catch (UnsupportedEncodingException e) {
              Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
              return null;
          }
      }
  
      // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean
  
      private VCardUtils() {
      }
  }
  