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