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