• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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