blob: e867bd6d57777cbce9c685a9c974154c0647b9d0 [file] [log] [blame]
/*
* Copyright (C) 2013 Samsung System LSI
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.map;
import android.database.Cursor;
import android.util.Base64;
import android.util.Log;
import com.android.bluetooth.mapapi.BluetoothMapContract;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Various utility methods and generic defines that can be used throughout MAPS
*/
public class BluetoothMapUtils {
private static final String TAG = "BluetoothMapUtils";
private static final boolean D = BluetoothMapService.DEBUG;
private static final boolean V = BluetoothMapService.VERBOSE;
/* We use the upper 4 bits for the type mask.
* TODO: When more types are needed, consider just using a number
* in stead of a bit to indicate the message type. Then 4
* bit can be use for 16 different message types.
*/
private static final long HANDLE_TYPE_MASK = (((long) 0xff) << 56);
private static final long HANDLE_TYPE_MMS_MASK = (((long) 0x01) << 56);
private static final long HANDLE_TYPE_EMAIL_MASK = (((long) 0x02) << 56);
private static final long HANDLE_TYPE_SMS_GSM_MASK = (((long) 0x04) << 56);
private static final long HANDLE_TYPE_SMS_CDMA_MASK = (((long) 0x08) << 56);
private static final long HANDLE_TYPE_IM_MASK = (((long) 0x10) << 56);
public static final long CONVO_ID_TYPE_SMS_MMS = 1;
public static final long CONVO_ID_TYPE_EMAIL_IM = 2;
// MAP supported feature bit - included from MAP Spec 1.2
static final int MAP_FEATURE_DEFAULT_BITMASK = 0x0000001F;
static final int MAP_FEATURE_NOTIFICATION_REGISTRATION_BIT = 1 << 0;
static final int MAP_FEATURE_NOTIFICATION_BIT = 1 << 1;
static final int MAP_FEATURE_BROWSING_BIT = 1 << 2;
static final int MAP_FEATURE_UPLOADING_BIT = 1 << 3;
static final int MAP_FEATURE_DELETE_BIT = 1 << 4;
static final int MAP_FEATURE_INSTANCE_INFORMATION_BIT = 1 << 5;
static final int MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT = 1 << 6;
static final int MAP_FEATURE_EVENT_REPORT_V12_BIT = 1 << 7;
static final int MAP_FEATURE_MESSAGE_FORMAT_V11_BIT = 1 << 8;
static final int MAP_FEATURE_MESSAGE_LISTING_FORMAT_V11_BIT = 1 << 9;
static final int MAP_FEATURE_PERSISTENT_MESSAGE_HANDLE_BIT = 1 << 10;
static final int MAP_FEATURE_DATABASE_INDENTIFIER_BIT = 1 << 11;
static final int MAP_FEATURE_FOLDER_VERSION_COUNTER_BIT = 1 << 12;
static final int MAP_FEATURE_CONVERSATION_VERSION_COUNTER_BIT = 1 << 13;
static final int MAP_FEATURE_PARTICIPANT_PRESENCE_CHANGE_BIT = 1 << 14;
static final int MAP_FEATURE_PARTICIPANT_CHAT_STATE_CHANGE_BIT = 1 << 15;
static final int MAP_FEATURE_PBAP_CONTACT_CROSS_REFERENCE_BIT = 1 << 16;
static final int MAP_FEATURE_NOTIFICATION_FILTERING_BIT = 1 << 17;
static final int MAP_FEATURE_DEFINED_TIMESTAMP_FORMAT_BIT = 1 << 18;
static final String MAP_V10_STR = "1.0";
static final String MAP_V11_STR = "1.1";
static final String MAP_V12_STR = "1.2";
// Event Report versions
static final int MAP_EVENT_REPORT_V10 = 10; // MAP spec 1.1
static final int MAP_EVENT_REPORT_V11 = 11; // MAP spec 1.2
static final int MAP_EVENT_REPORT_V12 = 12; // MAP spec 1.3 'to be' incl. IM
// Message Format versions
static final int MAP_MESSAGE_FORMAT_V10 = 10; // MAP spec below 1.3
static final int MAP_MESSAGE_FORMAT_V11 = 11; // MAP spec 1.3
// Message Listing Format versions
static final int MAP_MESSAGE_LISTING_FORMAT_V10 = 10; // MAP spec below 1.3
static final int MAP_MESSAGE_LISTING_FORMAT_V11 = 11; // MAP spec 1.3
/**
* This enum is used to convert from the bMessage type property to a type safe
* type. Hence do not change the names of the enum values.
*/
public enum TYPE {
NONE, EMAIL, SMS_GSM, SMS_CDMA, MMS, IM;
private static TYPE[] sAllValues = values();
public static TYPE fromOrdinal(int n) {
if (n < sAllValues.length) {
return sAllValues[n];
}
return NONE;
}
}
public static String getDateTimeString(long timestamp) {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
Date date = new Date(timestamp);
return format.format(date); // Format to YYYYMMDDTHHMMSS local time
}
public static void printCursor(Cursor c) {
if (D) {
StringBuilder sb = new StringBuilder();
sb.append("\nprintCursor:\n");
for (int i = 0; i < c.getColumnCount(); i++) {
if (c.getColumnName(i).equals(BluetoothMapContract.MessageColumns.DATE)
|| c.getColumnName(i)
.equals(BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY)
|| c.getColumnName(i)
.equals(BluetoothMapContract.ChatStatusColumns.LAST_ACTIVE)
|| c.getColumnName(i)
.equals(BluetoothMapContract.PresenceColumns.LAST_ONLINE)) {
sb.append(" ")
.append(c.getColumnName(i))
.append(" : ")
.append(getDateTimeString(c.getLong(i)))
.append("\n");
} else {
sb.append(" ")
.append(c.getColumnName(i))
.append(" : ")
.append(c.getString(i))
.append("\n");
}
}
Log.d(TAG, sb.toString());
}
}
public static String getLongAsString(long v) {
char[] result = new char[16];
int v1 = (int) (v & 0xffffffff);
int v2 = (int) ((v >> 32) & 0xffffffff);
int c;
for (int i = 0; i < 8; i++) {
c = v2 & 0x0f;
c += (c < 10) ? '0' : ('A' - 10);
result[7 - i] = (char) c;
v2 >>= 4;
c = v1 & 0x0f;
c += (c < 10) ? '0' : ('A' - 10);
result[15 - i] = (char) c;
v1 >>= 4;
}
return new String(result);
}
/**
* Converts a hex-string to a long - please mind that Java has no unsigned data types, hence
* any value passed to this function, which has the upper bit set, will return a negative value.
* The bitwise content of the variable will however be the same.
* Will ignore any white-space characters as well as '-' seperators
* @param valueStr a hexstring - NOTE: shall not contain any "0x" prefix.
* @return
* @throws UnsupportedEncodingException if "US-ASCII" charset is not supported,
* NullPointerException if a null pointer is passed to the function,
* NumberFormatException if the string contains invalid characters.
*
*/
public static long getLongFromString(String valueStr) throws UnsupportedEncodingException {
if (valueStr == null) {
throw new NullPointerException();
}
if (V) {
Log.i(TAG, "getLongFromString(): converting: " + valueStr);
}
byte[] nibbles;
nibbles = valueStr.getBytes("US-ASCII");
if (V) {
Log.i(TAG, " byte values: " + Arrays.toString(nibbles));
}
byte c;
int count = 0;
int length = nibbles.length;
long value = 0;
for (int i = 0; i != length; i++) {
c = nibbles[i];
if (c >= '0' && c <= '9') {
c -= '0';
} else if (c >= 'A' && c <= 'F') {
c -= ('A' - 10);
} else if (c >= 'a' && c <= 'f') {
c -= ('a' - 10);
} else if (c <= ' ' || c == '-') {
if (V) {
Log.v(TAG,
"Skipping c = '" + new String(new byte[]{(byte) c}, "US-ASCII") + "'");
}
continue; // Skip any whitespace and '-' (which is used for UUIDs)
} else {
throw new NumberFormatException("Invalid character:" + c);
}
value = value << 4; // The last nibble shall not be shifted
value += c;
count++;
if (count > 16) {
throw new NullPointerException("String to large - count: " + count);
}
}
if (V) {
Log.i(TAG, " length: " + count);
}
return value;
}
private static final int LONG_LONG_LENGTH = 32;
public static String getLongLongAsString(long vLow, long vHigh) {
char[] result = new char[LONG_LONG_LENGTH];
int v1 = (int) (vLow & 0xffffffff);
int v2 = (int) ((vLow >> 32) & 0xffffffff);
int v3 = (int) (vHigh & 0xffffffff);
int v4 = (int) ((vHigh >> 32) & 0xffffffff);
int c, d, i;
// Handle the lower bytes
for (i = 0; i < 8; i++) {
c = v2 & 0x0f;
c += (c < 10) ? '0' : ('A' - 10);
d = v4 & 0x0f;
d += (d < 10) ? '0' : ('A' - 10);
result[23 - i] = (char) c;
result[7 - i] = (char) d;
v2 >>= 4;
v4 >>= 4;
c = v1 & 0x0f;
c += (c < 10) ? '0' : ('A' - 10);
d = v3 & 0x0f;
d += (d < 10) ? '0' : ('A' - 10);
result[31 - i] = (char) c;
result[15 - i] = (char) d;
v1 >>= 4;
v3 >>= 4;
}
// Remove any leading 0's
for (i = 0; i < LONG_LONG_LENGTH; i++) {
if (result[i] != '0') {
break;
}
}
return new String(result, i, LONG_LONG_LENGTH - i);
}
/**
* Convert a Content Provider handle and a Messagetype into a unique handle
* @param cpHandle content provider handle
* @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
* @return String Formatted Map Handle
*/
public static String getMapHandle(long cpHandle, TYPE messageType) {
String mapHandle = "-1";
/* Avoid NPE for possible "null" value of messageType */
if (messageType != null) {
switch (messageType) {
case MMS:
mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_MMS_MASK);
break;
case SMS_GSM:
mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
break;
case SMS_CDMA:
mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
break;
case EMAIL:
mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_EMAIL_MASK);
break;
case IM:
mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_IM_MASK);
break;
case NONE:
break;
default:
throw new IllegalArgumentException("Message type not supported");
}
} else {
if (D) {
Log.e(TAG, " Invalid messageType input");
}
}
return mapHandle;
}
/**
* Convert a Content Provider handle and a Messagetype into a unique handle
* @param cpHandle content provider handle
* @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
* @return String Formatted Map Handle
*/
public static String getMapConvoHandle(long cpHandle, TYPE messageType) {
String mapHandle = "-1";
switch (messageType) {
case MMS:
case SMS_GSM:
case SMS_CDMA:
mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_SMS_MMS);
break;
case EMAIL:
case IM:
mapHandle = getLongLongAsString(cpHandle, CONVO_ID_TYPE_EMAIL_IM);
break;
default:
throw new IllegalArgumentException("Message type not supported");
}
return mapHandle;
}
/**
* Convert a handle string the the raw long representation, including the type bit.
* @param mapHandle the handle string
* @return the handle value
*/
public static long getMsgHandleAsLong(String mapHandle) {
return Long.parseLong(mapHandle, 16);
}
/**
* Convert a Map Handle into a content provider Handle
* @param mapHandle handle to convert from
* @return content provider handle without message type mask
*/
public static long getCpHandle(String mapHandle) {
long cpHandle = getMsgHandleAsLong(mapHandle);
if (D) {
Log.d(TAG, "-> MAP handle:" + mapHandle);
}
/* remove masks as the call should already know what type of message this handle is for */
cpHandle &= ~HANDLE_TYPE_MASK;
if (D) {
Log.d(TAG, "->CP handle:" + cpHandle);
}
return cpHandle;
}
/**
* Extract the message type from the handle.
* @param mapHandle
* @return
*/
public static TYPE getMsgTypeFromHandle(String mapHandle) {
long cpHandle = getMsgHandleAsLong(mapHandle);
if ((cpHandle & HANDLE_TYPE_MMS_MASK) != 0) {
return TYPE.MMS;
}
if ((cpHandle & HANDLE_TYPE_EMAIL_MASK) != 0) {
return TYPE.EMAIL;
}
if ((cpHandle & HANDLE_TYPE_SMS_GSM_MASK) != 0) {
return TYPE.SMS_GSM;
}
if ((cpHandle & HANDLE_TYPE_SMS_CDMA_MASK) != 0) {
return TYPE.SMS_CDMA;
}
if ((cpHandle & HANDLE_TYPE_IM_MASK) != 0) {
return TYPE.IM;
}
throw new IllegalArgumentException("Message type not found in handle string.");
}
/**
* TODO: Is this still needed after changing to another XML encoder? It should escape illegal
* characters.
* Strip away any illegal XML characters, that would otherwise cause the
* xml serializer to throw an exception.
* Examples of such characters are the emojis used on Android.
* @param text The string to validate
* @return the same string if valid, otherwise a new String stripped for
* any illegal characters. If a null pointer is passed an empty string will be returned.
*/
public static String stripInvalidChars(String text) {
if (text == null) {
return "";
}
char[] out = new char[text.length()];
int i, o, l;
for (i = 0, o = 0, l = text.length(); i < l; i++) {
char c = text.charAt(i);
if ((c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd)) {
out[o++] = c;
} // Else we skip the character
}
if (i == o) {
return text;
} else { // We removed some characters, create the new string
return new String(out, 0, o);
}
}
/**
* Truncate UTF-8 string encoded byte array to desired length
* @param utf8String String to convert to bytes array h
* @param maxLength Max length of byte array returned including null termination
* @return byte array containing valid utf8 characters with max length
* @throws UnsupportedEncodingException
*/
public static byte[] truncateUtf8StringToBytearray(String utf8String, int maxLength)
throws UnsupportedEncodingException {
byte[] utf8Bytes = new byte[utf8String.length() + 1];
try {
System.arraycopy(utf8String.getBytes("UTF-8"), 0, utf8Bytes, 0, utf8String.length());
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "truncateUtf8StringToBytearray: getBytes exception ", e);
throw e;
}
if (utf8Bytes.length > maxLength) {
/* if 'continuation' byte is in place 200,
* then strip previous bytes until utf-8 start byte is found */
if ((utf8Bytes[maxLength - 1] & 0xC0) == 0x80) {
for (int i = maxLength - 2; i >= 0; i--) {
if ((utf8Bytes[i] & 0xC0) == 0xC0) {
/* first byte in utf-8 character found,
* now copy i - 1 bytes to outBytes and add null termination */
utf8Bytes = Arrays.copyOf(utf8Bytes, i + 1);
utf8Bytes[i] = 0;
break;
}
}
} else {
/* copy bytes to outBytes and null terminate */
utf8Bytes = Arrays.copyOf(utf8Bytes, maxLength);
utf8Bytes[maxLength - 1] = 0;
}
}
return utf8Bytes;
}
private static final Pattern PATTERN = Pattern.compile("=\\?(.+?)\\?(.)\\?(.+?(?=\\?=))\\?=");
/**
* Method for converting quoted printable og base64 encoded string from headers.
* @param in the string with encoding
* @return decoded string if success - else the same string as was as input.
*/
public static String stripEncoding(String in) {
String str = null;
if (in.contains("=?") && in.contains("?=")) {
String encoding;
String charset;
String encodedText;
String match;
Matcher m = PATTERN.matcher(in);
while (m.find()) {
match = m.group(0);
charset = m.group(1);
encoding = m.group(2);
encodedText = m.group(3);
Log.v(TAG,
"Matching:" + match + "\nCharset: " + charset + "\nEncoding : " + encoding
+ "\nText: " + encodedText);
if (encoding.equalsIgnoreCase("Q")) {
//quoted printable
Log.d(TAG, "StripEncoding: Quoted Printable string : " + encodedText);
str = new String(quotedPrintableToUtf8(encodedText, charset));
in = in.replace(match, str);
} else if (encoding.equalsIgnoreCase("B")) {
// base64
try {
Log.d(TAG, "StripEncoding: base64 string : " + encodedText);
str = new String(
Base64.decode(encodedText.getBytes(charset), Base64.DEFAULT),
charset);
Log.d(TAG, "StripEncoding: decoded string : " + str);
in = in.replace(match, str);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "stripEncoding: Unsupported charset: " + charset);
} catch (IllegalArgumentException e) {
Log.e(TAG, "stripEncoding: string not encoded as base64: " + encodedText);
}
} else {
Log.e(TAG, "stripEncoding: Hit unknown encoding: " + encoding);
}
}
}
return in;
}
/**
* Convert a quoted-printable encoded string to a UTF-8 string:
* - Remove any soft line breaks: "=<CRLF>"
* - Convert all "=xx" to the corresponding byte
* @param text quoted-printable encoded UTF-8 text
* @return decoded UTF-8 string
*/
public static byte[] quotedPrintableToUtf8(String text, String charset) {
byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
byte[] input = null;
try {
input = text.getBytes("US-ASCII");
} catch (UnsupportedEncodingException e) {
/* This cannot happen as "US-ASCII" is supported for all Java implementations */
}
if (input == null) {
return "".getBytes();
}
int in, out, stopCnt = input.length - 2; // Leave room for peaking the next two bytes
/* Algorithm:
* - Search for token, copying all non token chars
* */
for (in = 0, out = 0; in < stopCnt; in++) {
byte b0 = input[in];
if (b0 == '=') {
byte b1 = input[++in];
byte b2 = input[++in];
if (b1 == '\r' && b2 == '\n') {
continue; // soft line break, remove all tree;
}
if (((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F') || (b1 >= 'a'
&& b1 <= 'f')) && ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F') || (
b2 >= 'a' && b2 <= 'f'))) {
if (V) {
Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
}
if (b1 <= '9') {
b1 = (byte) (b1 - '0');
} else if (b1 <= 'F') {
b1 = (byte) (b1 - 'A' + 10);
} else if (b1 <= 'f') {
b1 = (byte) (b1 - 'a' + 10);
}
if (b2 <= '9') {
b2 = (byte) (b2 - '0');
} else if (b2 <= 'F') {
b2 = (byte) (b2 - 'A' + 10);
} else if (b2 <= 'f') {
b2 = (byte) (b2 - 'a' + 10);
}
if (V) {
Log.v(TAG,
"Resulting nibble values: " + String.format("b1=%x b2=%x", b1, b2));
}
output[out++] = (byte) (b1 << 4 | b2); // valid hex char, append
if (V) {
Log.v(TAG, "Resulting value: " + String.format("0x%2x", output[out - 1]));
}
continue;
}
Log.w(TAG, "Received wrongly quoted printable encoded text. "
+ "Continuing at best effort...");
/* If we get a '=' without either a hex value or CRLF following, just add it and
* rewind the in counter. */
output[out++] = b0;
in -= 2;
continue;
} else {
output[out++] = b0;
continue;
}
}
// Just add any remaining characters. If they contain any encoding, it is invalid,
// and best effort would be just to display the characters.
while (in < input.length) {
output[out++] = input[in++];
}
String result = null;
// Figure out if we support the charset, else fall back to UTF-8, as this is what
// the MAP specification suggest to use, and is compatible with US-ASCII.
if (charset == null) {
charset = "UTF-8";
} else {
charset = charset.toUpperCase();
try {
if (!Charset.isSupported(charset)) {
charset = "UTF-8";
}
} catch (IllegalCharsetNameException e) {
Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
charset = "UTF-8";
}
}
try {
result = new String(output, 0, out, charset);
} catch (UnsupportedEncodingException e) {
/* This cannot happen unless Charset.isSupported() is out of sync with String */
try {
result = new String(output, 0, out, "UTF-8");
} catch (UnsupportedEncodingException e2) {
Log.e(TAG, "quotedPrintableToUtf8: " + e);
}
}
return result.getBytes(); /* return the result as "UTF-8" bytes */
}
/**
* Encodes an array of bytes into an array of quoted-printable 7-bit characters.
* Unsafe characters are escaped.
* Simplified version of encoder from QuetedPrintableCodec.java (Apache external)
*
* @param bytes
* array of bytes to be encoded
* @return UTF-8 string containing quoted-printable characters
*/
private static final byte ESCAPE_CHAR = '=';
private static final byte TAB = 9;
private static final byte SPACE = 32;
public static final String encodeQuotedPrintable(byte[] bytes) {
if (bytes == null) {
return null;
}
BitSet printable = new BitSet(256);
// alpha characters
for (int i = 33; i <= 60; i++) {
printable.set(i);
}
for (int i = 62; i <= 126; i++) {
printable.set(i);
}
printable.set(TAB);
printable.set(SPACE);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i];
if (b < 0) {
b = 256 + b;
}
if (printable.get(b)) {
buffer.write(b);
} else {
buffer.write(ESCAPE_CHAR);
char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
buffer.write(hex1);
buffer.write(hex2);
}
}
try {
return buffer.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
//cannot happen
return "";
}
}
}