Home | History | Annotate | Download | only in map
      1 /*
      2 * Copyright (C) 2013 Samsung System LSI
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *      http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 */
     15 package com.android.bluetooth.map;
     16 
     17 import android.database.Cursor;
     18 import android.util.Base64;
     19 import android.util.Log;
     20 
     21 import com.android.bluetooth.mapapi.BluetoothMapContract;
     22 
     23 import java.io.ByteArrayOutputStream;
     24 import java.io.UnsupportedEncodingException;
     25 import java.nio.charset.Charset;
     26 import java.nio.charset.IllegalCharsetNameException;
     27 import java.text.SimpleDateFormat;
     28 import java.util.Arrays;
     29 import java.util.BitSet;
     30 import java.util.Date;
     31 import java.util.regex.Matcher;
     32 import java.util.regex.Pattern;
     33 
     34 
     35 /**
     36  * Various utility methods and generic defines that can be used throughout MAPS
     37  */
     38 public class BluetoothMapUtils {
     39 
     40     private static final String TAG = "BluetoothMapUtils";
     41     private static final boolean D = BluetoothMapService.DEBUG;
     42     private static final boolean V = BluetoothMapService.VERBOSE;
     43     /* We use the upper 4 bits for the type mask.
     44      * TODO: When more types are needed, consider just using a number
     45      *       in stead of a bit to indicate the message type. Then 4
     46      *       bit can be use for 16 different message types.
     47      */
     48     private static final long HANDLE_TYPE_MASK = (((long) 0xff) << 56);
     49     private static final long HANDLE_TYPE_MMS_MASK = (((long) 0x01) << 56);
     50     private static final long HANDLE_TYPE_EMAIL_MASK = (((long) 0x02) << 56);
     51     private static final long HANDLE_TYPE_SMS_GSM_MASK = (((long) 0x04) << 56);
     52     private static final long HANDLE_TYPE_SMS_CDMA_MASK = (((long) 0x08) << 56);
     53     private static final long HANDLE_TYPE_IM_MASK = (((long) 0x10) << 56);
     54 
     55     public static final long CONVO_ID_TYPE_SMS_MMS = 1;
     56     public static final long CONVO_ID_TYPE_EMAIL_IM = 2;
     57 
     58     // MAP supported feature bit - included from MAP Spec 1.2
     59     static final int MAP_FEATURE_DEFAULT_BITMASK = 0x0000001F;
     60 
     61     static final int MAP_FEATURE_NOTIFICATION_REGISTRATION_BIT = 1 << 0;
     62     static final int MAP_FEATURE_NOTIFICATION_BIT = 1 << 1;
     63     static final int MAP_FEATURE_BROWSING_BIT = 1 << 2;
     64     static final int MAP_FEATURE_UPLOADING_BIT = 1 << 3;
     65     static final int MAP_FEATURE_DELETE_BIT = 1 << 4;
     66     static final int MAP_FEATURE_INSTANCE_INFORMATION_BIT = 1 << 5;
     67     static final int MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT = 1 << 6;
     68     static final int MAP_FEATURE_EVENT_REPORT_V12_BIT = 1 << 7;
     69     static final int MAP_FEATURE_MESSAGE_FORMAT_V11_BIT = 1 << 8;
     70     static final int MAP_FEATURE_MESSAGE_LISTING_FORMAT_V11_BIT = 1 << 9;
     71     static final int MAP_FEATURE_PERSISTENT_MESSAGE_HANDLE_BIT = 1 << 10;
     72     static final int MAP_FEATURE_DATABASE_INDENTIFIER_BIT = 1 << 11;
     73     static final int MAP_FEATURE_FOLDER_VERSION_COUNTER_BIT = 1 << 12;
     74     static final int MAP_FEATURE_CONVERSATION_VERSION_COUNTER_BIT = 1 << 13;
     75     static final int MAP_FEATURE_PARTICIPANT_PRESENCE_CHANGE_BIT = 1 << 14;
     76     static final int MAP_FEATURE_PARTICIPANT_CHAT_STATE_CHANGE_BIT = 1 << 15;
     77 
     78     static final int MAP_FEATURE_PBAP_CONTACT_CROSS_REFERENCE_BIT = 1 << 16;
     79     static final int MAP_FEATURE_NOTIFICATION_FILTERING_BIT = 1 << 17;
     80     static final int MAP_FEATURE_DEFINED_TIMESTAMP_FORMAT_BIT = 1 << 18;
     81 
     82     static final String MAP_V10_STR = "1.0";
     83     static final String MAP_V11_STR = "1.1";
     84     static final String MAP_V12_STR = "1.2";
     85 
     86     // Event Report versions
     87     static final int MAP_EVENT_REPORT_V10 = 10; // MAP spec 1.1
     88     static final int MAP_EVENT_REPORT_V11 = 11; // MAP spec 1.2
     89     static final int MAP_EVENT_REPORT_V12 = 12; // MAP spec 1.3 'to be' incl. IM
     90 
     91     // Message Format versions
     92     static final int MAP_MESSAGE_FORMAT_V10 = 10; // MAP spec below 1.3
     93     static final int MAP_MESSAGE_FORMAT_V11 = 11; // MAP spec 1.3
     94 
     95     // Message Listing Format versions
     96     static final int MAP_MESSAGE_LISTING_FORMAT_V10 = 10; // MAP spec below 1.3
     97     static final int MAP_MESSAGE_LISTING_FORMAT_V11 = 11; // MAP spec 1.3
     98 
     99     /**
    100      * This enum is used to convert from the bMessage type property to a type safe
    101      * type. Hence do not change the names of the enum values.
    102      */
    103     public enum TYPE {
    104         NONE, EMAIL, SMS_GSM, SMS_CDMA, MMS, IM;
    105         private static TYPE[] sAllValues = values();
    106 
    107         public static TYPE fromOrdinal(int n) {
    108             if (n < sAllValues.length) {
    109                 return sAllValues[n];
    110             }
    111             return NONE;
    112         }
    113     }
    114 
    115     public static String getDateTimeString(long timestamp) {
    116         SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
    117         Date date = new Date(timestamp);
    118         return format.format(date); // Format to YYYYMMDDTHHMMSS local time
    119     }
    120 
    121 
    122     public static void printCursor(Cursor c) {
    123         if (D) {
    124             StringBuilder sb = new StringBuilder();
    125             sb.append("\nprintCursor:\n");
    126             for (int i = 0; i < c.getColumnCount(); i++) {
    127                 if (c.getColumnName(i).equals(BluetoothMapContract.MessageColumns.DATE)
    128                         || c.getColumnName(i)
    129                         .equals(BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY)
    130                         || c.getColumnName(i)
    131                         .equals(BluetoothMapContract.ChatStatusColumns.LAST_ACTIVE)
    132                         || c.getColumnName(i)
    133                         .equals(BluetoothMapContract.PresenceColumns.LAST_ONLINE)) {
    134                     sb.append("  ")
    135                             .append(c.getColumnName(i))
    136                             .append(" : ")
    137                             .append(getDateTimeString(c.getLong(i)))
    138                             .append("\n");
    139                 } else {
    140                     sb.append("  ")
    141                             .append(c.getColumnName(i))
    142                             .append(" : ")
    143                             .append(c.getString(i))
    144                             .append("\n");
    145                 }
    146             }
    147             Log.d(TAG, sb.toString());
    148         }
    149     }
    150 
    151     public static String getLongAsString(long v) {
    152         char[] result = new char[16];
    153         int v1 = (int) (v & 0xffffffff);
    154         int v2 = (int) ((v >> 32) & 0xffffffff);
    155         int c;
    156         for (int i = 0; i < 8; i++) {
    157             c = v2 & 0x0f;
    158             c += (c < 10) ? '0' : ('A' - 10);
    159             result[7 - i] = (char) c;
    160             v2 >>= 4;
    161             c = v1 & 0x0f;
    162             c += (c < 10) ? '0' : ('A' - 10);
    163             result[15 - i] = (char) c;
    164             v1 >>= 4;
    165         }
    166         return new String(result);
    167     }
    168 
    169     /**
    170      * Converts a hex-string to a long - please mind that Java has no unsigned data types, hence
    171      * any value passed to this function, which has the upper bit set, will return a negative value.
    172      * The bitwise content of the variable will however be the same.
    173      * Will ignore any white-space characters as well as '-' seperators
    174      * @param valueStr a hexstring - NOTE: shall not contain any "0x" prefix.
    175      * @return
    176      * @throws UnsupportedEncodingException if "US-ASCII" charset is not supported,
    177      * NullPointerException if a null pointer is passed to the function,
    178      * NumberFormatException if the string contains invalid characters.
    179      *
    180      */
    181     public static long getLongFromString(String valueStr) throws UnsupportedEncodingException {
    182         if (valueStr == null) {
    183             throw new NullPointerException();
    184         }
    185         if (V) {
    186             Log.i(TAG, "getLongFromString(): converting: " + valueStr);
    187         }
    188         byte[] nibbles;
    189         nibbles = valueStr.getBytes("US-ASCII");
    190         if (V) {
    191             Log.i(TAG, "  byte values: " + Arrays.toString(nibbles));
    192         }
    193         byte c;
    194         int count = 0;
    195         int length = nibbles.length;
    196         long value = 0;
    197         for (int i = 0; i != length; i++) {
    198             c = nibbles[i];
    199             if (c >= '0' && c <= '9') {
    200                 c -= '0';
    201             } else if (c >= 'A' && c <= 'F') {
    202                 c -= ('A' - 10);
    203             } else if (c >= 'a' && c <= 'f') {
    204                 c -= ('a' - 10);
    205             } else if (c <= ' ' || c == '-') {
    206                 if (V) {
    207                     Log.v(TAG,
    208                             "Skipping c = '" + new String(new byte[]{(byte) c}, "US-ASCII") + "'");
    209                 }
    210                 continue; // Skip any whitespace and '-' (which is used for UUIDs)
    211             } else {
    212                 throw new NumberFormatException("Invalid character:" + c);
    213             }
    214             value = value << 4; // The last nibble shall not be shifted
    215             value += c;
    216             count++;
    217             if (count > 16) {
    218                 throw new NullPointerException("String to large - count: " + count);
    219             }
    220         }
    221         if (V) {
    222             Log.i(TAG, "  length: " + count);
    223         }
    224         return value;
    225     }
    226 
    227     private static final int LONG_LONG_LENGTH = 32;
    228 
    229     public static String getLongLongAsString(long vLow, long vHigh) {
    230         char[] result = new char[LONG_LONG_LENGTH];
    231         int v1 = (int) (vLow & 0xffffffff);
    232         int v2 = (int) ((vLow >> 32) & 0xffffffff);
    233         int v3 = (int) (vHigh & 0xffffffff);
    234         int v4 = (int) ((vHigh >> 32) & 0xffffffff);
    235         int c, d, i;
    236         // Handle the lower bytes
    237         for (i = 0; i < 8; i++) {
    238             c = v2 & 0x0f;
    239             c += (c < 10) ? '0' : ('A' - 10);
    240             d = v4 & 0x0f;
    241             d += (d < 10) ? '0' : ('A' - 10);
    242             result[23 - i] = (char) c;
    243             result[7 - i] = (char) d;
    244             v2 >>= 4;
    245             v4 >>= 4;
    246             c = v1 & 0x0f;
    247             c += (c < 10) ? '0' : ('A' - 10);
    248             d = v3 & 0x0f;
    249             d += (d < 10) ? '0' : ('A' - 10);
    250             result[31 - i] = (char) c;
    251             result[15 - i] = (char) d;
    252             v1 >>= 4;
    253             v3 >>= 4;
    254         }
    255         // Remove any leading 0's
    256         for (i = 0; i < LONG_LONG_LENGTH; i++) {
    257             if (result[i] != '0') {
    258                 break;
    259             }
    260         }
    261         return new String(result, i, LONG_LONG_LENGTH - i);
    262     }
    263 
    264 
    265     /**
    266      * Convert a Content Provider handle and a Messagetype into a unique handle
    267      * @param cpHandle content provider handle
    268      * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
    269      * @return String Formatted Map Handle
    270      */
    271     public static String getMapHandle(long cpHandle, TYPE messageType) {
    272         String mapHandle = "-1";
    273         /* Avoid NPE for possible "null" value of messageType */
    274         if (messageType != null) {
    275             switch (messageType) {
    276                 case MMS:
    277                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_MMS_MASK);
    278                     break;
    279                 case SMS_GSM:
    280                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
    281                     break;
    282                 case SMS_CDMA:
    283                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
    284                     break;
    285                 case EMAIL:
    286                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_EMAIL_MASK);
    287                     break;
    288                 case IM:
    289                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_IM_MASK);
    290                     break;
    291                 case NONE:
    292                     break;
    293                 default:
    294                     throw new IllegalArgumentException("Message type not supported");
    295             }
    296         } else {
    297             if (D) {
    298                 Log.e(TAG, " Invalid messageType input");
    299             }
    300         }
    301         return mapHandle;
    302 
    303     }
    304 
    305     /**
    306      * Convert a Content Provider handle and a Messagetype into a unique handle
    307      * @param cpHandle content provider handle
    308      * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
    309      * @return String Formatted Map Handle
    310      */
    311     public static String getMapConvoHandle(long cpHandle, TYPE messageType) {
    312         String mapHandle = "-1";
    313         switch (messageType) {
    314             case MMS:
    315             case SMS_GSM:
    316             case SMS_CDMA:
    317                 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_SMS_MMS);
    318                 break;
    319             case EMAIL:
    320             case IM:
    321                 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_EMAIL_IM);
    322                 break;
    323             default:
    324                 throw new IllegalArgumentException("Message type not supported");
    325         }
    326         return mapHandle;
    327 
    328     }
    329 
    330     /**
    331      * Convert a handle string the the raw long representation, including the type bit.
    332      * @param mapHandle the handle string
    333      * @return the handle value
    334      */
    335     public static long getMsgHandleAsLong(String mapHandle) {
    336         return Long.parseLong(mapHandle, 16);
    337     }
    338 
    339     /**
    340      * Convert a Map Handle into a content provider Handle
    341      * @param mapHandle handle to convert from
    342      * @return content provider handle without message type mask
    343      */
    344     public static long getCpHandle(String mapHandle) {
    345         long cpHandle = getMsgHandleAsLong(mapHandle);
    346         if (D) {
    347             Log.d(TAG, "-> MAP handle:" + mapHandle);
    348         }
    349         /* remove masks as the call should already know what type of message this handle is for */
    350         cpHandle &= ~HANDLE_TYPE_MASK;
    351         if (D) {
    352             Log.d(TAG, "->CP handle:" + cpHandle);
    353         }
    354 
    355         return cpHandle;
    356     }
    357 
    358     /**
    359      * Extract the message type from the handle.
    360      * @param mapHandle
    361      * @return
    362      */
    363     public static TYPE getMsgTypeFromHandle(String mapHandle) {
    364         long cpHandle = getMsgHandleAsLong(mapHandle);
    365 
    366         if ((cpHandle & HANDLE_TYPE_MMS_MASK) != 0) {
    367             return TYPE.MMS;
    368         }
    369         if ((cpHandle & HANDLE_TYPE_EMAIL_MASK) != 0) {
    370             return TYPE.EMAIL;
    371         }
    372         if ((cpHandle & HANDLE_TYPE_SMS_GSM_MASK) != 0) {
    373             return TYPE.SMS_GSM;
    374         }
    375         if ((cpHandle & HANDLE_TYPE_SMS_CDMA_MASK) != 0) {
    376             return TYPE.SMS_CDMA;
    377         }
    378         if ((cpHandle & HANDLE_TYPE_IM_MASK) != 0) {
    379             return TYPE.IM;
    380         }
    381 
    382         throw new IllegalArgumentException("Message type not found in handle string.");
    383     }
    384 
    385     /**
    386      * TODO: Is this still needed after changing to another XML encoder? It should escape illegal
    387      *       characters.
    388      * Strip away any illegal XML characters, that would otherwise cause the
    389      * xml serializer to throw an exception.
    390      * Examples of such characters are the emojis used on Android.
    391      * @param text The string to validate
    392      * @return the same string if valid, otherwise a new String stripped for
    393      * any illegal characters. If a null pointer is passed an empty string will be returned.
    394      */
    395     public static String stripInvalidChars(String text) {
    396         if (text == null) {
    397             return "";
    398         }
    399         char[] out = new char[text.length()];
    400         int i, o, l;
    401         for (i = 0, o = 0, l = text.length(); i < l; i++) {
    402             char c = text.charAt(i);
    403             if ((c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd)) {
    404                 out[o++] = c;
    405             } // Else we skip the character
    406         }
    407 
    408         if (i == o) {
    409             return text;
    410         } else { // We removed some characters, create the new string
    411             return new String(out, 0, o);
    412         }
    413     }
    414 
    415     /**
    416      * Truncate UTF-8 string encoded byte array to desired length
    417      * @param utf8String String to convert to bytes array h
    418      * @param maxLength Max length of byte array returned including null termination
    419      * @return byte array containing valid utf8 characters with max length
    420      * @throws UnsupportedEncodingException
    421      */
    422     public static byte[] truncateUtf8StringToBytearray(String utf8String, int maxLength)
    423             throws UnsupportedEncodingException {
    424 
    425         byte[] utf8Bytes = new byte[utf8String.length() + 1];
    426         try {
    427             System.arraycopy(utf8String.getBytes("UTF-8"), 0, utf8Bytes, 0, utf8String.length());
    428         } catch (UnsupportedEncodingException e) {
    429             Log.e(TAG, "truncateUtf8StringToBytearray: getBytes exception ", e);
    430             throw e;
    431         }
    432 
    433         if (utf8Bytes.length > maxLength) {
    434             /* if 'continuation' byte is in place 200,
    435              * then strip previous bytes until utf-8 start byte is found */
    436             if ((utf8Bytes[maxLength - 1] & 0xC0) == 0x80) {
    437                 for (int i = maxLength - 2; i >= 0; i--) {
    438                     if ((utf8Bytes[i] & 0xC0) == 0xC0) {
    439                         /* first byte in utf-8 character found,
    440                          * now copy i - 1 bytes to outBytes and add null termination */
    441                         utf8Bytes = Arrays.copyOf(utf8Bytes, i + 1);
    442                         utf8Bytes[i] = 0;
    443                         break;
    444                     }
    445                 }
    446             } else {
    447                 /* copy bytes to outBytes and null terminate */
    448                 utf8Bytes = Arrays.copyOf(utf8Bytes, maxLength);
    449                 utf8Bytes[maxLength - 1] = 0;
    450             }
    451         }
    452         return utf8Bytes;
    453     }
    454 
    455     private static final Pattern PATTERN = Pattern.compile("=\\?(.+?)\\?(.)\\?(.+?(?=\\?=))\\?=");
    456 
    457     /**
    458      * Method for converting quoted printable og base64 encoded string from headers.
    459      * @param in the string with encoding
    460      * @return decoded string if success - else the same string as was as input.
    461      */
    462     public static String stripEncoding(String in) {
    463         String str = null;
    464         if (in.contains("=?") && in.contains("?=")) {
    465             String encoding;
    466             String charset;
    467             String encodedText;
    468             String match;
    469             Matcher m = PATTERN.matcher(in);
    470             while (m.find()) {
    471                 match = m.group(0);
    472                 charset = m.group(1);
    473                 encoding = m.group(2);
    474                 encodedText = m.group(3);
    475                 Log.v(TAG,
    476                         "Matching:" + match + "\nCharset: " + charset + "\nEncoding : " + encoding
    477                                 + "\nText: " + encodedText);
    478                 if (encoding.equalsIgnoreCase("Q")) {
    479                     //quoted printable
    480                     Log.d(TAG, "StripEncoding: Quoted Printable string : " + encodedText);
    481                     str = new String(quotedPrintableToUtf8(encodedText, charset));
    482                     in = in.replace(match, str);
    483                 } else if (encoding.equalsIgnoreCase("B")) {
    484                     // base64
    485                     try {
    486 
    487                         Log.d(TAG, "StripEncoding: base64 string : " + encodedText);
    488                         str = new String(
    489                                 Base64.decode(encodedText.getBytes(charset), Base64.DEFAULT),
    490                                 charset);
    491                         Log.d(TAG, "StripEncoding: decoded string : " + str);
    492                         in = in.replace(match, str);
    493                     } catch (UnsupportedEncodingException e) {
    494                         Log.e(TAG, "stripEncoding: Unsupported charset: " + charset);
    495                     } catch (IllegalArgumentException e) {
    496                         Log.e(TAG, "stripEncoding: string not encoded as base64: " + encodedText);
    497                     }
    498                 } else {
    499                     Log.e(TAG, "stripEncoding: Hit unknown encoding: " + encoding);
    500                 }
    501             }
    502         }
    503         return in;
    504     }
    505 
    506 
    507     /**
    508      * Convert a quoted-printable encoded string to a UTF-8 string:
    509      *  - Remove any soft line breaks: "=<CRLF>"
    510      *  - Convert all "=xx" to the corresponding byte
    511      * @param text quoted-printable encoded UTF-8 text
    512      * @return decoded UTF-8 string
    513      */
    514     public static byte[] quotedPrintableToUtf8(String text, String charset) {
    515         byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
    516         byte[] input = null;
    517         try {
    518             input = text.getBytes("US-ASCII");
    519         } catch (UnsupportedEncodingException e) {
    520             /* This cannot happen as "US-ASCII" is supported for all Java implementations */
    521         }
    522 
    523         if (input == null) {
    524             return "".getBytes();
    525         }
    526 
    527         int in, out, stopCnt = input.length - 2; // Leave room for peaking the next two bytes
    528 
    529         /* Algorithm:
    530          *  - Search for token, copying all non token chars
    531          * */
    532         for (in = 0, out = 0; in < stopCnt; in++) {
    533             byte b0 = input[in];
    534             if (b0 == '=') {
    535                 byte b1 = input[++in];
    536                 byte b2 = input[++in];
    537                 if (b1 == '\r' && b2 == '\n') {
    538                     continue; // soft line break, remove all tree;
    539                 }
    540                 if (((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F') || (b1 >= 'a'
    541                         && b1 <= 'f')) && ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F') || (
    542                         b2 >= 'a' && b2 <= 'f'))) {
    543                     if (V) {
    544                         Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
    545                     }
    546                     if (b1 <= '9') {
    547                         b1 = (byte) (b1 - '0');
    548                     } else if (b1 <= 'F') {
    549                         b1 = (byte) (b1 - 'A' + 10);
    550                     } else if (b1 <= 'f') {
    551                         b1 = (byte) (b1 - 'a' + 10);
    552                     }
    553 
    554                     if (b2 <= '9') {
    555                         b2 = (byte) (b2 - '0');
    556                     } else if (b2 <= 'F') {
    557                         b2 = (byte) (b2 - 'A' + 10);
    558                     } else if (b2 <= 'f') {
    559                         b2 = (byte) (b2 - 'a' + 10);
    560                     }
    561 
    562                     if (V) {
    563                         Log.v(TAG,
    564                                 "Resulting nibble values: " + String.format("b1=%x b2=%x", b1, b2));
    565                     }
    566 
    567                     output[out++] = (byte) (b1 << 4 | b2); // valid hex char, append
    568                     if (V) {
    569                         Log.v(TAG, "Resulting value: " + String.format("0x%2x", output[out - 1]));
    570                     }
    571                     continue;
    572                 }
    573                 Log.w(TAG, "Received wrongly quoted printable encoded text. "
    574                         + "Continuing at best effort...");
    575                 /* If we get a '=' without either a hex value or CRLF following, just add it and
    576                  * rewind the in counter. */
    577                 output[out++] = b0;
    578                 in -= 2;
    579                 continue;
    580             } else {
    581                 output[out++] = b0;
    582                 continue;
    583             }
    584         }
    585 
    586         // Just add any remaining characters. If they contain any encoding, it is invalid,
    587         // and best effort would be just to display the characters.
    588         while (in < input.length) {
    589             output[out++] = input[in++];
    590         }
    591 
    592         String result = null;
    593         // Figure out if we support the charset, else fall back to UTF-8, as this is what
    594         // the MAP specification suggest to use, and is compatible with US-ASCII.
    595         if (charset == null) {
    596             charset = "UTF-8";
    597         } else {
    598             charset = charset.toUpperCase();
    599             try {
    600                 if (!Charset.isSupported(charset)) {
    601                     charset = "UTF-8";
    602                 }
    603             } catch (IllegalCharsetNameException e) {
    604                 Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
    605                 charset = "UTF-8";
    606             }
    607         }
    608         try {
    609             result = new String(output, 0, out, charset);
    610         } catch (UnsupportedEncodingException e) {
    611             /* This cannot happen unless Charset.isSupported() is out of sync with String */
    612             try {
    613                 result = new String(output, 0, out, "UTF-8");
    614             } catch (UnsupportedEncodingException e2) {
    615                 Log.e(TAG, "quotedPrintableToUtf8: " + e);
    616             }
    617         }
    618         return result.getBytes(); /* return the result as "UTF-8" bytes */
    619     }
    620 
    621     /**
    622      * Encodes an array of bytes into an array of quoted-printable 7-bit characters.
    623      * Unsafe characters are escaped.
    624      * Simplified version of encoder from QuetedPrintableCodec.java (Apache external)
    625      *
    626      * @param bytes
    627      *                  array of bytes to be encoded
    628      * @return UTF-8 string containing quoted-printable characters
    629      */
    630 
    631     private static final byte ESCAPE_CHAR = '=';
    632     private static final byte TAB = 9;
    633     private static final byte SPACE = 32;
    634 
    635     public static final String encodeQuotedPrintable(byte[] bytes) {
    636         if (bytes == null) {
    637             return null;
    638         }
    639 
    640         BitSet printable = new BitSet(256);
    641         // alpha characters
    642         for (int i = 33; i <= 60; i++) {
    643             printable.set(i);
    644         }
    645         for (int i = 62; i <= 126; i++) {
    646             printable.set(i);
    647         }
    648         printable.set(TAB);
    649         printable.set(SPACE);
    650         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    651         for (int i = 0; i < bytes.length; i++) {
    652             int b = bytes[i];
    653             if (b < 0) {
    654                 b = 256 + b;
    655             }
    656             if (printable.get(b)) {
    657                 buffer.write(b);
    658             } else {
    659                 buffer.write(ESCAPE_CHAR);
    660                 char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
    661                 char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
    662                 buffer.write(hex1);
    663                 buffer.write(hex2);
    664             }
    665         }
    666         try {
    667             return buffer.toString("UTF-8");
    668         } catch (UnsupportedEncodingException e) {
    669             //cannot happen
    670             return "";
    671         }
    672     }
    673 
    674 }
    675 
    676