BluetoothMapUtils.java revision bc5ec351447ea8841b9a27e18772aefd62b047bc
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