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