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,
    105         EMAIL,
    106         SMS_GSM,
    107         SMS_CDMA,
    108         MMS,
    109         IM;
    110         private static TYPE[] allValues = values();
    111         public static TYPE fromOrdinal(int n) {
    112             if(n < allValues.length)
    113                return allValues[n];
    114             return NONE;
    115         }
    116     }
    117 
    118     static public String getDateTimeString(long timestamp) {
    119         SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
    120         Date date = new Date(timestamp);
    121         return format.format(date); // Format to YYYYMMDDTHHMMSS local time
    122     }
    123 
    124 
    125     public static void printCursor(Cursor c) {
    126         if (D) {
    127             StringBuilder sb = new StringBuilder();
    128             sb.append("\nprintCursor:\n");
    129             for(int i = 0; i < c.getColumnCount(); i++) {
    130                 if(c.getColumnName(i).equals(BluetoothMapContract.MessageColumns.DATE) ||
    131                    c.getColumnName(i).equals(
    132                            BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY) ||
    133                    c.getColumnName(i).equals(BluetoothMapContract.ChatStatusColumns.LAST_ACTIVE) ||
    134                    c.getColumnName(i).equals(BluetoothMapContract.PresenceColumns.LAST_ONLINE) ){
    135                     sb.append("  ").append(c.getColumnName(i)).append(" : ").append(
    136                             getDateTimeString(c.getLong(i))).append("\n");
    137                 } else {
    138                     sb.append("  ").append(c.getColumnName(i)).append(" : ").append(
    139                             c.getString(i)).append("\n");
    140                 }
    141             }
    142             Log.d(TAG, sb.toString());
    143         }
    144     }
    145 
    146     public static String getLongAsString(long v) {
    147         char[] result = new char[16];
    148         int v1 = (int) (v & 0xffffffff);
    149         int v2 = (int) ((v>>32) & 0xffffffff);
    150         int c;
    151         for (int i = 0; i < 8; i++) {
    152             c = v2 & 0x0f;
    153             c += (c < 10) ? '0' : ('A'-10);
    154             result[7 - i] = (char) c;
    155             v2 >>= 4;
    156             c = v1 & 0x0f;
    157             c += (c < 10) ? '0' : ('A'-10);
    158             result[15 - i] = (char)c;
    159             v1 >>= 4;
    160         }
    161         return new String(result);
    162     }
    163 
    164     /**
    165      * Converts a hex-string to a long - please mind that Java has no unsigned data types, hence
    166      * any value passed to this function, which has the upper bit set, will return a negative value.
    167      * The bitwise content of the variable will however be the same.
    168      * Will ignore any white-space characters as well as '-' seperators
    169      * @param valueStr a hexstring - NOTE: shall not contain any "0x" prefix.
    170      * @return
    171      * @throws UnsupportedEncodingException if "US-ASCII" charset is not supported,
    172      * NullPointerException if a null pointer is passed to the function,
    173      * NumberFormatException if the string contains invalid characters.
    174      *
    175      */
    176     public static long getLongFromString(String valueStr) throws UnsupportedEncodingException {
    177         if(valueStr == null) throw new NullPointerException();
    178         if(V) Log.i(TAG, "getLongFromString(): converting: " + valueStr);
    179         byte[] nibbles;
    180         nibbles = valueStr.getBytes("US-ASCII");
    181         if(V) Log.i(TAG, "  byte values: " + Arrays.toString(nibbles));
    182         byte c;
    183         int count = 0;
    184         int length = nibbles.length;
    185         long value = 0;
    186         for(int i = 0; i != length; i++) {
    187             c = nibbles[i];
    188             if(c >= '0' && c <= '9') {
    189                 c -= '0';
    190             } else if(c >= 'A' && c <= 'F') {
    191                 c -= ('A'-10);
    192             } else if(c >= 'a' && c <= 'f') {
    193                 c -= ('a'-10);
    194             } else if(c <= ' ' || c == '-') {
    195                 if(V)Log.v(TAG, "Skipping c = '" + new String(new byte[]{ (byte)c }, "US-ASCII")
    196                         + "'");
    197                 continue; // Skip any whitespace and '-' (which is used for UUIDs)
    198             } else {
    199                 throw new NumberFormatException("Invalid character:" + c);
    200             }
    201             value = value << 4; // The last nibble shall not be shifted
    202             value += c;
    203             count++;
    204             if(count > 16) throw new NullPointerException("String to large - count: " + count);
    205         }
    206         if(V) Log.i(TAG, "  length: " + count);
    207         return value;
    208     }
    209     private static final int LONG_LONG_LENGTH = 32;
    210     public static String getLongLongAsString(long vLow, long vHigh) {
    211         char[] result = new char[LONG_LONG_LENGTH];
    212         int v1 = (int) (vLow & 0xffffffff);
    213         int v2 = (int) ((vLow>>32) & 0xffffffff);
    214         int v3 = (int) (vHigh & 0xffffffff);
    215         int v4 = (int) ((vHigh>>32) & 0xffffffff);
    216         int c,d,i;
    217         // Handle the lower bytes
    218         for (i = 0; i < 8; i++) {
    219             c = v2 & 0x0f;
    220             c += (c < 10) ? '0' : ('A'-10);
    221             d = v4 & 0x0f;
    222             d += (d < 10) ? '0' : ('A'-10);
    223             result[23 - i] = (char) c;
    224             result[7 - i] = (char) d;
    225             v2 >>= 4;
    226             v4 >>= 4;
    227             c = v1 & 0x0f;
    228             c += (c < 10) ? '0' : ('A'-10);
    229             d = v3 & 0x0f;
    230             d += (d < 10) ? '0' : ('A'-10);
    231             result[31 - i] = (char)c;
    232             result[15 - i] = (char)d;
    233             v1 >>= 4;
    234             v3 >>= 4;
    235         }
    236         // Remove any leading 0's
    237         for(i = 0; i < LONG_LONG_LENGTH; i++) {
    238             if(result[i] != '0') {
    239                 break;
    240             }
    241         }
    242         return new String(result, i, LONG_LONG_LENGTH-i);
    243     }
    244 
    245 
    246     /**
    247      * Convert a Content Provider handle and a Messagetype into a unique handle
    248      * @param cpHandle content provider handle
    249      * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
    250      * @return String Formatted Map Handle
    251      */
    252     public static String getMapHandle(long cpHandle, TYPE messageType){
    253         String mapHandle = "-1";
    254         /* Avoid NPE for possible "null" value of messageType */
    255         if(messageType != null) {
    256             switch(messageType)
    257             {
    258                 case MMS:
    259                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_MMS_MASK);
    260                     break;
    261                 case SMS_GSM:
    262                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
    263                     break;
    264                 case SMS_CDMA:
    265                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
    266                     break;
    267                 case EMAIL:
    268                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_EMAIL_MASK);
    269                     break;
    270                 case IM:
    271                     mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_IM_MASK);
    272                     break;
    273                 case NONE:
    274                     break;
    275                 default:
    276                     throw new IllegalArgumentException("Message type not supported");
    277             }
    278         } else {
    279             if(D)Log.e(TAG," Invalid messageType input");
    280         }
    281         return mapHandle;
    282 
    283     }
    284 
    285     /**
    286      * Convert a Content Provider handle and a Messagetype into a unique handle
    287      * @param cpHandle content provider handle
    288      * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
    289      * @return String Formatted Map Handle
    290      */
    291     public static String getMapConvoHandle(long cpHandle, TYPE messageType){
    292         String mapHandle = "-1";
    293         switch(messageType)
    294         {
    295             case MMS:
    296             case SMS_GSM:
    297             case SMS_CDMA:
    298                 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_SMS_MMS);
    299                 break;
    300             case EMAIL:
    301             case IM:
    302                 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_EMAIL_IM);
    303                 break;
    304             default:
    305                 throw new IllegalArgumentException("Message type not supported");
    306         }
    307         return mapHandle;
    308 
    309     }
    310 
    311     /**
    312      * Convert a handle string the the raw long representation, including the type bit.
    313      * @param mapHandle the handle string
    314      * @return the handle value
    315      */
    316     static public long getMsgHandleAsLong(String mapHandle){
    317         return Long.parseLong(mapHandle, 16);
    318     }
    319     /**
    320      * Convert a Map Handle into a content provider Handle
    321      * @param mapHandle handle to convert from
    322      * @return content provider handle without message type mask
    323      */
    324     static public long getCpHandle(String mapHandle)
    325     {
    326         long cpHandle = getMsgHandleAsLong(mapHandle);
    327         if(D)Log.d(TAG,"-> MAP handle:"+mapHandle);
    328         /* remove masks as the call should already know what type of message this handle is for */
    329         cpHandle &= ~HANDLE_TYPE_MASK;
    330         if(D)Log.d(TAG,"->CP handle:"+cpHandle);
    331 
    332         return cpHandle;
    333     }
    334 
    335     /**
    336      * Extract the message type from the handle.
    337      * @param mapHandle
    338      * @return
    339      */
    340     static public TYPE getMsgTypeFromHandle(String mapHandle) {
    341         long cpHandle = getMsgHandleAsLong(mapHandle);
    342 
    343         if((cpHandle & HANDLE_TYPE_MMS_MASK) != 0)
    344             return TYPE.MMS;
    345         if((cpHandle & HANDLE_TYPE_EMAIL_MASK) != 0)
    346             return TYPE.EMAIL;
    347         if((cpHandle & HANDLE_TYPE_SMS_GSM_MASK) != 0)
    348             return TYPE.SMS_GSM;
    349         if((cpHandle & HANDLE_TYPE_SMS_CDMA_MASK) != 0)
    350             return TYPE.SMS_CDMA;
    351         if((cpHandle & HANDLE_TYPE_IM_MASK) != 0)
    352             return TYPE.IM;
    353 
    354         throw new IllegalArgumentException("Message type not found in handle string.");
    355     }
    356 
    357     /**
    358      * TODO: Is this still needed after changing to another XML encoder? It should escape illegal
    359      *       characters.
    360      * Strip away any illegal XML characters, that would otherwise cause the
    361      * xml serializer to throw an exception.
    362      * Examples of such characters are the emojis used on Android.
    363      * @param text The string to validate
    364      * @return the same string if valid, otherwise a new String stripped for
    365      * any illegal characters. If a null pointer is passed an empty string will be returned.
    366      */
    367     static public String stripInvalidChars(String text) {
    368         if(text == null) {
    369             return "";
    370         }
    371         char out[] = new char[text.length()];
    372         int i, o, l;
    373         for(i=0, o=0, l=text.length(); i<l; i++){
    374             char c = text.charAt(i);
    375             if((c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd)) {
    376                 out[o++] = c;
    377             } // Else we skip the character
    378         }
    379 
    380         if(i==o) {
    381             return text;
    382         } else { // We removed some characters, create the new string
    383             return new String(out,0,o);
    384         }
    385     }
    386 
    387     /**
    388      * Truncate UTF-8 string encoded byte array to desired length
    389      * @param utf8String String to convert to bytes array h
    390      * @param length Max length of byte array returned including null termination
    391      * @return byte array containing valid utf8 characters with max length
    392      * @throws UnsupportedEncodingException
    393      */
    394     static public byte[] truncateUtf8StringToBytearray(String utf8String, int maxLength)
    395             throws UnsupportedEncodingException {
    396 
    397         byte[] utf8Bytes = null;
    398         try {
    399             utf8Bytes = utf8String.getBytes("UTF-8");
    400         } catch (UnsupportedEncodingException e) {
    401             Log.e(TAG,"truncateUtf8StringToBytearray: getBytes exception ", e);
    402             throw e;
    403         }
    404 
    405         if (utf8Bytes.length > (maxLength - 1)) {
    406             /* if 'continuation' byte is in place 200,
    407              * then strip previous bytes until utf-8 start byte is found */
    408             if ( (utf8Bytes[maxLength - 1] & 0xC0) == 0x80 ) {
    409                 for (int i = maxLength - 2; i >= 0; i--) {
    410                     if ((utf8Bytes[i] & 0xC0) == 0xC0) {
    411                         /* first byte in utf-8 character found,
    412                          * now copy i - 1 bytes to outBytes and add null termination */
    413                         utf8Bytes = Arrays.copyOf(utf8Bytes, i+1);
    414                         utf8Bytes[i] = 0;
    415                         break;
    416                     }
    417                 }
    418             } else {
    419                 /* copy bytes to outBytes and null terminate */
    420                 utf8Bytes = Arrays.copyOf(utf8Bytes, maxLength);
    421                 utf8Bytes[maxLength - 1] = 0;
    422             }
    423         }
    424         return utf8Bytes;
    425     }
    426     private static Pattern p = Pattern.compile("=\\?(.+?)\\?(.)\\?(.+?(?=\\?=))\\?=");
    427 
    428     /**
    429      * Method for converting quoted printable og base64 encoded string from headers.
    430      * @param in the string with encoding
    431      * @return decoded string if success - else the same string as was as input.
    432      */
    433     static public String stripEncoding(String in){
    434         String str = null;
    435         if(in.contains("=?") && in.contains("?=")){
    436             String encoding;
    437             String charset;
    438             String encodedText;
    439             String match;
    440             Matcher m = p.matcher(in);
    441             while(m.find()){
    442                 match = m.group(0);
    443                 charset = m.group(1);
    444                 encoding = m.group(2);
    445                 encodedText = m.group(3);
    446                 Log.v(TAG, "Matching:" + match +"\nCharset: "+charset +"\nEncoding : " +encoding
    447                         + "\nText: " + encodedText);
    448                 if(encoding.equalsIgnoreCase("Q")){
    449                     //quoted printable
    450                     Log.d(TAG,"StripEncoding: Quoted Printable string : " + encodedText);
    451                     str = new String(quotedPrintableToUtf8(encodedText,charset));
    452                     in = in.replace(match, str);
    453                 }else if(encoding.equalsIgnoreCase("B")){
    454                     // base64
    455                     try{
    456 
    457                         Log.d(TAG,"StripEncoding: base64 string : " + encodedText);
    458                         str = new String(Base64.decode(encodedText.getBytes(charset),
    459                                 Base64.DEFAULT), charset);
    460                         Log.d(TAG,"StripEncoding: decoded string : " + str);
    461                         in = in.replace(match, str);
    462                     }catch(UnsupportedEncodingException e){
    463                         Log.e(TAG, "stripEncoding: Unsupported charset: " + charset);
    464                     }catch (IllegalArgumentException e){
    465                         Log.e(TAG,"stripEncoding: string not encoded as base64: " +encodedText);
    466                     }
    467                 }else{
    468                     Log.e(TAG, "stripEncoding: Hit unknown encoding: "+encoding);
    469                 }
    470             }
    471         }
    472         return in;
    473     }
    474 
    475 
    476     /**
    477      * Convert a quoted-printable encoded string to a UTF-8 string:
    478      *  - Remove any soft line breaks: "=<CRLF>"
    479      *  - Convert all "=xx" to the corresponding byte
    480      * @param text quoted-printable encoded UTF-8 text
    481      * @return decoded UTF-8 string
    482      */
    483     public static byte[] quotedPrintableToUtf8(String text, String charset) {
    484         byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
    485         byte[] input = null;
    486         try {
    487             input = text.getBytes("US-ASCII");
    488         } catch (UnsupportedEncodingException e) {
    489             /* This cannot happen as "US-ASCII" is supported for all Java implementations */ }
    490 
    491         if(input == null){
    492             return "".getBytes();
    493         }
    494 
    495         int in, out, stopCnt = input.length-2; // Leave room for peaking the next two bytes
    496 
    497         /* Algorithm:
    498          *  - Search for token, copying all non token chars
    499          * */
    500         for(in=0, out=0; in < stopCnt; in++){
    501             byte b0 = input[in];
    502             if(b0 == '=') {
    503                 byte b1 = input[++in];
    504                 byte b2 = input[++in];
    505                 if(b1 == '\r' && b2 == '\n') {
    506                     continue; // soft line break, remove all tree;
    507                 }
    508                 if(((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F')
    509                         || (b1 >= 'a' && b1 <= 'f'))
    510                         && ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F')
    511                         || (b2 >= 'a' && b2 <= 'f'))) {
    512                     if(V)Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
    513                     if(b1 <= '9')       b1 = (byte) (b1 - '0');
    514                     else if (b1 <= 'F') b1 = (byte) (b1 - 'A' + 10);
    515                     else if (b1 <= 'f') b1 = (byte) (b1 - 'a' + 10);
    516 
    517                     if(b2 <= '9')       b2 = (byte) (b2 - '0');
    518                     else if (b2 <= 'F') b2 = (byte) (b2 - 'A' + 10);
    519                     else if (b2 <= 'f') b2 = (byte) (b2 - 'a' + 10);
    520 
    521                     if(V)Log.v(TAG, "Resulting nibble values: " +
    522                             String.format("b1=%x b2=%x", b1, b2));
    523 
    524                     output[out++] = (byte)(b1<<4 | b2); // valid hex char, append
    525                     if(V)Log.v(TAG, "Resulting value: "  + String.format("0x%2x", output[out-1]));
    526                     continue;
    527                 }
    528                 Log.w(TAG, "Received wrongly quoted printable encoded text. " +
    529                         "Continuing at best effort...");
    530                 /* If we get a '=' without either a hex value or CRLF following, just add it and
    531                  * rewind the in counter. */
    532                 output[out++] = b0;
    533                 in -= 2;
    534                 continue;
    535             } else {
    536                 output[out++] = b0;
    537                 continue;
    538             }
    539         }
    540 
    541         // Just add any remaining characters. If they contain any encoding, it is invalid,
    542         // and best effort would be just to display the characters.
    543         while (in < input.length) {
    544             output[out++] = input[in++];
    545         }
    546 
    547         String result = null;
    548         // Figure out if we support the charset, else fall back to UTF-8, as this is what
    549         // the MAP specification suggest to use, and is compatible with US-ASCII.
    550         if(charset == null){
    551             charset = "UTF-8";
    552         } else {
    553             charset = charset.toUpperCase();
    554             try {
    555                 if(Charset.isSupported(charset) == false) {
    556                     charset = "UTF-8";
    557                 }
    558             } catch (IllegalCharsetNameException e) {
    559                 Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
    560                 charset = "UTF-8";
    561             }
    562         }
    563         try{
    564             result = new String(output, 0, out, charset);
    565         } catch (UnsupportedEncodingException e) {
    566             /* This cannot happen unless Charset.isSupported() is out of sync with String */
    567             try{
    568                 result = new String(output, 0, out, "UTF-8");
    569             } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
    570         }
    571         return result.getBytes(); /* return the result as "UTF-8" bytes */
    572     }
    573 
    574     /**
    575      * Encodes an array of bytes into an array of quoted-printable 7-bit characters.
    576      * Unsafe characters are escaped.
    577      * Simplified version of encoder from QuetedPrintableCodec.java (Apache external)
    578      *
    579      * @param bytes
    580      *                  array of bytes to be encoded
    581      * @return UTF-8 string containing quoted-printable characters
    582      */
    583 
    584     private static byte ESCAPE_CHAR = '=';
    585     private static byte TAB = 9;
    586     private static byte SPACE = 32;
    587 
    588     public static final String encodeQuotedPrintable(byte[] bytes) {
    589         if (bytes == null) {
    590             return null;
    591         }
    592 
    593         BitSet printable = new BitSet(256);
    594         // alpha characters
    595         for (int i = 33; i <= 60; i++) {
    596             printable.set(i);
    597         }
    598         for (int i = 62; i <= 126; i++) {
    599             printable.set(i);
    600         }
    601         printable.set(TAB);
    602         printable.set(SPACE);
    603         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    604         for (int i = 0; i < bytes.length; i++) {
    605             int b = bytes[i];
    606             if (b < 0) {
    607                 b = 256 + b;
    608             }
    609             if (printable.get(b)) {
    610                 buffer.write(b);
    611             } else {
    612                 buffer.write(ESCAPE_CHAR);
    613                 char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
    614                 char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
    615                 buffer.write(hex1);
    616                 buffer.write(hex2);
    617             }
    618         }
    619         try {
    620             return buffer.toString("UTF-8");
    621         } catch (UnsupportedEncodingException e) {
    622             //cannot happen
    623             return "";
    624         }
    625     }
    626 
    627 }
    628 
    629