BluetoothMapUtils.java revision b5dde8c7ec12952afb9fee7a41707e65d2a61737
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*/
15package com.android.bluetooth.map;
16
17import android.database.Cursor;
18import android.util.Base64;
19import android.util.Log;
20
21import com.android.bluetooth.mapapi.BluetoothMapContract;
22
23import java.io.ByteArrayOutputStream;
24import java.io.UnsupportedEncodingException;
25import java.nio.charset.Charset;
26import java.nio.charset.IllegalCharsetNameException;
27import java.text.SimpleDateFormat;
28import java.util.Arrays;
29import java.util.BitSet;
30import java.util.Date;
31import java.util.regex.Matcher;
32import java.util.regex.Pattern;
33
34
35/**
36 * Various utility methods and generic defines that can be used throughout MAPS
37 */
38public 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 = 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     */
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     */
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
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