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