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