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