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