All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.vcard.VCardUtils Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 14-robolectric-10818077
Show newest version
/*
 * 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 sKnownPhoneTypesMap_ItoS;
    private static final Set sPhoneTypesUnknownToContactsSet;
    private static final Map sKnownPhoneTypeMap_StoI;
    private static final Map sKnownImPropNameMap_ItoS;
    private static final Set sMobilePhoneLabelSet;

    static {
        sKnownPhoneTypesMap_ItoS = new HashMap();
        sKnownPhoneTypeMap_StoI = new HashMap();

        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();
        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();
        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(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 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 [email protected]
                        // 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 constructListFromValue(final String value,
            final int vcardType) {
        final List list = new ArrayList();
        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 values) {
        if (values == null) {
            return true;
        }
        for (final String value : values) {
            if (TextUtils.isEmpty(value)) {
                continue;
            }
            if (!TextUtilsPort.isPrintableAsciiOnly(value)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 

* This is useful when checking the string should be encoded into quoted-printable * or not, which is required by vCard 2.1. *

*

* See the definition of "7bit" in vCard 2.1 spec for more information. *

*/ public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { if (values == null) { return true; } return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); } public static boolean containsOnlyNonCrLfPrintableAscii(final Collection 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 sUnAcceptableAsciiInV21WordSet = new HashSet(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); /** *

* This is useful since vCard 3.0 often requires the ("X-") properties and groups * should contain only alphabets, digits, and hyphen. *

*

* 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. *

*/ public static boolean containsOnlyAlphaDigitHyphen(final String...values) { if (values == null) { return true; } return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); } public static boolean containsOnlyAlphaDigitHyphen(final Collection 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 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; } /** *

* Returns true when the given String is categorized as "word" specified in vCard spec 2.1. *

*

* vCard 2.1 specifies:
* word = <any printable 7bit us-ascii except []=:., > *

*/ 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[]{ ';', ':' }; /** *

* Returns String available as parameter value in vCard 3.0. *

*

* 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. *

*

* Note: We remove DQUOTE inside the given value silently for now. *

*/ 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 list = new ArrayList(); 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() { } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy