1 /* 2 * Copyright (C) 2013 Samsung System LSI 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.android.bluetooth.map; 16 17 import android.database.Cursor; 18 import android.util.Base64; 19 import android.util.Log; 20 21 import com.android.bluetooth.mapapi.BluetoothMapContract; 22 23 import java.io.ByteArrayOutputStream; 24 import java.io.UnsupportedEncodingException; 25 import java.nio.charset.Charset; 26 import java.nio.charset.IllegalCharsetNameException; 27 import java.text.SimpleDateFormat; 28 import java.util.Arrays; 29 import java.util.BitSet; 30 import java.util.Date; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 35 /** 36 * Various utility methods and generic defines that can be used throughout MAPS 37 */ 38 public class BluetoothMapUtils { 39 40 private static final String TAG = "BluetoothMapUtils"; 41 private static final boolean D = BluetoothMapService.DEBUG; 42 private static final boolean V = BluetoothMapService.VERBOSE; 43 /* We use the upper 4 bits for the type mask. 44 * TODO: When more types are needed, consider just using a number 45 * in stead of a bit to indicate the message type. Then 4 46 * bit can be use for 16 different message types. 47 */ 48 private static final long HANDLE_TYPE_MASK = (((long) 0xff) << 56); 49 private static final long HANDLE_TYPE_MMS_MASK = (((long) 0x01) << 56); 50 private static final long HANDLE_TYPE_EMAIL_MASK = (((long) 0x02) << 56); 51 private static final long HANDLE_TYPE_SMS_GSM_MASK = (((long) 0x04) << 56); 52 private static final long HANDLE_TYPE_SMS_CDMA_MASK = (((long) 0x08) << 56); 53 private static final long HANDLE_TYPE_IM_MASK = (((long) 0x10) << 56); 54 55 public static final long CONVO_ID_TYPE_SMS_MMS = 1; 56 public static final long CONVO_ID_TYPE_EMAIL_IM = 2; 57 58 // MAP supported feature bit - included from MAP Spec 1.2 59 static final int MAP_FEATURE_DEFAULT_BITMASK = 0x0000001F; 60 61 static final int MAP_FEATURE_NOTIFICATION_REGISTRATION_BIT = 1 << 0; 62 static final int MAP_FEATURE_NOTIFICATION_BIT = 1 << 1; 63 static final int MAP_FEATURE_BROWSING_BIT = 1 << 2; 64 static final int MAP_FEATURE_UPLOADING_BIT = 1 << 3; 65 static final int MAP_FEATURE_DELETE_BIT = 1 << 4; 66 static final int MAP_FEATURE_INSTANCE_INFORMATION_BIT = 1 << 5; 67 static final int MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT = 1 << 6; 68 static final int MAP_FEATURE_EVENT_REPORT_V12_BIT = 1 << 7; 69 static final int MAP_FEATURE_MESSAGE_FORMAT_V11_BIT = 1 << 8; 70 static final int MAP_FEATURE_MESSAGE_LISTING_FORMAT_V11_BIT = 1 << 9; 71 static final int MAP_FEATURE_PERSISTENT_MESSAGE_HANDLE_BIT = 1 << 10; 72 static final int MAP_FEATURE_DATABASE_INDENTIFIER_BIT = 1 << 11; 73 static final int MAP_FEATURE_FOLDER_VERSION_COUNTER_BIT = 1 << 12; 74 static final int MAP_FEATURE_CONVERSATION_VERSION_COUNTER_BIT = 1 << 13; 75 static final int MAP_FEATURE_PARTICIPANT_PRESENCE_CHANGE_BIT = 1 << 14; 76 static final int MAP_FEATURE_PARTICIPANT_CHAT_STATE_CHANGE_BIT = 1 << 15; 77 78 static final int MAP_FEATURE_PBAP_CONTACT_CROSS_REFERENCE_BIT = 1 << 16; 79 static final int MAP_FEATURE_NOTIFICATION_FILTERING_BIT = 1 << 17; 80 static final int MAP_FEATURE_DEFINED_TIMESTAMP_FORMAT_BIT = 1 << 18; 81 82 static final String MAP_V10_STR = "1.0"; 83 static final String MAP_V11_STR = "1.1"; 84 static final String MAP_V12_STR = "1.2"; 85 86 // Event Report versions 87 static final int MAP_EVENT_REPORT_V10 = 10; // MAP spec 1.1 88 static final int MAP_EVENT_REPORT_V11 = 11; // MAP spec 1.2 89 static final int MAP_EVENT_REPORT_V12 = 12; // MAP spec 1.3 'to be' incl. IM 90 91 // Message Format versions 92 static final int MAP_MESSAGE_FORMAT_V10 = 10; // MAP spec below 1.3 93 static final int MAP_MESSAGE_FORMAT_V11 = 11; // MAP spec 1.3 94 95 // Message Listing Format versions 96 static final int MAP_MESSAGE_LISTING_FORMAT_V10 = 10; // MAP spec below 1.3 97 static final int MAP_MESSAGE_LISTING_FORMAT_V11 = 11; // MAP spec 1.3 98 99 /** 100 * This enum is used to convert from the bMessage type property to a type safe 101 * type. Hence do not change the names of the enum values. 102 */ 103 public enum TYPE { 104 NONE, EMAIL, SMS_GSM, SMS_CDMA, MMS, IM; 105 private static TYPE[] sAllValues = values(); 106 fromOrdinal(int n)107 public static TYPE fromOrdinal(int n) { 108 if (n < sAllValues.length) { 109 return sAllValues[n]; 110 } 111 return NONE; 112 } 113 } 114 getDateTimeString(long timestamp)115 public static String getDateTimeString(long timestamp) { 116 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 117 Date date = new Date(timestamp); 118 return format.format(date); // Format to YYYYMMDDTHHMMSS local time 119 } 120 121 printCursor(Cursor c)122 public static void printCursor(Cursor c) { 123 if (D) { 124 StringBuilder sb = new StringBuilder(); 125 sb.append("\nprintCursor:\n"); 126 for (int i = 0; i < c.getColumnCount(); i++) { 127 if (c.getColumnName(i).equals(BluetoothMapContract.MessageColumns.DATE) 128 || c.getColumnName(i) 129 .equals(BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY) 130 || c.getColumnName(i) 131 .equals(BluetoothMapContract.ChatStatusColumns.LAST_ACTIVE) 132 || c.getColumnName(i) 133 .equals(BluetoothMapContract.PresenceColumns.LAST_ONLINE)) { 134 sb.append(" ") 135 .append(c.getColumnName(i)) 136 .append(" : ") 137 .append(getDateTimeString(c.getLong(i))) 138 .append("\n"); 139 } else { 140 sb.append(" ") 141 .append(c.getColumnName(i)) 142 .append(" : ") 143 .append(c.getString(i)) 144 .append("\n"); 145 } 146 } 147 Log.d(TAG, sb.toString()); 148 } 149 } 150 getLongAsString(long v)151 public static String getLongAsString(long v) { 152 char[] result = new char[16]; 153 int v1 = (int) (v & 0xffffffff); 154 int v2 = (int) ((v >> 32) & 0xffffffff); 155 int c; 156 for (int i = 0; i < 8; i++) { 157 c = v2 & 0x0f; 158 c += (c < 10) ? '0' : ('A' - 10); 159 result[7 - i] = (char) c; 160 v2 >>= 4; 161 c = v1 & 0x0f; 162 c += (c < 10) ? '0' : ('A' - 10); 163 result[15 - i] = (char) c; 164 v1 >>= 4; 165 } 166 return new String(result); 167 } 168 169 /** 170 * Converts a hex-string to a long - please mind that Java has no unsigned data types, hence 171 * any value passed to this function, which has the upper bit set, will return a negative value. 172 * The bitwise content of the variable will however be the same. 173 * Will ignore any white-space characters as well as '-' seperators 174 * @param valueStr a hexstring - NOTE: shall not contain any "0x" prefix. 175 * @return 176 * @throws UnsupportedEncodingException if "US-ASCII" charset is not supported, 177 * NullPointerException if a null pointer is passed to the function, 178 * NumberFormatException if the string contains invalid characters. 179 * 180 */ getLongFromString(String valueStr)181 public static long getLongFromString(String valueStr) throws UnsupportedEncodingException { 182 if (valueStr == null) { 183 throw new NullPointerException(); 184 } 185 if (V) { 186 Log.i(TAG, "getLongFromString(): converting: " + valueStr); 187 } 188 byte[] nibbles; 189 nibbles = valueStr.getBytes("US-ASCII"); 190 if (V) { 191 Log.i(TAG, " byte values: " + Arrays.toString(nibbles)); 192 } 193 byte c; 194 int count = 0; 195 int length = nibbles.length; 196 long value = 0; 197 for (int i = 0; i != length; i++) { 198 c = nibbles[i]; 199 if (c >= '0' && c <= '9') { 200 c -= '0'; 201 } else if (c >= 'A' && c <= 'F') { 202 c -= ('A' - 10); 203 } else if (c >= 'a' && c <= 'f') { 204 c -= ('a' - 10); 205 } else if (c <= ' ' || c == '-') { 206 if (V) { 207 Log.v(TAG, 208 "Skipping c = '" + new String(new byte[]{(byte) c}, "US-ASCII") + "'"); 209 } 210 continue; // Skip any whitespace and '-' (which is used for UUIDs) 211 } else { 212 throw new NumberFormatException("Invalid character:" + c); 213 } 214 value = value << 4; // The last nibble shall not be shifted 215 value += c; 216 count++; 217 if (count > 16) { 218 throw new NullPointerException("String to large - count: " + count); 219 } 220 } 221 if (V) { 222 Log.i(TAG, " length: " + count); 223 } 224 return value; 225 } 226 227 private static final int LONG_LONG_LENGTH = 32; 228 getLongLongAsString(long vLow, long vHigh)229 public static String getLongLongAsString(long vLow, long vHigh) { 230 char[] result = new char[LONG_LONG_LENGTH]; 231 int v1 = (int) (vLow & 0xffffffff); 232 int v2 = (int) ((vLow >> 32) & 0xffffffff); 233 int v3 = (int) (vHigh & 0xffffffff); 234 int v4 = (int) ((vHigh >> 32) & 0xffffffff); 235 int c, d, i; 236 // Handle the lower bytes 237 for (i = 0; i < 8; i++) { 238 c = v2 & 0x0f; 239 c += (c < 10) ? '0' : ('A' - 10); 240 d = v4 & 0x0f; 241 d += (d < 10) ? '0' : ('A' - 10); 242 result[23 - i] = (char) c; 243 result[7 - i] = (char) d; 244 v2 >>= 4; 245 v4 >>= 4; 246 c = v1 & 0x0f; 247 c += (c < 10) ? '0' : ('A' - 10); 248 d = v3 & 0x0f; 249 d += (d < 10) ? '0' : ('A' - 10); 250 result[31 - i] = (char) c; 251 result[15 - i] = (char) d; 252 v1 >>= 4; 253 v3 >>= 4; 254 } 255 // Remove any leading 0's 256 for (i = 0; i < LONG_LONG_LENGTH; i++) { 257 if (result[i] != '0') { 258 break; 259 } 260 } 261 return new String(result, i, LONG_LONG_LENGTH - i); 262 } 263 264 265 /** 266 * Convert a Content Provider handle and a Messagetype into a unique handle 267 * @param cpHandle content provider handle 268 * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL) 269 * @return String Formatted Map Handle 270 */ getMapHandle(long cpHandle, TYPE messageType)271 public static String getMapHandle(long cpHandle, TYPE messageType) { 272 String mapHandle = "-1"; 273 /* Avoid NPE for possible "null" value of messageType */ 274 if (messageType != null) { 275 switch (messageType) { 276 case MMS: 277 mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_MMS_MASK); 278 break; 279 case SMS_GSM: 280 mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_GSM_MASK); 281 break; 282 case SMS_CDMA: 283 mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_CDMA_MASK); 284 break; 285 case EMAIL: 286 mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_EMAIL_MASK); 287 break; 288 case IM: 289 mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_IM_MASK); 290 break; 291 case NONE: 292 break; 293 default: 294 throw new IllegalArgumentException("Message type not supported"); 295 } 296 } else { 297 if (D) { 298 Log.e(TAG, " Invalid messageType input"); 299 } 300 } 301 return mapHandle; 302 303 } 304 305 /** 306 * Convert a Content Provider handle and a Messagetype into a unique handle 307 * @param cpHandle content provider handle 308 * @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL) 309 * @return String Formatted Map Handle 310 */ getMapConvoHandle(long cpHandle, TYPE messageType)311 public static String getMapConvoHandle(long cpHandle, TYPE messageType) { 312 String mapHandle = "-1"; 313 switch (messageType) { 314 case MMS: 315 case SMS_GSM: 316 case SMS_CDMA: 317 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_SMS_MMS); 318 break; 319 case EMAIL: 320 case IM: 321 mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_EMAIL_IM); 322 break; 323 default: 324 throw new IllegalArgumentException("Message type not supported"); 325 } 326 return mapHandle; 327 328 } 329 330 /** 331 * Convert a handle string the the raw long representation, including the type bit. 332 * @param mapHandle the handle string 333 * @return the handle value 334 */ getMsgHandleAsLong(String mapHandle)335 public static long getMsgHandleAsLong(String mapHandle) { 336 return Long.parseLong(mapHandle, 16); 337 } 338 339 /** 340 * Convert a Map Handle into a content provider Handle 341 * @param mapHandle handle to convert from 342 * @return content provider handle without message type mask 343 */ getCpHandle(String mapHandle)344 public static long getCpHandle(String mapHandle) { 345 long cpHandle = getMsgHandleAsLong(mapHandle); 346 if (D) { 347 Log.d(TAG, "-> MAP handle:" + mapHandle); 348 } 349 /* remove masks as the call should already know what type of message this handle is for */ 350 cpHandle &= ~HANDLE_TYPE_MASK; 351 if (D) { 352 Log.d(TAG, "->CP handle:" + cpHandle); 353 } 354 355 return cpHandle; 356 } 357 358 /** 359 * Extract the message type from the handle. 360 * @param mapHandle 361 * @return 362 */ getMsgTypeFromHandle(String mapHandle)363 public static TYPE getMsgTypeFromHandle(String mapHandle) { 364 long cpHandle = getMsgHandleAsLong(mapHandle); 365 366 if ((cpHandle & HANDLE_TYPE_MMS_MASK) != 0) { 367 return TYPE.MMS; 368 } 369 if ((cpHandle & HANDLE_TYPE_EMAIL_MASK) != 0) { 370 return TYPE.EMAIL; 371 } 372 if ((cpHandle & HANDLE_TYPE_SMS_GSM_MASK) != 0) { 373 return TYPE.SMS_GSM; 374 } 375 if ((cpHandle & HANDLE_TYPE_SMS_CDMA_MASK) != 0) { 376 return TYPE.SMS_CDMA; 377 } 378 if ((cpHandle & HANDLE_TYPE_IM_MASK) != 0) { 379 return TYPE.IM; 380 } 381 382 throw new IllegalArgumentException("Message type not found in handle string."); 383 } 384 385 /** 386 * TODO: Is this still needed after changing to another XML encoder? It should escape illegal 387 * characters. 388 * Strip away any illegal XML characters, that would otherwise cause the 389 * xml serializer to throw an exception. 390 * Examples of such characters are the emojis used on Android. 391 * @param text The string to validate 392 * @return the same string if valid, otherwise a new String stripped for 393 * any illegal characters. If a null pointer is passed an empty string will be returned. 394 */ stripInvalidChars(String text)395 public static String stripInvalidChars(String text) { 396 if (text == null) { 397 return ""; 398 } 399 char[] out = new char[text.length()]; 400 int i, o, l; 401 for (i = 0, o = 0, l = text.length(); i < l; i++) { 402 char c = text.charAt(i); 403 if ((c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd)) { 404 out[o++] = c; 405 } // Else we skip the character 406 } 407 408 if (i == o) { 409 return text; 410 } else { // We removed some characters, create the new string 411 return new String(out, 0, o); 412 } 413 } 414 415 /** 416 * Truncate UTF-8 string encoded byte array to desired length 417 * @param utf8String String to convert to bytes array h 418 * @param maxLength Max length of byte array returned including null termination 419 * @return byte array containing valid utf8 characters with max length 420 * @throws UnsupportedEncodingException 421 */ truncateUtf8StringToBytearray(String utf8String, int maxLength)422 public static byte[] truncateUtf8StringToBytearray(String utf8String, int maxLength) 423 throws UnsupportedEncodingException { 424 425 byte[] utf8Bytes = new byte[utf8String.length() + 1]; 426 try { 427 System.arraycopy(utf8String.getBytes("UTF-8"), 0, utf8Bytes, 0, utf8String.length()); 428 } catch (UnsupportedEncodingException e) { 429 Log.e(TAG, "truncateUtf8StringToBytearray: getBytes exception ", e); 430 throw e; 431 } 432 433 if (utf8Bytes.length > maxLength) { 434 /* if 'continuation' byte is in place 200, 435 * then strip previous bytes until utf-8 start byte is found */ 436 if ((utf8Bytes[maxLength - 1] & 0xC0) == 0x80) { 437 for (int i = maxLength - 2; i >= 0; i--) { 438 if ((utf8Bytes[i] & 0xC0) == 0xC0) { 439 /* first byte in utf-8 character found, 440 * now copy i - 1 bytes to outBytes and add null termination */ 441 utf8Bytes = Arrays.copyOf(utf8Bytes, i + 1); 442 utf8Bytes[i] = 0; 443 break; 444 } 445 } 446 } else { 447 /* copy bytes to outBytes and null terminate */ 448 utf8Bytes = Arrays.copyOf(utf8Bytes, maxLength); 449 utf8Bytes[maxLength - 1] = 0; 450 } 451 } 452 return utf8Bytes; 453 } 454 455 private static final Pattern PATTERN = Pattern.compile("=\\?(.+?)\\?(.)\\?(.+?(?=\\?=))\\?="); 456 457 /** 458 * Method for converting quoted printable og base64 encoded string from headers. 459 * @param in the string with encoding 460 * @return decoded string if success - else the same string as was as input. 461 */ stripEncoding(String in)462 public static String stripEncoding(String in) { 463 String str = null; 464 if (in.contains("=?") && in.contains("?=")) { 465 String encoding; 466 String charset; 467 String encodedText; 468 String match; 469 Matcher m = PATTERN.matcher(in); 470 while (m.find()) { 471 match = m.group(0); 472 charset = m.group(1); 473 encoding = m.group(2); 474 encodedText = m.group(3); 475 Log.v(TAG, 476 "Matching:" + match + "\nCharset: " + charset + "\nEncoding : " + encoding 477 + "\nText: " + encodedText); 478 if (encoding.equalsIgnoreCase("Q")) { 479 //quoted printable 480 Log.d(TAG, "StripEncoding: Quoted Printable string : " + encodedText); 481 str = new String(quotedPrintableToUtf8(encodedText, charset)); 482 in = in.replace(match, str); 483 } else if (encoding.equalsIgnoreCase("B")) { 484 // base64 485 try { 486 487 Log.d(TAG, "StripEncoding: base64 string : " + encodedText); 488 str = new String( 489 Base64.decode(encodedText.getBytes(charset), Base64.DEFAULT), 490 charset); 491 Log.d(TAG, "StripEncoding: decoded string : " + str); 492 in = in.replace(match, str); 493 } catch (UnsupportedEncodingException e) { 494 Log.e(TAG, "stripEncoding: Unsupported charset: " + charset); 495 } catch (IllegalArgumentException e) { 496 Log.e(TAG, "stripEncoding: string not encoded as base64: " + encodedText); 497 } 498 } else { 499 Log.e(TAG, "stripEncoding: Hit unknown encoding: " + encoding); 500 } 501 } 502 } 503 return in; 504 } 505 506 507 /** 508 * Convert a quoted-printable encoded string to a UTF-8 string: 509 * - Remove any soft line breaks: "=<CRLF>" 510 * - Convert all "=xx" to the corresponding byte 511 * @param text quoted-printable encoded UTF-8 text 512 * @return decoded UTF-8 string 513 */ quotedPrintableToUtf8(String text, String charset)514 public static byte[] quotedPrintableToUtf8(String text, String charset) { 515 byte[] output = new byte[text.length()]; // We allocate for the worst case memory need 516 byte[] input = null; 517 try { 518 input = text.getBytes("US-ASCII"); 519 } catch (UnsupportedEncodingException e) { 520 /* This cannot happen as "US-ASCII" is supported for all Java implementations */ 521 } 522 523 if (input == null) { 524 return "".getBytes(); 525 } 526 527 int in, out, stopCnt = input.length - 2; // Leave room for peaking the next two bytes 528 529 /* Algorithm: 530 * - Search for token, copying all non token chars 531 * */ 532 for (in = 0, out = 0; in < stopCnt; in++) { 533 byte b0 = input[in]; 534 if (b0 == '=') { 535 byte b1 = input[++in]; 536 byte b2 = input[++in]; 537 if (b1 == '\r' && b2 == '\n') { 538 continue; // soft line break, remove all tree; 539 } 540 if (((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F') || (b1 >= 'a' 541 && b1 <= 'f')) && ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F') || ( 542 b2 >= 'a' && b2 <= 'f'))) { 543 if (V) { 544 Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2)); 545 } 546 if (b1 <= '9') { 547 b1 = (byte) (b1 - '0'); 548 } else if (b1 <= 'F') { 549 b1 = (byte) (b1 - 'A' + 10); 550 } else if (b1 <= 'f') { 551 b1 = (byte) (b1 - 'a' + 10); 552 } 553 554 if (b2 <= '9') { 555 b2 = (byte) (b2 - '0'); 556 } else if (b2 <= 'F') { 557 b2 = (byte) (b2 - 'A' + 10); 558 } else if (b2 <= 'f') { 559 b2 = (byte) (b2 - 'a' + 10); 560 } 561 562 if (V) { 563 Log.v(TAG, 564 "Resulting nibble values: " + String.format("b1=%x b2=%x", b1, b2)); 565 } 566 567 output[out++] = (byte) (b1 << 4 | b2); // valid hex char, append 568 if (V) { 569 Log.v(TAG, "Resulting value: " + String.format("0x%2x", output[out - 1])); 570 } 571 continue; 572 } 573 Log.w(TAG, "Received wrongly quoted printable encoded text. " 574 + "Continuing at best effort..."); 575 /* If we get a '=' without either a hex value or CRLF following, just add it and 576 * rewind the in counter. */ 577 output[out++] = b0; 578 in -= 2; 579 continue; 580 } else { 581 output[out++] = b0; 582 continue; 583 } 584 } 585 586 // Just add any remaining characters. If they contain any encoding, it is invalid, 587 // and best effort would be just to display the characters. 588 while (in < input.length) { 589 output[out++] = input[in++]; 590 } 591 592 String result = null; 593 // Figure out if we support the charset, else fall back to UTF-8, as this is what 594 // the MAP specification suggest to use, and is compatible with US-ASCII. 595 if (charset == null) { 596 charset = "UTF-8"; 597 } else { 598 charset = charset.toUpperCase(); 599 try { 600 if (!Charset.isSupported(charset)) { 601 charset = "UTF-8"; 602 } 603 } catch (IllegalCharsetNameException e) { 604 Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8."); 605 charset = "UTF-8"; 606 } 607 } 608 try { 609 result = new String(output, 0, out, charset); 610 } catch (UnsupportedEncodingException e) { 611 /* This cannot happen unless Charset.isSupported() is out of sync with String */ 612 try { 613 result = new String(output, 0, out, "UTF-8"); 614 } catch (UnsupportedEncodingException e2) { 615 Log.e(TAG, "quotedPrintableToUtf8: " + e); 616 } 617 } 618 return result.getBytes(); /* return the result as "UTF-8" bytes */ 619 } 620 621 /** 622 * Encodes an array of bytes into an array of quoted-printable 7-bit characters. 623 * Unsafe characters are escaped. 624 * Simplified version of encoder from QuetedPrintableCodec.java (Apache external) 625 * 626 * @param bytes 627 * array of bytes to be encoded 628 * @return UTF-8 string containing quoted-printable characters 629 */ 630 631 private static final byte ESCAPE_CHAR = '='; 632 private static final byte TAB = 9; 633 private static final byte SPACE = 32; 634 encodeQuotedPrintable(byte[] bytes)635 public static final String encodeQuotedPrintable(byte[] bytes) { 636 if (bytes == null) { 637 return null; 638 } 639 640 BitSet printable = new BitSet(256); 641 // alpha characters 642 for (int i = 33; i <= 60; i++) { 643 printable.set(i); 644 } 645 for (int i = 62; i <= 126; i++) { 646 printable.set(i); 647 } 648 printable.set(TAB); 649 printable.set(SPACE); 650 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 651 for (int i = 0; i < bytes.length; i++) { 652 int b = bytes[i]; 653 if (b < 0) { 654 b = 256 + b; 655 } 656 if (printable.get(b)) { 657 buffer.write(b); 658 } else { 659 buffer.write(ESCAPE_CHAR); 660 char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); 661 char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); 662 buffer.write(hex1); 663 buffer.write(hex2); 664 } 665 } 666 try { 667 return buffer.toString("UTF-8"); 668 } catch (UnsupportedEncodingException e) { 669 //cannot happen 670 return ""; 671 } 672 } 673 674 } 675 676