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();
fromOrdinal(int n)111         public static TYPE fromOrdinal(int n) {
112             if(n < allValues.length)
113                return allValues[n];
114             return NONE;
115         }
116     }
117 
getDateTimeString(long timestamp)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 
printCursor(Cursor c)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 
getLongAsString(long v)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      */
getLongFromString(String valueStr)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;
getLongLongAsString(long vLow, long vHigh)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      */
getMapHandle(long cpHandle, TYPE messageType)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      */
getMapConvoHandle(long cpHandle, TYPE messageType)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      */
getMsgHandleAsLong(String mapHandle)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      */
getCpHandle(String mapHandle)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      */
getMsgTypeFromHandle(String mapHandle)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      */
stripInvalidChars(String text)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      */
truncateUtf8StringToBytearray(String utf8String, int maxLength)394     static public byte[] truncateUtf8StringToBytearray(String utf8String, int maxLength)
395             throws UnsupportedEncodingException {
396 
397         byte[] utf8Bytes = new byte[utf8String.length() + 1];
398         try {
399             System.arraycopy(utf8String.getBytes("UTF-8"), 0,
400                              utf8Bytes, 0, utf8String.length());
401         } catch (UnsupportedEncodingException e) {
402             Log.e(TAG,"truncateUtf8StringToBytearray: getBytes exception ", e);
403             throw e;
404         }
405 
406         if (utf8Bytes.length > maxLength) {
407             /* if 'continuation' byte is in place 200,
408              * then strip previous bytes until utf-8 start byte is found */
409             if ( (utf8Bytes[maxLength - 1] & 0xC0) == 0x80 ) {
410                 for (int i = maxLength - 2; i >= 0; i--) {
411                     if ((utf8Bytes[i] & 0xC0) == 0xC0) {
412                         /* first byte in utf-8 character found,
413                          * now copy i - 1 bytes to outBytes and add null termination */
414                         utf8Bytes = Arrays.copyOf(utf8Bytes, i+1);
415                         utf8Bytes[i] = 0;
416                         break;
417                     }
418                 }
419             } else {
420                 /* copy bytes to outBytes and null terminate */
421                 utf8Bytes = Arrays.copyOf(utf8Bytes, maxLength);
422                 utf8Bytes[maxLength - 1] = 0;
423             }
424         }
425         return utf8Bytes;
426     }
427     private static Pattern p = Pattern.compile("=\\?(.+?)\\?(.)\\?(.+?(?=\\?=))\\?=");
428 
429     /**
430      * Method for converting quoted printable og base64 encoded string from headers.
431      * @param in the string with encoding
432      * @return decoded string if success - else the same string as was as input.
433      */
stripEncoding(String in)434     static public String stripEncoding(String in){
435         String str = null;
436         if(in.contains("=?") && in.contains("?=")){
437             String encoding;
438             String charset;
439             String encodedText;
440             String match;
441             Matcher m = p.matcher(in);
442             while(m.find()){
443                 match = m.group(0);
444                 charset = m.group(1);
445                 encoding = m.group(2);
446                 encodedText = m.group(3);
447                 Log.v(TAG, "Matching:" + match +"\nCharset: "+charset +"\nEncoding : " +encoding
448                         + "\nText: " + encodedText);
449                 if(encoding.equalsIgnoreCase("Q")){
450                     //quoted printable
451                     Log.d(TAG,"StripEncoding: Quoted Printable string : " + encodedText);
452                     str = new String(quotedPrintableToUtf8(encodedText,charset));
453                     in = in.replace(match, str);
454                 }else if(encoding.equalsIgnoreCase("B")){
455                     // base64
456                     try{
457 
458                         Log.d(TAG,"StripEncoding: base64 string : " + encodedText);
459                         str = new String(Base64.decode(encodedText.getBytes(charset),
460                                 Base64.DEFAULT), charset);
461                         Log.d(TAG,"StripEncoding: decoded string : " + str);
462                         in = in.replace(match, str);
463                     }catch(UnsupportedEncodingException e){
464                         Log.e(TAG, "stripEncoding: Unsupported charset: " + charset);
465                     }catch (IllegalArgumentException e){
466                         Log.e(TAG,"stripEncoding: string not encoded as base64: " +encodedText);
467                     }
468                 }else{
469                     Log.e(TAG, "stripEncoding: Hit unknown encoding: "+encoding);
470                 }
471             }
472         }
473         return in;
474     }
475 
476 
477     /**
478      * Convert a quoted-printable encoded string to a UTF-8 string:
479      *  - Remove any soft line breaks: "=<CRLF>"
480      *  - Convert all "=xx" to the corresponding byte
481      * @param text quoted-printable encoded UTF-8 text
482      * @return decoded UTF-8 string
483      */
quotedPrintableToUtf8(String text, String charset)484     public static byte[] quotedPrintableToUtf8(String text, String charset) {
485         byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
486         byte[] input = null;
487         try {
488             input = text.getBytes("US-ASCII");
489         } catch (UnsupportedEncodingException e) {
490             /* This cannot happen as "US-ASCII" is supported for all Java implementations */ }
491 
492         if(input == null){
493             return "".getBytes();
494         }
495 
496         int in, out, stopCnt = input.length-2; // Leave room for peaking the next two bytes
497 
498         /* Algorithm:
499          *  - Search for token, copying all non token chars
500          * */
501         for(in=0, out=0; in < stopCnt; in++){
502             byte b0 = input[in];
503             if(b0 == '=') {
504                 byte b1 = input[++in];
505                 byte b2 = input[++in];
506                 if(b1 == '\r' && b2 == '\n') {
507                     continue; // soft line break, remove all tree;
508                 }
509                 if(((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F')
510                         || (b1 >= 'a' && b1 <= 'f'))
511                         && ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F')
512                         || (b2 >= 'a' && b2 <= 'f'))) {
513                     if(V)Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
514                     if(b1 <= '9')       b1 = (byte) (b1 - '0');
515                     else if (b1 <= 'F') b1 = (byte) (b1 - 'A' + 10);
516                     else if (b1 <= 'f') b1 = (byte) (b1 - 'a' + 10);
517 
518                     if(b2 <= '9')       b2 = (byte) (b2 - '0');
519                     else if (b2 <= 'F') b2 = (byte) (b2 - 'A' + 10);
520                     else if (b2 <= 'f') b2 = (byte) (b2 - 'a' + 10);
521 
522                     if(V)Log.v(TAG, "Resulting nibble values: " +
523                             String.format("b1=%x b2=%x", b1, b2));
524 
525                     output[out++] = (byte)(b1<<4 | b2); // valid hex char, append
526                     if(V)Log.v(TAG, "Resulting value: "  + String.format("0x%2x", output[out-1]));
527                     continue;
528                 }
529                 Log.w(TAG, "Received wrongly quoted printable encoded text. " +
530                         "Continuing at best effort...");
531                 /* If we get a '=' without either a hex value or CRLF following, just add it and
532                  * rewind the in counter. */
533                 output[out++] = b0;
534                 in -= 2;
535                 continue;
536             } else {
537                 output[out++] = b0;
538                 continue;
539             }
540         }
541 
542         // Just add any remaining characters. If they contain any encoding, it is invalid,
543         // and best effort would be just to display the characters.
544         while (in < input.length) {
545             output[out++] = input[in++];
546         }
547 
548         String result = null;
549         // Figure out if we support the charset, else fall back to UTF-8, as this is what
550         // the MAP specification suggest to use, and is compatible with US-ASCII.
551         if(charset == null){
552             charset = "UTF-8";
553         } else {
554             charset = charset.toUpperCase();
555             try {
556                 if(Charset.isSupported(charset) == false) {
557                     charset = "UTF-8";
558                 }
559             } catch (IllegalCharsetNameException e) {
560                 Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
561                 charset = "UTF-8";
562             }
563         }
564         try{
565             result = new String(output, 0, out, charset);
566         } catch (UnsupportedEncodingException e) {
567             /* This cannot happen unless Charset.isSupported() is out of sync with String */
568             try{
569                 result = new String(output, 0, out, "UTF-8");
570             } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
571         }
572         return result.getBytes(); /* return the result as "UTF-8" bytes */
573     }
574 
575     /**
576      * Encodes an array of bytes into an array of quoted-printable 7-bit characters.
577      * Unsafe characters are escaped.
578      * Simplified version of encoder from QuetedPrintableCodec.java (Apache external)
579      *
580      * @param bytes
581      *                  array of bytes to be encoded
582      * @return UTF-8 string containing quoted-printable characters
583      */
584 
585     private static byte ESCAPE_CHAR = '=';
586     private static byte TAB = 9;
587     private static byte SPACE = 32;
588 
encodeQuotedPrintable(byte[] bytes)589     public static final String encodeQuotedPrintable(byte[] bytes) {
590         if (bytes == null) {
591             return null;
592         }
593 
594         BitSet printable = new BitSet(256);
595         // alpha characters
596         for (int i = 33; i <= 60; i++) {
597             printable.set(i);
598         }
599         for (int i = 62; i <= 126; i++) {
600             printable.set(i);
601         }
602         printable.set(TAB);
603         printable.set(SPACE);
604         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
605         for (int i = 0; i < bytes.length; i++) {
606             int b = bytes[i];
607             if (b < 0) {
608                 b = 256 + b;
609             }
610             if (printable.get(b)) {
611                 buffer.write(b);
612             } else {
613                 buffer.write(ESCAPE_CHAR);
614                 char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
615                 char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
616                 buffer.write(hex1);
617                 buffer.write(hex2);
618             }
619         }
620         try {
621             return buffer.toString("UTF-8");
622         } catch (UnsupportedEncodingException e) {
623             //cannot happen
624             return "";
625         }
626     }
627 
628 }
629 
630