summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Xie <mattx@google.com>2013-07-18 18:18:36 -0700
committerZhihai Xu <zhihaixu@google.com>2013-08-09 18:44:53 -0700
commitfd6603b8bf9ed72dcc8bd59aaef3209251b6e17c (patch)
treefecaf3c95adce97dc5176cc341d903fe488d5edf
parentbb1ac417208c8e283f9b5b49f4413856500ed0f9 (diff)
downloadandroid_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.tar.gz
android_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.tar.bz2
android_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.zip
Bluetooth MAP profile - sms and mms support initial check-in
bug:10116530 Change-Id: If9ce878d71c1e1b12416014c433da03b3033e158
-rw-r--r--Android.mk2
-rw-r--r--AndroidManifest.xml35
-rw-r--r--res/values/config.xml1
-rw-r--r--res/values/strings_map.xml16
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapActivity.java284
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapAppParams.java704
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapAuthenticator.java88
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapContent.java1536
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapContentObserver.java1168
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapFolderElement.java133
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapMessageListing.java92
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java256
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapObexServer.java695
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapReceiver.java64
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java72
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapService.java778
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapSmsPdu.java721
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapUtils.java116
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessage.java778
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java538
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessageSms.java82
-rw-r--r--src/com/android/bluetooth/map/BluetoothMnsObexClient.java346
-rw-r--r--src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java79
23 files changed, 8583 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk
index b0606df35..4c24df918 100644
--- a/Android.mk
+++ b/Android.mk
@@ -10,7 +10,7 @@ LOCAL_PACKAGE_NAME := Bluetooth
LOCAL_CERTIFICATE := platform
LOCAL_JNI_SHARED_LIBRARIES := libbluetooth_jni
-LOCAL_JAVA_LIBRARIES := javax.obex
+LOCAL_JAVA_LIBRARIES := javax.obex telephony-common mms-common
LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard
LOCAL_REQUIRED_MODULES := libbluetooth_jni bluetooth.default
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4a363dd57..017bd88c0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,6 +50,13 @@
<uses-permission android:name="android.permission.MANAGE_USERS"/>
<uses-permission android:name="com.google.android.gallery3d.permission.GALLERY_PROVIDER"/>
<uses-permission android:name="com.android.gallery3d.permission.GALLERY_PROVIDER"/>
+ <uses-permission android:name="android.permission.MMS_SEND_OUTBOX_MSG"/>
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <uses-permission android:name="android.permission.READ_SMS" />
+ <uses-permission android:name="android.permission.WRITE_SMS" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- For PBAP Owner Vcard Info -->
<uses-permission android:name="android.permission.READ_PROFILE"/>
@@ -220,6 +227,34 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
+ <activity android:name=".map.BluetoothMapActivity"
+ android:process="@string/process"
+ android:excludeFromRecents="true"
+ android:theme="@*android:style/Theme.Holo.Dialog.Alert"
+ android:enabled="@bool/profile_supported_map">
+ <intent-filter>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <service
+ android:process="@string/process"
+ android:name=".map.BluetoothMapService"
+ android:enabled="@bool/profile_supported_map" >
+ <intent-filter>
+ <action android:name="android.bluetooth.IBluetoothMap" />
+ </intent-filter>
+ </service>
+ <receiver
+ android:process="@string/process"
+ android:exported="true"
+ android:name=".map.BluetoothMapReceiver"
+ android:enabled="@bool/profile_supported_map">
+ <intent-filter>
+ <action android:name="android.bluetooth.adapter.action.STATE_CHANGED"/>
+ <action android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REPLY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </receiver>
<service
android:process="@string/process"
android:name = ".gatt.GattService"
diff --git a/res/values/config.xml b/res/values/config.xml
index af9092660..24eeb2c53 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -23,4 +23,5 @@
<bool name="profile_supported_gatt">true</bool>
<bool name="pbap_include_photos_in_vcard">false</bool>
<bool name="pbap_use_profile_for_owner_vcard">true</bool>
+ <bool name="profile_supported_map">true</bool>
</resources>
diff --git a/res/values/strings_map.xml b/res/values/strings_map.xml
new file mode 100644
index 000000000..ba30b9969
--- /dev/null
+++ b/res/values/strings_map.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="map_session_key_dialog_title">Type session key for %1$s</string>
+ <string name="map_session_key_dialog_header">Bluetooth session key required</string>
+ <string name="map_acceptance_timeout_message">There was time out to accept connection with %1$s</string>
+ <string name="map_authentication_timeout_message">There was time out to input session key with %1$s</string>
+ <string name="map_auth_notif_ticker">Obex authentication request</string>
+ <!-- Notification title when a Bluetooth device wants to pair with us -->
+ <string name="map_auth_notif_title">Session Key</string>
+ <!-- Notification message when a Bluetooth device wants to pair with us -->
+ <string name="map_auth_notif_message">Type session key for %1$s</string>
+ <string name="map_defaultname">Carkit</string>
+ <string name="map_unknownName">Unknown name</string>
+ <string name="map_localPhoneName">My name</string>
+ <string name="map_defaultnumber">000000</string>
+</resources>
diff --git a/src/com/android/bluetooth/map/BluetoothMapActivity.java b/src/com/android/bluetooth/map/BluetoothMapActivity.java
new file mode 100644
index 000000000..d415eef21
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapActivity.java
@@ -0,0 +1,284 @@
+
+/*
+* 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 com.android.bluetooth.R;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.Preference;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Button;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.text.InputFilter.LengthFilter;
+
+import com.android.internal.app.AlertActivity;
+import com.android.internal.app.AlertController;
+
+/**
+ * MapActivity shows two dialogues: One for accepting incoming map request and
+ * the other prompts the user to enter a session key for authentication with a
+ * remote Bluetooth device.
+ */
+public class BluetoothMapActivity extends AlertActivity implements
+ DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener, TextWatcher {
+ private static final String TAG = "BluetoothMapActivity";
+
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ private static final int BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH = 16;
+
+ private static final int DIALOG_YES_NO_AUTH = 1;
+
+ private static final String KEY_USER_TIMEOUT = "user_timeout";
+
+ private View mView;
+
+ private EditText mKeyView;
+
+ private TextView messageView;
+
+ private String mSessionKey = "";
+
+ private int mCurrentDialog;
+
+ private Button mOkButton;
+
+ private CheckBox mAlwaysAllowed;
+
+ private boolean mTimeout = false;
+
+ private boolean mAlwaysAllowedValue = true;
+
+ private static final int DISMISS_TIMEOUT_DIALOG = 0;
+
+ private static final int DISMISS_TIMEOUT_DIALOG_VALUE = 2000;
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothMapService.USER_CONFIRM_TIMEOUT_ACTION.equals(intent.getAction())) {
+ return;
+ }
+ onTimeout();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent i = getIntent();
+ String action = i.getAction();
+ if (action.equals(BluetoothMapService.AUTH_CHALL_ACTION)) {
+ showMapDialog(DIALOG_YES_NO_AUTH);
+ mCurrentDialog = DIALOG_YES_NO_AUTH;
+ } else {
+ Log.e(TAG, "Error: this activity may be started only with intent "
+ + "MAP_ACCESS_REQUEST or MAP_AUTH_CHALL ");
+ finish();
+ }
+ registerReceiver(mReceiver, new IntentFilter(
+ BluetoothMapService.USER_CONFIRM_TIMEOUT_ACTION));
+ }
+
+ private void showMapDialog(int id) {
+ final AlertController.AlertParams p = mAlertParams;
+ switch (id) {
+ case DIALOG_YES_NO_AUTH:
+ p.mIconId = android.R.drawable.ic_dialog_info;
+ p.mTitle = getString(R.string.map_session_key_dialog_header);
+ p.mView = createView(DIALOG_YES_NO_AUTH);
+ p.mPositiveButtonText = getString(android.R.string.ok);
+ p.mPositiveButtonListener = this;
+ p.mNegativeButtonText = getString(android.R.string.cancel);
+ p.mNegativeButtonListener = this;
+ setupAlert();
+ mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
+ mOkButton.setEnabled(false);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private String createDisplayText(final int id) {
+ String mRemoteName = BluetoothMapService.getRemoteDeviceName();
+ switch (id) {
+ case DIALOG_YES_NO_AUTH:
+ String mMessage2 = getString(R.string.map_session_key_dialog_title, mRemoteName);
+ return mMessage2;
+ default:
+ return null;
+ }
+ }
+
+ private View createView(final int id) {
+ switch (id) {
+ case DIALOG_YES_NO_AUTH:
+ mView = getLayoutInflater().inflate(R.layout.auth, null);
+ messageView = (TextView)mView.findViewById(R.id.message);
+ messageView.setText(createDisplayText(id));
+ mKeyView = (EditText)mView.findViewById(R.id.text);
+ mKeyView.addTextChangedListener(this);
+ mKeyView.setFilters(new InputFilter[] {
+ new LengthFilter(BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH)
+ });
+ return mView;
+ default:
+ return null;
+ }
+ }
+
+ private void onPositive() {
+ if (!mTimeout) {
+ if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
+ sendIntentToReceiver(BluetoothMapService.AUTH_RESPONSE_ACTION,
+ BluetoothMapService.EXTRA_SESSION_KEY, mSessionKey);
+ mKeyView.removeTextChangedListener(this);
+ }
+ }
+ mTimeout = false;
+ finish();
+ }
+
+ private void onNegative() {
+ if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
+ sendIntentToReceiver(BluetoothMapService.AUTH_CANCELLED_ACTION, null, null);
+ mKeyView.removeTextChangedListener(this);
+ }
+ finish();
+ }
+
+ private void sendIntentToReceiver(final String intentName, final String extraName,
+ final String extraValue) {
+ Intent intent = new Intent(intentName);
+ intent.setClassName(BluetoothMapService.THIS_PACKAGE_NAME, BluetoothMapReceiver.class
+ .getName());
+ if (extraName != null) {
+ intent.putExtra(extraName, extraValue);
+ }
+ sendBroadcast(intent);
+ }
+
+ private void sendIntentToReceiver(final String intentName, final String extraName,
+ final boolean extraValue) {
+ Intent intent = new Intent(intentName);
+ intent.setClassName(BluetoothMapService.THIS_PACKAGE_NAME, BluetoothMapReceiver.class
+ .getName());
+ if (extraName != null) {
+ intent.putExtra(extraName, extraValue);
+ }
+ sendBroadcast(intent);
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
+ mSessionKey = mKeyView.getText().toString();
+ }
+ onPositive();
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ onNegative();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void onTimeout() {
+ mTimeout = true;
+ if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
+ messageView.setText(getString(R.string.map_authentication_timeout_message,
+ BluetoothMapService.getRemoteDeviceName()));
+ mKeyView.setVisibility(View.GONE);
+ mKeyView.clearFocus();
+ mKeyView.removeTextChangedListener(this);
+ mOkButton.setEnabled(true);
+ mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE);
+ }
+
+ mTimeoutHandler.sendMessageDelayed(mTimeoutHandler.obtainMessage(DISMISS_TIMEOUT_DIALOG),
+ DISMISS_TIMEOUT_DIALOG_VALUE);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mTimeout = savedInstanceState.getBoolean(KEY_USER_TIMEOUT);
+ if (V) Log.v(TAG, "onRestoreInstanceState() mTimeout: " + mTimeout);
+ if (mTimeout) {
+ onTimeout();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_USER_TIMEOUT, mTimeout);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mReceiver);
+ }
+
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ return true;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int before, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ public void afterTextChanged(android.text.Editable s) {
+ if (s.length() > 0) {
+ mOkButton.setEnabled(true);
+ }
+ }
+
+ private final Handler mTimeoutHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case DISMISS_TIMEOUT_DIALOG:
+ if (V) Log.v(TAG, "Received DISMISS_TIMEOUT_DIALOG msg.");
+ finish();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapAppParams.java b/src/com/android/bluetooth/map/BluetoothMapAppParams.java
new file mode 100644
index 000000000..e55c61185
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapAppParams.java
@@ -0,0 +1,704 @@
+/*
+* 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 java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+
+import android.util.Log;
+
+/**
+ * This class encapsulates the appParams needed for MAP.
+ */
+public class BluetoothMapAppParams {
+
+ private static final String TAG = "BluetoothMapAppParams";
+
+ private static final int MAX_LIST_COUNT = 0x01;
+ private static final int MAX_LIST_COUNT_LEN = 0x02; //, 0x0000, 0xFFFF),
+ private static final int START_OFFSET = 0x02;
+ private static final int START_OFFSET_LEN = 0x02; //, 0x0000, 0xFFFF),
+ private static final int FILTER_MESSAGE_TYPE = 0x03;
+ private static final int FILTER_MESSAGE_TYPE_LEN = 0x01; //, 0x0000, 0x000f),
+ private static final int FILTER_PERIOD_BEGIN = 0x04;
+ private static final int FILTER_PERIOD_END = 0x05;
+ private static final int FILTER_READ_STATUS = 0x06;
+ private static final int FILTER_READ_STATUS_LEN = 0x01; //, 0x0000, 0x0002),
+ private static final int FILTER_RECIPIENT = 0x07;
+ private static final int FILTER_ORIGINATOR = 0x08;
+ private static final int FILTER_PRIORITY = 0x09;
+ private static final int FILTER_PRIORITY_LEN = 0x01; //, 0x0000, 0x0002),
+ private static final int ATTACHMENT = 0x0A;
+ private static final int ATTACHMENT_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int TRANSPARENT = 0x0B;
+ private static final int TRANSPARENT_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int RETRY = 0x0C;
+ private static final int RETRY_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int NEW_MESSAGE = 0x0D;
+ private static final int NEW_MESSAGE_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int NOTIFICATION_STATUS = 0x0E;
+ private static final int NOTIFICATION_STATUS_LEN = 0x02; //, 0x0000, 0xFFFF),
+ private static final int MAS_INSTANCE_ID = 0x0F;
+ private static final int MAS_INSTANCE_ID_LEN = 0x01; //, 0x0000, 0x00FF),
+ private static final int PARAMETER_MASK = 0x10;
+ private static final int PARAMETER_MASK_LEN = 0x04; //, 0x0000, 0x0000),
+ private static final int FOLDER_LISTING_SIZE = 0x11;
+ private static final int FOLDER_LISTING_SIZE_LEN = 0x02; //, 0x0000, 0xFFFF),
+ private static final int MESSAGE_LISTING_SIZE = 0x12;
+ private static final int MESSAGE_LISTING_SIZE_LEN = 0x02; //, 0x0000, 0xFFFF),
+ private static final int SUBJECT_LENGTH = 0x13;
+ private static final int SUBJECT_LENGTH_LEN = 0x01; //, 0x0000, 0x00FF),
+ private static final int CHARSET = 0x14;
+ private static final int CHARSET_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int FRACTION_REQUEST = 0x15;
+ private static final int FRACTION_REQUEST_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int FRACTION_DELIVER = 0x16;
+ private static final int FRACTION_DELIVER_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int STATUS_INDICATOR = 0x17;
+ private static final int STATUS_INDICATOR_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int STATUS_VALUE = 0x18;
+ private static final int STATUS_VALUE_LEN = 0x01; //, 0x0000, 0x0001),
+ private static final int MSE_TIME = 0x19;
+
+ public static final int INVALID_VALUE_PARAMETER = -1;
+ public static final int NOTIFICATION_STATUS_NO = 0;
+ public static final int NOTIFICATION_STATUS_YES = 1;
+ public static final int STATUS_INDICATOR_READ = 0;
+ public static final int STATUS_INDICATOR_DELETED = 1;
+ public static final int STATUS_VALUE_YES = 1;
+ public static final int STATUS_VALUE_NO = 0;
+ public static final int CHARSET_NATIVE = 0;
+ public static final int CHARSET_UTF8 = 1;
+
+ private int maxListCount = INVALID_VALUE_PARAMETER;
+ private int startOffset = INVALID_VALUE_PARAMETER;
+ private int filterMessageType = INVALID_VALUE_PARAMETER;
+ private long filterPeriodBegin = INVALID_VALUE_PARAMETER;
+ private long filterPeriodEnd = INVALID_VALUE_PARAMETER;
+ private int filterReadStatus = INVALID_VALUE_PARAMETER;
+ private String filterRecipient = null;
+ private String filterOriginator = null;
+ private int filterPriority = INVALID_VALUE_PARAMETER;
+ private int attachment = INVALID_VALUE_PARAMETER;
+ private int transparent = INVALID_VALUE_PARAMETER;
+ private int retry = INVALID_VALUE_PARAMETER;
+ private int newMessage = INVALID_VALUE_PARAMETER;
+ private int notificationStatus = INVALID_VALUE_PARAMETER;
+ private int masInstanceId = INVALID_VALUE_PARAMETER;
+ private long parameterMask = INVALID_VALUE_PARAMETER;
+ private int folderListingSize = INVALID_VALUE_PARAMETER;
+ private int messageListingSize = INVALID_VALUE_PARAMETER;
+ private int subjectLength = INVALID_VALUE_PARAMETER;
+ private int charset = INVALID_VALUE_PARAMETER;
+ private int fractionRequest = INVALID_VALUE_PARAMETER;
+ private int fractionDeliver = INVALID_VALUE_PARAMETER;
+ private int statusIndicator = INVALID_VALUE_PARAMETER;
+ private int statusValue = INVALID_VALUE_PARAMETER;
+ private long mseTime = INVALID_VALUE_PARAMETER;
+
+ /**
+ * Default constructor, used to build an application parameter object to be
+ * encoded. By default the member variables will be initialized to
+ * {@link INVALID_VALUE_PARAMETER} for values, and empty strings for String
+ * typed members.
+ */
+ public BluetoothMapAppParams() {
+ }
+
+ /**
+ * Creates an application parameter object based on a application parameter
+ * OBEX header. The content of the {@link appParam} byte array will be
+ * parsed, and its content will be stored in the member variables.
+ * {@link INVALID_VALUE_PARAMETER} can be used to determine if a value is
+ * set or not, where strings will be empty, if {@link appParam} did not
+ * contain the parameter.
+ *
+ * @param appParams
+ * the byte array containing the application parameters OBEX
+ * header
+ * @throws IllegalArgumentException
+ * when a parameter does not respect the valid ranges specified
+ * in the MAP spec.
+ * @throws ParseException
+ * if a parameter string if formated incorrectly.
+ */
+ public BluetoothMapAppParams(final byte[] appParams)
+ throws IllegalArgumentException, ParseException {
+ ParseParams(appParams);
+ }
+
+ /**
+ * Parse an application parameter OBEX header stored in a ByteArray.
+ *
+ * @param appParams
+ * the byte array containing the application parameters OBEX
+ * header
+ * @throws IllegalArgumentException
+ * when a parameter does not respect the valid ranges specified
+ * in the MAP spec.
+ * @throws ParseException
+ * if a parameter string if formated incorrectly.
+ */
+ private void ParseParams(final byte[] appParams) throws ParseException,
+ IllegalArgumentException {
+ int i = 0;
+ int tagId, tagLength;
+ ByteBuffer appParamBuf = ByteBuffer.wrap(appParams);
+ appParamBuf.order(ByteOrder.BIG_ENDIAN);
+ while (i < appParams.length) {
+ tagId = appParams[i++] & 0xff; // Convert to unsigned to support values above 127
+ tagLength = appParams[i++] & 0xff; // Convert to unsigned to support values above 127
+ switch (tagId) {
+ case MAX_LIST_COUNT:
+ if (tagLength != MAX_LIST_COUNT_LEN) {
+ Log.w(TAG, "MAX_LIST_COUNT: Wrong length received: " + tagLength
+ + " expected: " + MAX_LIST_COUNT_LEN);
+ break;
+ }
+ setMaxListCount(appParamBuf.getShort(i) & 0xffff); // Make it unsigned
+ break;
+ case START_OFFSET:
+ if (tagLength != START_OFFSET_LEN) {
+ Log.w(TAG, "START_OFFSET: Wrong length received: " + tagLength + " expected: "
+ + START_OFFSET_LEN);
+ break;
+ }
+ setStartOffset(appParamBuf.getShort(i) & 0xffff); // Make it unsigned
+ break;
+ case FILTER_MESSAGE_TYPE:
+ setFilterMessageType(appParams[i] & 0x0f);
+ break;
+ case FILTER_PERIOD_BEGIN:
+ setFilterPeriodBegin(new String(appParams, i, tagLength));
+ break;
+ case FILTER_PERIOD_END:
+ setFilterPeriodEnd(new String(appParams, i, tagLength));
+ break;
+ case FILTER_READ_STATUS:
+ setFilterReadStatus(appParams[i] & 0x03); // Lower two bits
+ break;
+ case FILTER_RECIPIENT:
+ setFilterRecipient(new String(appParams, i, tagLength));
+ break;
+ case FILTER_ORIGINATOR:
+ setFilterOriginator(new String(appParams, i, tagLength));
+ break;
+ case FILTER_PRIORITY:
+ setFilterPriority(appParams[i] & 0x03); // Lower two bits
+ break;
+ case ATTACHMENT:
+ setAttachment(appParams[i] & 0x01); // Lower bit
+ break;
+ case TRANSPARENT:
+ setTransparent(appParams[i] & 0x01); // Lower bit
+ break;
+ case RETRY:
+ setRetry(appParams[i] & 0x01); // Lower bit
+ break;
+ case NEW_MESSAGE:
+ setNewMessage(appParams[i] & 0x01); // Lower bit
+ break;
+ case NOTIFICATION_STATUS:
+ setNotificationStatus(appParams[i] & 0x01); // Lower bit
+ break;
+ case MAS_INSTANCE_ID:
+ setMasInstanceId(appParams[i] & 0xff);
+ break;
+ case PARAMETER_MASK:
+ setParameterMask(appParamBuf.getInt(i) & 0xffffffffL); // Make it unsigned
+ break;
+ case FOLDER_LISTING_SIZE:
+ setFolderListingSize(appParamBuf.getShort(i) & 0xffff); // Make it unsigned
+ break;
+ case MESSAGE_LISTING_SIZE:
+ setMessageListingSize(appParamBuf.getShort(i) & 0xffff); // Make it unsigned
+ break;
+ case SUBJECT_LENGTH:
+ setSubjectLength(appParams[i] & 0xff);
+ break;
+ case CHARSET:
+ setCharset(appParams[i] & 0x01); // Lower bit
+ break;
+ case FRACTION_REQUEST:
+ setFractionRequest(appParams[i] & 0x01); // Lower bit
+ break;
+ case FRACTION_DELIVER:
+ setFractionDeliver(appParams[i] & 0x01); // Lower bit
+ break;
+ case STATUS_INDICATOR:
+ setStatusIndicator(appParams[i] & 0x01); // Lower bit
+ break;
+ case STATUS_VALUE:
+ setStatusValue(appParams[i] & 0x01); // Lower bit
+ break;
+ case MSE_TIME:
+ setMseTime(new String(appParams, i, tagLength));
+ break;
+ default:
+ // Just skip unknown Tags, no need to report error
+ Log.w(TAG, "Unknown TagId received ( 0x" + Integer.toString(tagId, 16)
+ + "), skipping...");
+ break;
+ }
+ i += tagLength; // Offset to next TagId
+ }
+ }
+
+ /**
+ * Get the approximate length needed to store the appParameters in a byte
+ * array.
+ *
+ * @return the length in bytes
+ * @throws UnsupportedEncodingException
+ * if the platform does not support UTF-8 encoding.
+ */
+ private int getParamMaxLength() throws UnsupportedEncodingException {
+ int length = 0;
+ length += 25 * 2; // tagId + tagLength
+ length += 27; // fixed sizes
+ length += getFilterPeriodBegin() == INVALID_VALUE_PARAMETER ? 0 : 15;
+ length += getFilterPeriodEnd() == INVALID_VALUE_PARAMETER ? 0 : 15;
+ if (getFilterRecipient() != null)
+ length += getFilterRecipient().getBytes("UTF-8").length;
+ if (getFilterOriginator() != null)
+ length += getFilterOriginator().getBytes("UTF-8").length;
+ length += getMseTime() == INVALID_VALUE_PARAMETER ? 0 : 20;
+ return length;
+ }
+
+ /**
+ * Encode the application parameter object to a byte array.
+ *
+ * @return a byte Array representation of the application parameter object.
+ * @throws UnsupportedEncodingException
+ * if the platform does not support UTF-8 encoding.
+ */
+ public byte[] EncodeParams() throws UnsupportedEncodingException {
+ ByteBuffer appParamBuf = ByteBuffer.allocate(getParamMaxLength());
+ appParamBuf.order(ByteOrder.BIG_ENDIAN);
+ byte[] retBuf;
+
+ if (getMaxListCount() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) MAX_LIST_COUNT);
+ appParamBuf.put((byte) MAX_LIST_COUNT_LEN);
+ appParamBuf.putShort((short) getMaxListCount());
+ }
+ if (getStartOffset() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) START_OFFSET);
+ appParamBuf.put((byte) START_OFFSET_LEN);
+ appParamBuf.putShort((short) getStartOffset());
+ }
+ if (getFilterMessageType() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FILTER_MESSAGE_TYPE);
+ appParamBuf.put((byte) FILTER_MESSAGE_TYPE_LEN);
+ appParamBuf.put((byte) getFilterMessageType());
+ }
+ if (getFilterPeriodBegin() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FILTER_PERIOD_BEGIN);
+ appParamBuf.put((byte) getFilterPeriodBeginString().getBytes("UTF-8").length);
+ appParamBuf.put(getFilterPeriodBeginString().getBytes("UTF-8"));
+ }
+ if (getFilterPeriodEnd() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FILTER_PERIOD_END);
+ appParamBuf.put((byte) getFilterPeriodEndString().getBytes("UTF-8").length);
+ appParamBuf.put(getFilterPeriodEndString().getBytes("UTF-8"));
+ }
+ if (getFilterReadStatus() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FILTER_READ_STATUS);
+ appParamBuf.put((byte) FILTER_READ_STATUS_LEN);
+ appParamBuf.put((byte) getFilterReadStatus());
+ }
+ if (getFilterRecipient() != null) {
+ appParamBuf.put((byte) FILTER_RECIPIENT);
+ appParamBuf.put((byte) getFilterRecipient().getBytes("UTF-8").length);
+ appParamBuf.put(getFilterRecipient().getBytes("UTF-8"));
+ }
+ if (getFilterOriginator() != null) {
+ appParamBuf.put((byte) FILTER_ORIGINATOR);
+ appParamBuf.put((byte) getFilterOriginator().getBytes("UTF-8").length);
+ appParamBuf.put(getFilterOriginator().getBytes("UTF-8"));
+ }
+ if (getFilterPriority() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FILTER_PRIORITY);
+ appParamBuf.put((byte) FILTER_PRIORITY_LEN);
+ appParamBuf.put((byte) getFilterPriority());
+ }
+ if (getAttachment() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) ATTACHMENT);
+ appParamBuf.put((byte) ATTACHMENT_LEN);
+ appParamBuf.put((byte) getAttachment());
+ }
+ if (getTransparent() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) TRANSPARENT);
+ appParamBuf.put((byte) TRANSPARENT_LEN);
+ appParamBuf.put((byte) getTransparent());
+ }
+ if (getRetry() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) RETRY);
+ appParamBuf.put((byte) RETRY_LEN);
+ appParamBuf.put((byte) getRetry());
+ }
+ if (getNewMessage() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) NEW_MESSAGE);
+ appParamBuf.put((byte) NEW_MESSAGE_LEN);
+ appParamBuf.put((byte) getNewMessage());
+ }
+ if (getNotificationStatus() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) NOTIFICATION_STATUS);
+ appParamBuf.put((byte) NOTIFICATION_STATUS_LEN);
+ appParamBuf.putShort((short) getNotificationStatus());
+ }
+ if (getMasInstanceId() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) MAS_INSTANCE_ID);
+ appParamBuf.put((byte) MAS_INSTANCE_ID_LEN);
+ appParamBuf.put((byte) getMasInstanceId());
+ }
+ if (getParameterMask() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) PARAMETER_MASK);
+ appParamBuf.put((byte) PARAMETER_MASK_LEN);
+ appParamBuf.putInt((int) getParameterMask());
+ }
+ if (getFolderListingSize() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FOLDER_LISTING_SIZE);
+ appParamBuf.put((byte) FOLDER_LISTING_SIZE_LEN);
+ appParamBuf.putShort((short) getFolderListingSize());
+ }
+ if (getMessageListingSize() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) MESSAGE_LISTING_SIZE);
+ appParamBuf.put((byte) MESSAGE_LISTING_SIZE_LEN);
+ appParamBuf.putShort((short) getMessageListingSize());
+ }
+ if (getSubjectLength() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) SUBJECT_LENGTH);
+ appParamBuf.put((byte) SUBJECT_LENGTH_LEN);
+ appParamBuf.put((byte) getSubjectLength());
+ }
+ if (getCharset() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) CHARSET);
+ appParamBuf.put((byte) CHARSET_LEN);
+ appParamBuf.put((byte) getCharset());
+ }
+ if (getFractionRequest() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FRACTION_REQUEST);
+ appParamBuf.put((byte) FRACTION_REQUEST_LEN);
+ appParamBuf.put((byte) getFractionRequest());
+ }
+ if (getFractionDeliver() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) FRACTION_DELIVER);
+ appParamBuf.put((byte) FRACTION_DELIVER_LEN);
+ appParamBuf.put((byte) getFractionDeliver());
+ }
+ if (getStatusIndicator() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) STATUS_INDICATOR);
+ appParamBuf.put((byte) STATUS_INDICATOR_LEN);
+ appParamBuf.put((byte) getStatusIndicator());
+ }
+ if (getStatusValue() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) STATUS_VALUE);
+ appParamBuf.put((byte) STATUS_VALUE_LEN);
+ appParamBuf.put((byte) getStatusValue());
+ }
+ if (getMseTime() != INVALID_VALUE_PARAMETER) {
+ appParamBuf.put((byte) MSE_TIME);
+ appParamBuf.put((byte) getMseTimeString().getBytes("UTF-8").length);
+ appParamBuf.put(getMseTimeString().getBytes("UTF-8"));
+ }
+ // We need to reduce the length of the array to match the content
+ retBuf = Arrays.copyOfRange(appParamBuf.array(), appParamBuf.arrayOffset(),
+ appParamBuf.arrayOffset() + appParamBuf.position());
+ return retBuf;
+ }
+
+ public int getMaxListCount() {
+ return maxListCount;
+ }
+
+ public void setMaxListCount(int maxListCount) throws IllegalArgumentException {
+ if (maxListCount < 0 || maxListCount > 0xFFFF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
+ this.maxListCount = maxListCount;
+ }
+
+ public int getStartOffset() {
+ return startOffset;
+ }
+
+ public void setStartOffset(int startOffset) throws IllegalArgumentException {
+ if (startOffset < 0 || startOffset > 0xFFFF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
+ this.startOffset = startOffset;
+ }
+
+ public int getFilterMessageType() {
+ return filterMessageType;
+ }
+
+ public void setFilterMessageType(int filterMessageType) throws IllegalArgumentException {
+ if (filterMessageType < 0 || filterMessageType > 0x000F)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x000F");
+ this.filterMessageType = filterMessageType;
+ }
+
+ public long getFilterPeriodBegin() {
+ return filterPeriodBegin;
+ }
+
+ public String getFilterPeriodBeginString() {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = new Date(filterPeriodBegin);
+ return format.format(date); // Format to YYYYMMDDTHHMMSS local time
+ }
+
+ public void setFilterPeriodBegin(long filterPeriodBegin) {
+ this.filterPeriodBegin = filterPeriodBegin;
+ }
+
+ public void setFilterPeriodBegin(String filterPeriodBegin) throws ParseException {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = format.parse(filterPeriodBegin);
+ this.filterPeriodBegin = date.getTime();
+ }
+
+ public long getFilterPeriodEnd() {
+ return filterPeriodEnd;
+ }
+
+ public String getFilterPeriodEndString() {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = new Date(filterPeriodEnd);
+ return format.format(date); // Format to YYYYMMDDTHHMMSS local time
+ }
+
+ public void setFilterPeriodEnd(long filterPeriodEnd) {
+ this.filterPeriodEnd = filterPeriodEnd;
+ }
+
+ public void setFilterPeriodEnd(String filterPeriodEnd) throws ParseException {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = format.parse(filterPeriodEnd);
+ this.filterPeriodEnd = date.getTime();
+ }
+
+ public int getFilterReadStatus() {
+ return filterReadStatus;
+ }
+
+ public void setFilterReadStatus(int filterReadStatus) throws IllegalArgumentException {
+ if (filterReadStatus < 0 || filterReadStatus > 0x0002)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0002");
+ this.filterReadStatus = filterReadStatus;
+ }
+
+ public String getFilterRecipient() {
+ return filterRecipient;
+ }
+
+ public void setFilterRecipient(String filterRecipient) {
+ this.filterRecipient = filterRecipient;
+ }
+
+ public String getFilterOriginator() {
+ return filterOriginator;
+ }
+
+ public void setFilterOriginator(String filterOriginator) {
+ this.filterOriginator = filterOriginator;
+ }
+
+ public int getFilterPriority() {
+ return filterPriority;
+ }
+
+ public void setFilterPriority(int filterPriority) throws IllegalArgumentException {
+ if (filterPriority < 0 || filterPriority > 0x0002)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0002");
+ this.filterPriority = filterPriority;
+ }
+
+ public int getAttachment() {
+ return attachment;
+ }
+
+ public void setAttachment(int attachment) throws IllegalArgumentException {
+ if (attachment < 0 || attachment > 0x0001)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.attachment = attachment;
+ }
+
+ public int getTransparent() {
+ return transparent;
+ }
+
+ public void setTransparent(int transparent) throws IllegalArgumentException {
+ if (transparent < 0 || transparent > 0x0001)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.transparent = transparent;
+ }
+
+ public int getRetry() {
+ return retry;
+ }
+
+ public void setRetry(int retry) throws IllegalArgumentException {
+ if (retry < 0 || retry > 0x0001)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.retry = retry;
+ }
+
+ public int getNewMessage() {
+ return newMessage;
+ }
+
+ public void setNewMessage(int newMessage) throws IllegalArgumentException {
+ if (newMessage < 0 || newMessage > 0x0001)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.newMessage = newMessage;
+ }
+
+ public int getNotificationStatus() {
+ return notificationStatus;
+ }
+
+ public void setNotificationStatus(int notificationStatus) throws IllegalArgumentException {
+ if (notificationStatus < 0 || notificationStatus > 0x0001)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.notificationStatus = notificationStatus;
+ }
+
+ public int getMasInstanceId() {
+ return masInstanceId;
+ }
+
+ public void setMasInstanceId(int masInstanceId) {
+ if (masInstanceId < 0 || masInstanceId > 0x00FF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x00FF");
+ this.masInstanceId = masInstanceId;
+ }
+
+ public long getParameterMask() {
+ return parameterMask;
+ }
+
+ public void setParameterMask(long parameterMask) {
+ if (parameterMask < 0 || parameterMask > 0xFFFFFFFFL)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFFFFFF");
+ this.parameterMask = parameterMask;
+ }
+
+ public int getFolderListingSize() {
+ return folderListingSize;
+ }
+
+ public void setFolderListingSize(int folderListingSize) {
+ if (folderListingSize < 0 || folderListingSize > 0xFFFF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
+ this.folderListingSize = folderListingSize;
+ }
+
+ public int getMessageListingSize() {
+ return messageListingSize;
+ }
+
+ public void setMessageListingSize(int messageListingSize) {
+ if (messageListingSize < 0 || messageListingSize > 0xFFFF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
+ this.messageListingSize = messageListingSize;
+ }
+
+ public int getSubjectLength() {
+ return subjectLength;
+ }
+
+ public void setSubjectLength(int subjectLength) {
+ if (subjectLength < 0 || subjectLength > 0xFF)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x00FF");
+ this.subjectLength = subjectLength;
+ }
+
+ public int getCharset() {
+ return charset;
+ }
+
+ public void setCharset(int charset) {
+ if (charset < 0 || charset > 0x1)
+ throw new IllegalArgumentException("Out of range: " + charset + ", valid range is 0x0000 to 0x0001");
+ this.charset = charset;
+ }
+
+ public int getFractionRequest() {
+ return fractionRequest;
+ }
+
+ public void setFractionRequest(int fractionRequest) {
+ if (fractionRequest < 0 || fractionRequest > 0x1)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.fractionRequest = fractionRequest;
+ }
+
+ public int getFractionDeliver() {
+ return fractionDeliver;
+ }
+
+ public void setFractionDeliver(int fractionDeliver) {
+ if (fractionDeliver < 0 || fractionDeliver > 0x1)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.fractionDeliver = fractionDeliver;
+ }
+
+ public int getStatusIndicator() {
+ return statusIndicator;
+ }
+
+ public void setStatusIndicator(int statusIndicator) {
+ if (statusIndicator < 0 || statusIndicator > 0x1)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.statusIndicator = statusIndicator;
+ }
+
+ public int getStatusValue() {
+ return statusValue;
+ }
+
+ public void setStatusValue(int statusValue) {
+ if (statusValue < 0 || statusValue > 0x1)
+ throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
+ this.statusValue = statusValue;
+ }
+
+ public long getMseTime() {
+ return mseTime;
+ }
+
+ public String getMseTimeString() {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
+ Date date = new Date(getMseTime());
+ return format.format(date); // Format to YYYYMMDDTHHMMSS±hhmm UTC time ± offset
+ }
+
+ public void setMseTime(long mseTime) {
+ this.mseTime = mseTime;
+ }
+
+ public void setMseTime(String mseTime) throws ParseException {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
+ Date date = format.parse(mseTime);
+ this.mseTime = date.getTime();
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java b/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java
new file mode 100644
index 000000000..12f64e063
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java
@@ -0,0 +1,88 @@
+/*
+* 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.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import javax.obex.Authenticator;
+import javax.obex.PasswordAuthentication;
+
+/**
+ * BluetoothMapAuthenticator is a used by BluetoothObexServer for obex
+ * authentication procedure.
+ */
+public class BluetoothMapAuthenticator implements Authenticator {
+ private static final String TAG = "BluetoothMapAuthenticator";
+
+ private boolean mChallenged;
+
+ private boolean mAuthCancelled;
+
+ private String mSessionKey;
+
+ private Handler mCallback;
+
+ public BluetoothMapAuthenticator(final Handler callback) {
+ mCallback = callback;
+ mChallenged = false;
+ mAuthCancelled = false;
+ mSessionKey = null;
+ }
+
+ public final synchronized void setChallenged(final boolean bool) {
+ mChallenged = bool;
+ }
+
+ public final synchronized void setCancelled(final boolean bool) {
+ mAuthCancelled = bool;
+ }
+
+ public final synchronized void setSessionKey(final String string) {
+ mSessionKey = string;
+ }
+
+ private void waitUserConfirmation() {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_OBEX_AUTH_CHALL;
+ msg.sendToTarget();
+ synchronized (this) {
+ while (!mChallenged && !mAuthCancelled) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Interrupted while waiting on isChalled");
+ }
+ }
+ }
+ }
+
+ public PasswordAuthentication onAuthenticationChallenge(final String description,
+ final boolean isUserIdRequired, final boolean isFullAccess) {
+ waitUserConfirmation();
+ if (mSessionKey.trim().length() != 0) {
+ PasswordAuthentication pa = new PasswordAuthentication(null, mSessionKey.getBytes());
+ return pa;
+ }
+ return null;
+ }
+
+ // TODO: Reserved for future use only, in case MSE challenge MCE
+ public byte[] onAuthenticationResponse(final byte[] userName) {
+ byte[] b = null;
+ return b;
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapContent.java b/src/com/android/bluetooth/map/BluetoothMapContent.java
new file mode 100644
index 000000000..531e90b2d
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapContent.java
@@ -0,0 +1,1536 @@
+/*
+* 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 java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.ParseException;
+
+import org.apache.http.util.ByteArrayBuffer;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.Sms;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+import com.google.android.mms.pdu.CharacterSets;
+
+public class BluetoothMapContent {
+ private static final String TAG = "BluetoothMapContent";
+
+ private static final boolean D = true;
+ private static final boolean V = true;
+
+ private static final int MASK_SUBJECT = 0x1;
+ private static final int MASK_DATETIME = 0x2;
+ private static final int MASK_SENDER_NAME = 0x4;
+ private static final int MASK_SENDER_ADDRESSING = 0x8;
+
+ private static final int MASK_RECIPIENT_NAME = 0x10;
+ private static final int MASK_RECIPIENT_ADDRESSING = 0x20;
+ private static final int MASK_TYPE = 0x40;
+ private static final int MASK_SIZE = 0x80;
+
+ private static final int MASK_RECEPTION_STATUS = 0x100;
+ private static final int MASK_TEXT = 0x200;
+ private static final int MASK_ATTACHMENT_SIZE = 0x400;
+ private static final int MASK_PRIORITY = 0x800;
+
+ private static final int MASK_READ = 0x1000;
+ private static final int MASK_SENT = 0x2000;
+ private static final int MASK_PROTECTED = 0x4000;
+ private static final int MASK_REPLYTO_ADDRESSING = 0x8000;
+
+ /* Type of MMS address. From Telephony.java it must be one of PduHeaders.BCC, */
+ /* PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO. These are from PduHeaders.java */
+ public static final int MMS_FROM = 0x89;
+ public static final int MMS_TO = 0x97;
+ public static final int MMS_BCC = 0x81;
+ public static final int MMS_CC = 0x82;
+
+ private Context mContext;
+ private ContentResolver mResolver;
+
+ static final String[] SMS_PROJECTION = new String[] {
+ BaseColumns._ID,
+ Sms.THREAD_ID,
+ Sms.ADDRESS,
+ Sms.BODY,
+ Sms.DATE,
+ Sms.READ,
+ Sms.TYPE,
+ Sms.STATUS,
+ Sms.LOCKED,
+ Sms.ERROR_CODE,
+ };
+
+ static final String[] MMS_PROJECTION = new String[] {
+ BaseColumns._ID,
+ Mms.THREAD_ID,
+ Mms.MESSAGE_ID,
+ Mms.MESSAGE_SIZE,
+ Mms.SUBJECT,
+ Mms.CONTENT_TYPE,
+ Mms.TEXT_ONLY,
+ Mms.DATE,
+ Mms.DATE_SENT,
+ Mms.READ,
+ Mms.MESSAGE_BOX,
+ Mms.STATUS,
+ };
+
+ private class FilterInfo {
+ public static final int TYPE_SMS = 0;
+ public static final int TYPE_MMS = 1;
+
+ int msgType = TYPE_SMS;
+ int phoneType = 0;
+ String phoneNum = null;
+ String phoneAlphaTag = null;
+ }
+
+ public BluetoothMapContent(final Context context) {
+ mContext = context;
+ mResolver = mContext.getContentResolver();
+ if (mResolver == null) {
+ Log.d(TAG, "getContentResolver failed");
+ }
+ }
+
+ private void addSmsEntry() {
+ Log.d(TAG, "*** Adding dummy sms ***");
+
+ ContentValues mVal = new ContentValues();
+ mVal.put(Sms.ADDRESS, "1234");
+ mVal.put(Sms.BODY, "Hello!!!");
+ mVal.put(Sms.DATE, System.currentTimeMillis());
+ mVal.put(Sms.READ, "0");
+
+ Uri mUri = mResolver.insert(Sms.CONTENT_URI, mVal);
+ }
+
+ private BluetoothMapAppParams buildAppParams() {
+ BluetoothMapAppParams ap = new BluetoothMapAppParams();
+ try {
+ int paramMask = (MASK_SUBJECT
+ | MASK_DATETIME
+ | MASK_SENDER_NAME
+ | MASK_SENDER_ADDRESSING
+ | MASK_RECIPIENT_NAME
+ | MASK_RECIPIENT_ADDRESSING
+ | MASK_TYPE
+ | MASK_SIZE
+ | MASK_RECEPTION_STATUS
+ | MASK_TEXT
+ | MASK_ATTACHMENT_SIZE
+ | MASK_PRIORITY
+ | MASK_READ
+ | MASK_SENT
+ | MASK_PROTECTED
+ );
+ ap.setMaxListCount(5);
+ ap.setStartOffset(0);
+ ap.setFilterMessageType(0);
+ ap.setFilterPeriodBegin("20130101T000000");
+ ap.setFilterPeriodEnd("20131230T000000");
+ ap.setFilterReadStatus(0);
+ ap.setParameterMask(paramMask);
+ ap.setSubjectLength(10);
+ /* ap.setFilterOriginator("Sms*"); */
+ /* ap.setFilterRecipient("41*"); */
+ } catch (ParseException e) {
+ return null;
+ }
+ return ap;
+ }
+
+ private void printSms(Cursor c) {
+ String body = c.getString(c.getColumnIndex(Sms.BODY));
+ if (D) Log.d(TAG, "printSms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
+ " " + Sms.THREAD_ID + " : " + c.getLong(c.getColumnIndex(Sms.THREAD_ID)) +
+ " " + Sms.ADDRESS + " : " + c.getString(c.getColumnIndex(Sms.ADDRESS)) +
+ " " + Sms.BODY + " : " + body.substring(0, Math.min(body.length(), 8)) +
+ " " + Sms.DATE + " : " + c.getLong(c.getColumnIndex(Sms.DATE)) +
+ " " + Sms.TYPE + " : " + c.getInt(c.getColumnIndex(Sms.TYPE)));
+ }
+
+ private void printMms(Cursor c) {
+ if (D) Log.d(TAG, "printMms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
+ "\n " + Mms.THREAD_ID + " : " + c.getLong(c.getColumnIndex(Mms.THREAD_ID)) +
+ "\n " + Mms.MESSAGE_ID + " : " + c.getString(c.getColumnIndex(Mms.MESSAGE_ID)) +
+ "\n " + Mms.SUBJECT + " : " + c.getString(c.getColumnIndex(Mms.SUBJECT)) +
+ "\n " + Mms.CONTENT_TYPE + " : " + c.getString(c.getColumnIndex(Mms.CONTENT_TYPE)) +
+ "\n " + Mms.TEXT_ONLY + " : " + c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)) +
+ "\n " + Mms.DATE + " : " + c.getLong(c.getColumnIndex(Mms.DATE)) +
+ "\n " + Mms.DATE_SENT + " : " + c.getLong(c.getColumnIndex(Mms.DATE_SENT)) +
+ "\n " + Mms.READ + " : " + c.getInt(c.getColumnIndex(Mms.READ)) +
+ "\n " + Mms.MESSAGE_BOX + " : " + c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)) +
+ "\n " + Mms.STATUS + " : " + c.getInt(c.getColumnIndex(Mms.STATUS)));
+ }
+
+ private void printMmsAddr(long id) {
+ final String[] projection = null;
+ String selection = new String("msg_id=" + id);
+ String uriStr = String.format("content://mms/%d/addr", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ Cursor c = mResolver.query(
+ uriAddress,
+ projection,
+ selection,
+ null, null);
+
+ if (c.moveToFirst()) {
+ do {
+ String add = c.getString(c.getColumnIndex("address"));
+ Integer type = c.getInt(c.getColumnIndex("type"));
+ if (type == MMS_TO) {
+ Log.d(TAG, " recipient: " + add + " (type: " + type + ")");
+ } else if (type == MMS_FROM) {
+ Log.d(TAG, " originator: " + add + " (type: " + type + ")");
+ } else {
+ Log.d(TAG, " address other: " + add + " (type: " + type + ")");
+ }
+
+ } while(c.moveToNext());
+ }
+ }
+
+ private void printMmsPartImage(long partid) {
+ String uriStr = String.format("content://mms/part/%d", partid);
+ Uri uriAddress = Uri.parse(uriStr);
+ int ch;
+ StringBuffer sb = new StringBuffer("");
+ InputStream is = null;
+
+ try {
+ is = mResolver.openInputStream(uriAddress);
+
+ while ((ch = is.read()) != -1) {
+ sb.append((char)ch);
+ }
+ Log.d(TAG, sb.toString());
+
+ } catch (IOException e) {
+ // do nothing for now
+ e.printStackTrace();
+ }
+ }
+
+ private void printMmsParts(long id) {
+ final String[] projection = null;
+ String selection = new String("mid=" + id);
+ String uriStr = String.format("content://mms/%d/part", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ Cursor c = mResolver.query(
+ uriAddress,
+ projection,
+ selection,
+ null, null);
+
+ Log.d(TAG, " parts:");
+ if (c.moveToFirst()) {
+ do {
+ Long partid = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String ct = c.getString(c.getColumnIndex("ct"));
+ String name = c.getString(c.getColumnIndex("name"));
+ String charset = c.getString(c.getColumnIndex("chset"));
+ String filename = c.getString(c.getColumnIndex("fn"));
+ String text = c.getString(c.getColumnIndex("text"));
+ Integer fd = c.getInt(c.getColumnIndex("_data"));
+
+ Log.d(TAG, " _id : " + partid +
+ "\n ct : " + ct +
+ "\n partname : " + name +
+ "\n charset : " + charset +
+ "\n filename : " + filename +
+ "\n text : " + text +
+ "\n fd : " + fd);
+
+ /* if (ct.equals("image/jpeg")) { */
+ /* printMmsPartImage(partid); */
+ /* } */
+ } while(c.moveToNext());
+ }
+ }
+
+ public void dumpMmsTable() {
+ Log.d(TAG, "**** Dump of mms table ****");
+ Cursor c = mResolver.query(Mms.CONTENT_URI,
+ MMS_PROJECTION, null, null, "_id DESC");
+ if (c != null) {
+ Log.d(TAG, "c.getCount() = " + c.getCount());
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ printMms(c);
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ printMmsAddr(id);
+ printMmsParts(id);
+ }
+ } else {
+ Log.d(TAG, "query failed");
+ c.close();
+ }
+ }
+
+ public void dumpSmsTable() {
+ addSmsEntry();
+ Log.d(TAG, "**** Dump of sms table ****");
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, null, null, "_id DESC");
+ if (c != null) {
+ Log.d(TAG, "c.getCount() = " + c.getCount());
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ printSms(c);
+ }
+ } else {
+ Log.d(TAG, "query failed");
+ c.close();
+ }
+
+ }
+
+ public void dumpMessages() {
+ dumpSmsTable();
+ dumpMmsTable();
+
+ BluetoothMapAppParams ap = buildAppParams();
+ Log.d(TAG, "message listing size = " + msgListingSize("inbox", ap));
+ BluetoothMapMessageListing mList = msgListing("inbox", ap);
+ try {
+ mList.encode();
+ } catch (UnsupportedEncodingException ex) {
+ /* do nothing */
+ }
+ mList = msgListing("sent", ap);
+ try {
+ mList.encode();
+ } catch (UnsupportedEncodingException ex) {
+ /* do nothing */
+ }
+ }
+
+ private void setProtected(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_PROTECTED) != 0) {
+ String protect = "no";
+ Log.d(TAG, "setProtected: " + protect);
+ e.setProtect(protect);
+ }
+ }
+
+ private void setSent(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_SENT) != 0) {
+ int msgType = 0;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ msgType = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ }
+ String sent = null;
+ if (msgType == 2) {
+ sent = "yes";
+ } else {
+ sent = "no";
+ }
+ Log.d(TAG, "setSent: " + sent);
+ e.setSent(sent);
+ }
+ }
+
+ private void setRead(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_READ) != 0) {
+ int read = 0;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ read = c.getInt(c.getColumnIndex(Sms.READ));
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ read = c.getInt(c.getColumnIndex(Mms.READ));
+ }
+ String setread = null;
+ if (read == 1) {
+ setread = "yes";
+ } else {
+ setread = "no";
+ }
+ Log.d(TAG, "setRead: " + setread);
+ e.setRead(setread);
+ }
+ }
+
+ private void setPriority(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_PRIORITY) != 0) {
+ String priority = "no";
+ Log.d(TAG, "setPriority: " + priority);
+ e.setPriority(priority);
+ }
+ }
+
+ private void setAttachmentSize(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_ATTACHMENT_SIZE) != 0) {
+ int size = 0;
+ Log.d(TAG, "setAttachmentSize: " + size);
+ e.setAttachmentSize(size);
+ }
+ }
+
+ private void setText(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_TEXT) != 0) {
+ String hasText = "";
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ hasText = "yes";
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ int textOnly = c.getInt(c.getColumnIndex(Mms.TEXT_ONLY));
+ if (textOnly == 1) {
+ hasText = "yes";
+ } else {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String text = getTextPartsMms(id);
+ if (text != null && text.length() > 0) {
+ hasText = "yes";
+ } else {
+ hasText = "no";
+ }
+ }
+ }
+ Log.d(TAG, "setText: " + hasText);
+ e.setText(hasText);
+ }
+ }
+
+ private void setReceptionStatus(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_RECEPTION_STATUS) != 0) {
+ String status = "complete";
+ Log.d(TAG, "setReceptionStatus: " + status);
+ e.setReceptionStatus(status);
+ }
+ }
+
+ private void setSize(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_SIZE) != 0) {
+ int size = 0;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ String subject = c.getString(c.getColumnIndex(Sms.BODY));
+ size = subject.length();
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ size = c.getInt(c.getColumnIndex(Mms.MESSAGE_SIZE));
+ }
+ Log.d(TAG, "setSize: " + size);
+ e.setSize(size);
+ }
+ }
+
+ private void setType(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_TYPE) != 0) {
+ TYPE type = null;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ if (fi.phoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ type = TYPE.SMS_GSM;
+ } else if (fi.phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ type = TYPE.SMS_CDMA;
+ }
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ type = TYPE.MMS;
+ }
+ Log.d(TAG, "setType: " + type);
+ e.setType(type);
+ }
+ }
+
+ private void setRecipientAddressing(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_RECIPIENT_ADDRESSING) != 0) {
+ String address = null;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType == 1) {
+ address = fi.phoneNum;
+ } else {
+ address = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ }
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ address = getAddressMms(mResolver, id, MMS_TO);
+ }
+ Log.d(TAG, "setRecipientAddressing: " + address);
+ e.setRecipientAddressing(address);
+ }
+ }
+
+ private void setRecipientName(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_RECIPIENT_NAME) != 0) {
+ String name = null;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType != 1) {
+ String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ name = getContactNameFromPhone(phone);
+ } else {
+ name = fi.phoneAlphaTag;
+ }
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String phone = getAddressMms(mResolver, id, MMS_TO);
+ name = getContactNameFromPhone(phone);
+ }
+ Log.d(TAG, "setRecipientName: " + name);
+ e.setRecipientName(name);
+ }
+ }
+
+ private void setSenderAddressing(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_SENDER_ADDRESSING) != 0) {
+ String address = null;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType == 1) {
+ address = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ } else {
+ address = fi.phoneNum;
+ }
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ address = getAddressMms(mResolver, id, MMS_FROM);
+ }
+ Log.d(TAG, "setSenderAddressing: " + address);
+ e.setSenderAddressing(address);
+ }
+ }
+
+ private void setSenderName(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_SENDER_NAME) != 0) {
+ String name = null;
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType == 1) {
+ String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ name = getContactNameFromPhone(phone);
+ } else {
+ name = fi.phoneAlphaTag;
+ }
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String phone = getAddressMms(mResolver, id, MMS_FROM);
+ name = getContactNameFromPhone(phone);
+ }
+ Log.d(TAG, "setSenderName: " + name);
+ e.setSenderName(name);
+ }
+ }
+
+ private void setDateTime(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ long date = 0;
+
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ date = c.getLong(c.getColumnIndex(Sms.DATE));
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ /* Use Mms.DATE for all messages. Although contract class states */
+ /* Mms.DATE_SENT are for outgoing messages. But that is not working. */
+ date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L;
+
+ /* int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); */
+ /* if (msgBox == Mms.MESSAGE_BOX_INBOX) { */
+ /* date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L; */
+ /* } else { */
+ /* date = c.getLong(c.getColumnIndex(Mms.DATE_SENT)) * 1000L; */
+ /* } */
+ }
+ e.setDateTime(date);
+ }
+
+ private String getTextPartsMms(long id) {
+ String text = "";
+ String selection = new String("mid=" + id);
+ String uriStr = String.format("content://mms/%d/part", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ Cursor c = mResolver.query(uriAddress, null, selection,
+ null, null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ String ct = c.getString(c.getColumnIndex("ct"));
+ if (ct.equals("text/plain")) {
+ text += c.getString(c.getColumnIndex("text"));
+ }
+ } while(c.moveToNext());
+ }
+ if (c != null) {
+ c.close();
+ }
+ return text;
+ }
+
+ private void setSubject(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ String subject = "";
+ int subLength = ap.getSubjectLength();
+ if(subLength == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
+ subLength = 256;
+
+ if ((ap.getParameterMask() & MASK_SUBJECT) != 0) {
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ subject = c.getString(c.getColumnIndex(Sms.BODY));
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ subject = c.getString(c.getColumnIndex(Mms.SUBJECT));
+ if (subject == null || subject.length() == 0) {
+ /* Get subject from mms text body parts - if any exists */
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ subject = getTextPartsMms(id);
+ }
+ }
+ if (subject != null) {
+ subject = subject.substring(0, Math.min(subject.length(),
+ subLength));
+ }
+ Log.d(TAG, "setSubject: " + subject);
+ e.setSubject(subject);
+ }
+ }
+
+ private void setHandle(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ long handle = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ Log.d(TAG, "setHandle: " + handle);
+ e.setHandle(handle);
+ }
+
+ private BluetoothMapMessageListingElement element(Cursor c, FilterInfo fi,
+ BluetoothMapAppParams ap) {
+ BluetoothMapMessageListingElement e = new BluetoothMapMessageListingElement();
+
+ setHandle(e, c, fi, ap);
+ setSubject(e, c, fi, ap);
+ setDateTime(e, c, fi, ap);
+ setSenderName(e, c, fi, ap);
+ setSenderAddressing(e, c, fi, ap);
+ setRecipientName(e, c, fi, ap);
+ setRecipientAddressing(e, c, fi, ap);
+ setType(e, c, fi, ap);
+ setSize(e, c, fi, ap);
+ setReceptionStatus(e, c, fi, ap);
+ setText(e, c, fi, ap);
+ setAttachmentSize(e, c, fi, ap);
+ setPriority(e, c, fi, ap);
+ setRead(e, c, fi, ap);
+ setSent(e, c, fi, ap);
+ setProtected(e, c, fi, ap);
+ return e;
+ }
+
+ private String getContactNameFromPhone(String phone) {
+ String name = null;
+
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(phone));
+
+ String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME};
+ String selection = Contacts.IN_VISIBLE_GROUP + "=1";
+ String orderBy = Contacts.DISPLAY_NAME + " ASC";
+
+ Cursor c = mResolver.query(uri, projection, selection, null, orderBy);
+
+ if (c != null && c.getCount() >= 1) {
+ c.moveToFirst();
+ name = c.getString(c.getColumnIndex(Contacts.DISPLAY_NAME));
+ }
+
+ c.close();
+ return name;
+ }
+
+ static public String getAddressMms(ContentResolver r, long id, int type) {
+ String selection = new String("msg_id=" + id + " AND type=" + type);
+ String uriStr = String.format("content://mms/%d/addr", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ String addr = null;
+ Cursor c = r.query(uriAddress, null, selection, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ addr = c.getString(c.getColumnIndex("address"));
+ }
+
+ if (c != null) {
+ c.close();
+ }
+ return addr;
+ }
+
+ private boolean matchRecipientMms(Cursor c, FilterInfo fi, String recip) {
+ boolean res;
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String phone = getAddressMms(mResolver, id, MMS_TO);
+ if (phone != null && phone.length() > 0) {
+ if (phone.matches(recip)) {
+ Log.d(TAG, "match recipient phone = " + phone);
+ res = true;
+ } else {
+ String name = getContactNameFromPhone(phone);
+ if (name != null && name.length() > 0 && name.matches(recip)) {
+ Log.d(TAG, "match recipient name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ } else {
+ res = false;
+ }
+ return res;
+ }
+
+ private boolean matchRecipientSms(Cursor c, FilterInfo fi, String recip) {
+ boolean res;
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType == 1) {
+ String phone = fi.phoneNum;
+ String name = fi.phoneAlphaTag;
+ if (phone != null && phone.length() > 0 && phone.matches(recip)) {
+ Log.d(TAG, "match recipient phone = " + phone);
+ res = true;
+ } else if (name != null && name.length() > 0 && name.matches(recip)) {
+ Log.d(TAG, "match recipient name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ else {
+ String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ if (phone != null && phone.length() > 0) {
+ if (phone.matches(recip)) {
+ Log.d(TAG, "match recipient phone = " + phone);
+ res = true;
+ } else {
+ String name = getContactNameFromPhone(phone);
+ if (name != null && name.length() > 0 && name.matches(recip)) {
+ Log.d(TAG, "match recipient name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ } else {
+ res = false;
+ }
+ }
+ return res;
+ }
+
+ private boolean matchRecipient(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
+ boolean res;
+ String recip = ap.getFilterRecipient();
+ if (recip != null && recip.length() > 0) {
+ recip = recip.replace("*", ".*");
+ recip = ".*" + recip + ".*";
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ res = matchRecipientSms(c, fi, recip);
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ res = matchRecipientMms(c, fi, recip);
+ } else {
+ Log.d(TAG, "Unknown msg type: " + fi.msgType);
+ res = false;
+ }
+ } else {
+ res = true;
+ }
+ return res;
+ }
+
+ private boolean matchOriginatorMms(Cursor c, FilterInfo fi, String orig) {
+ boolean res;
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String phone = getAddressMms(mResolver, id, MMS_FROM);
+ if (phone != null && phone.length() > 0) {
+ if (phone.matches(orig)) {
+ Log.d(TAG, "match originator phone = " + phone);
+ res = true;
+ } else {
+ String name = getContactNameFromPhone(phone);
+ if (name != null && name.length() > 0 && name.matches(orig)) {
+ Log.d(TAG, "match originator name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ } else {
+ res = false;
+ }
+ return res;
+ }
+
+ private boolean matchOriginatorSms(Cursor c, FilterInfo fi, String orig) {
+ boolean res;
+ int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ if (msgType == 1) {
+ String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ if (phone !=null && phone.length() > 0) {
+ if (phone.matches(orig)) {
+ Log.d(TAG, "match originator phone = " + phone);
+ res = true;
+ } else {
+ String name = getContactNameFromPhone(phone);
+ if (name != null && name.length() > 0 && name.matches(orig)) {
+ Log.d(TAG, "match originator name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ } else {
+ res = false;
+ }
+ }
+ else {
+ String phone = fi.phoneNum;
+ String name = fi.phoneAlphaTag;
+ if (phone != null && phone.length() > 0 && phone.matches(orig)) {
+ Log.d(TAG, "match originator phone = " + phone);
+ res = true;
+ } else if (name != null && name.length() > 0 && name.matches(orig)) {
+ Log.d(TAG, "match originator name = " + name);
+ res = true;
+ } else {
+ res = false;
+ }
+ }
+ return res;
+ }
+
+ private boolean matchOriginator(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
+ boolean res;
+ String orig = ap.getFilterOriginator();
+ if (orig != null && orig.length() > 0) {
+ orig = orig.replace("*", ".*");
+ orig = ".*" + orig + ".*";
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ res = matchOriginatorSms(c, fi, orig);
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ res = matchOriginatorMms(c, fi, orig);
+ } else {
+ Log.d(TAG, "Unknown msg type: " + fi.msgType);
+ res = false;
+ }
+ } else {
+ res = true;
+ }
+ return res;
+ }
+
+ private boolean matchAddresses(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) {
+ if (matchOriginator(c, fi, ap) && matchRecipient(c, fi, ap)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private String setWhereFilterFolderTypeSms(String folder) {
+ String where = "";
+ if ("inbox".equalsIgnoreCase(folder)) {
+ where = "type = 1 AND thread_id <> -1";
+ }
+ else if ("outbox".equalsIgnoreCase(folder)) {
+ where = "(type = 4 OR type = 5 OR type = 6) AND thread_id <> -1";
+ }
+ else if ("sent".equalsIgnoreCase(folder)) {
+ where = "type = 2 AND thread_id <> -1";
+ }
+ else if ("draft".equalsIgnoreCase(folder)) {
+ where = "type = 3 AND thread_id <> -1";
+ }
+ else if ("deleted".equalsIgnoreCase(folder)) {
+ where = "thread_id = -1";
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterFolderTypeMms(String folder) {
+ String where = "";
+ if ("inbox".equalsIgnoreCase(folder)) {
+ where = "msg_box = 1 AND thread_id <> -1";
+ }
+ else if ("outbox".equalsIgnoreCase(folder)) {
+ where = "msg_box = 4 AND thread_id <> -1";
+ }
+ else if ("sent".equalsIgnoreCase(folder)) {
+ where = "msg_box = 2 AND thread_id <> -1";
+ }
+ else if ("draft".equalsIgnoreCase(folder)) {
+ where = "msg_box = 3 AND thread_id <> -1";
+ }
+ else if ("deleted".equalsIgnoreCase(folder)) {
+ where = "thread_id = -1";
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterFolderType(String folder, FilterInfo fi) {
+ String where = "";
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ where = setWhereFilterFolderTypeSms(folder);
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ where = setWhereFilterFolderTypeMms(folder);
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterReadStatus(BluetoothMapAppParams ap) {
+ String where = "";
+ if (ap.getFilterReadStatus() != -1) {
+ if ((ap.getFilterReadStatus() & 0x01) != 0) {
+ where = " AND read=0 ";
+ }
+
+ if ((ap.getFilterReadStatus() & 0x02) != 0) {
+ where = " AND read=1 ";
+ }
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterPeriod(BluetoothMapAppParams ap, FilterInfo fi) {
+ String where = "";
+ if ((ap.getFilterPeriodBegin() != -1)) {
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ where = " AND date >= " + ap.getFilterPeriodBegin();
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ where = " AND date >= " + (ap.getFilterPeriodBegin() / 1000L);
+ }
+ }
+
+ if ((ap.getFilterPeriodEnd() != -1)) {
+ if (fi.msgType == FilterInfo.TYPE_SMS) {
+ where += " AND date < " + ap.getFilterPeriodEnd();
+ } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ where += " AND date < " + (ap.getFilterPeriodEnd() / 1000L);
+ }
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterPhones(String str) {
+ String where = "";
+ str = str.replace("*", "%");
+
+ Cursor c = mResolver.query(ContactsContract.Contacts.CONTENT_URI, null,
+ ContactsContract.Contacts.DISPLAY_NAME + " like ?",
+ new String[]{str},
+ ContactsContract.Contacts.DISPLAY_NAME + " ASC");
+
+ while (c != null && c.moveToNext()) {
+ String contactId = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID));
+
+ Cursor p = mResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
+ new String[]{contactId},
+ null);
+
+ while (p != null && p.moveToNext()) {
+ String number = p.getString(
+ p.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+
+ where += " address = " + "'" + number + "'";
+ if (!p.isLast()) {
+ where += " OR ";
+ }
+ }
+ if (!c.isLast()) {
+ where += " OR ";
+ }
+ p.close();
+ }
+ c.close();
+
+ if (str != null && str.length() > 0) {
+ if (where.length() > 0) {
+ where += " OR ";
+ }
+ where += " address like " + "'" + str + "'";
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterOriginator(BluetoothMapAppParams ap,
+ FilterInfo fi) {
+ String where = "";
+ String orig = ap.getFilterOriginator();
+
+ if (orig != null && orig.length() > 0) {
+ String phones = setWhereFilterPhones(orig);
+
+ if (phones.length() > 0) {
+ where = " AND ((type <> 1) OR ( " + phones + " ))";
+ } else {
+ where = " AND (type <> 1)";
+ }
+
+ orig = orig.replace("*", ".*");
+ orig = ".*" + orig + ".*";
+
+ boolean localPhoneMatchOrig = false;
+ if (fi.phoneNum != null && fi.phoneNum.length() > 0
+ && fi.phoneNum.matches(orig)) {
+ localPhoneMatchOrig = true;
+ }
+
+ if (fi.phoneAlphaTag != null && fi.phoneAlphaTag.length() > 0
+ && fi.phoneAlphaTag.matches(orig)) {
+ localPhoneMatchOrig = true;
+ }
+
+ if (!localPhoneMatchOrig) {
+ where += " AND (type = 1)";
+ }
+ }
+
+ return where;
+ }
+
+ private String setWhereFilterRecipient(BluetoothMapAppParams ap,
+ FilterInfo fi) {
+ String where = "";
+ String recip = ap.getFilterRecipient();
+
+ if (recip != null && recip.length() > 0) {
+ String phones = setWhereFilterPhones(recip);
+
+ if (phones.length() > 0) {
+ where = " AND ((type = 1) OR ( " + phones + " ))";
+ } else {
+ where = " AND (type = 1)";
+ }
+
+ recip = recip.replace("*", ".*");
+ recip = ".*" + recip + ".*";
+
+ boolean localPhoneMatchOrig = false;
+ if (fi.phoneNum != null && fi.phoneNum.length() > 0
+ && fi.phoneNum.matches(recip)) {
+ localPhoneMatchOrig = true;
+ }
+
+ if (fi.phoneAlphaTag != null && fi.phoneAlphaTag.length() > 0
+ && fi.phoneAlphaTag.matches(recip)) {
+ localPhoneMatchOrig = true;
+ }
+
+ if (!localPhoneMatchOrig) {
+ where += " AND (type <> 1)";
+ }
+ }
+
+ return where;
+ }
+
+ private String setWhereFilter(String folder, FilterInfo fi, BluetoothMapAppParams ap) {
+ String where = "";
+
+ where += setWhereFilterFolderType(folder, fi);
+ where += setWhereFilterReadStatus(ap);
+ where += setWhereFilterPeriod(ap, fi);
+ /* where += setWhereFilterOriginator(ap, fi); */
+ /* where += setWhereFilterRecipient(ap, fi); */
+
+ Log.d(TAG, "where: " + where);
+
+ return where;
+ }
+
+ private boolean smsSelected(FilterInfo fi, BluetoothMapAppParams ap) {
+ int msgType = ap.getFilterMessageType();
+ int phoneType = fi.phoneType;
+
+ if (msgType == -1)
+ return true;
+ if ((msgType & 0x03) == 0)
+ return true;
+
+ if (((msgType & 0x01) == 0) && (phoneType == TelephonyManager.PHONE_TYPE_GSM))
+ return true;
+
+ if (((msgType & 0x02) == 0) && (phoneType == TelephonyManager.PHONE_TYPE_CDMA))
+ return true;
+
+ return false;
+ }
+
+ private boolean mmsSelected(FilterInfo fi, BluetoothMapAppParams ap) {
+ int msgType = ap.getFilterMessageType();
+
+ if (msgType == -1)
+ return true;
+
+ if ((msgType & 0x08) == 0)
+ return true;
+
+ return false;
+ }
+
+ private void setFilterInfo(FilterInfo fi) {
+ TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm != null) {
+ fi.phoneType = tm.getPhoneType();
+ fi.phoneNum = tm.getLine1Number();
+ fi.phoneAlphaTag = tm.getLine1AlphaTag();
+ Log.d(TAG, "phone type = " + fi.phoneType +
+ " phone num = " + fi.phoneNum +
+ " phone alpha tag = " + fi.phoneAlphaTag);
+ }
+ }
+
+ public BluetoothMapMessageListing msgListing(String folder, BluetoothMapAppParams ap) {
+ Log.d(TAG, "msgListing: folder = " + folder);
+ BluetoothMapMessageListing bmList = new BluetoothMapMessageListing();
+ BluetoothMapMessageListingElement e = null;
+
+ /* Cache some info used throughout filtering */
+ FilterInfo fi = new FilterInfo();
+ setFilterInfo(fi);
+
+ if (smsSelected(fi, ap)) {
+ fi.msgType = FilterInfo.TYPE_SMS;
+
+ String where = setWhereFilter(folder, fi, ap);
+
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, where, null, "date DESC");
+
+ if (c != null) {
+ while (c.moveToNext()) {
+ if (matchAddresses(c, fi, ap)) {
+ printSms(c);
+ e = element(c, fi, ap);
+ bmList.add(e);
+ }
+ }
+ c.close();
+ }
+ }
+
+ if (mmsSelected(fi, ap)) {
+ fi.msgType = FilterInfo.TYPE_MMS;
+
+ String where = setWhereFilter(folder, fi, ap);
+
+ Cursor c = mResolver.query(Mms.CONTENT_URI,
+ MMS_PROJECTION, where, null, "date DESC");
+
+ if (c != null) {
+ int cnt = 0;
+ while (c.moveToNext()) {
+ if (matchAddresses(c, fi, ap)) {
+ printMms(c);
+ e = element(c, fi, ap);
+ bmList.add(e);
+ }
+ }
+ c.close();
+ }
+ }
+
+ /* Enable this if post sorting and segmenting needed */
+ bmList.sort();
+ bmList.segment(ap.getMaxListCount(), ap.getStartOffset());
+
+ return bmList;
+ }
+
+ public int msgListingSize(String folder, BluetoothMapAppParams ap) {
+ Log.d(TAG, "msgListingSize: folder = " + folder);
+ int cnt = 0;
+
+ /* Cache some info used throughout filtering */
+ FilterInfo fi = new FilterInfo();
+ setFilterInfo(fi);
+
+ if (smsSelected(fi, ap)) {
+ String where = setWhereFilter(folder, fi, ap);
+
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, where, null, "date DESC");
+
+ if (c != null) {
+ cnt = c.getCount();
+ c.close();
+ }
+ }
+ Log.d(TAG, "msgListingSize: size = " + cnt);
+ return cnt;
+ }
+
+ /**
+ * Get the folder name of an SMS message or MMS message.
+ * @param c the cursor pointing at the message
+ * @return the folder name.
+ */
+ private String getFolderName(int type, int threadId) {
+
+ if(threadId == -1)
+ return "deleted";
+
+ switch(type) {
+ case 1:
+ return "inbox";
+ case 2:
+ return "send";
+ case 3:
+ return "draft";
+ case 4: // Just name outbox, failed and queued "outbox"
+ case 5:
+ case 6:
+ return "outbox";
+ }
+ return "";
+ }
+
+ public byte[] getMessage(String handle, int charset) throws UnsupportedEncodingException{
+ TYPE type = BluetoothMapUtils.getMsgTypeFromHandle(handle);
+ long id = BluetoothMapUtils.getCpHandle(handle);
+ switch(type) {
+ case SMS_GSM:
+ case SMS_CDMA:
+ return getSmsMessage(id, charset);
+ case MMS:
+ return getMmsMessage(id);
+ case EMAIL:
+ throw new IllegalArgumentException("Email not implemented - invalid message handle.");
+ }
+ throw new IllegalArgumentException("Invalid message handle.");
+ }
+
+ private void setVCardFromPhoneNumber(BluetoothMapbMessage message, String phone, boolean incoming) {
+ String contactId = null, contactName = null;
+ String[] phoneNumbers = null;
+ String[] emailAddresses = null;
+ Cursor p;
+
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(phone));
+
+ String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME};
+ String selection = Contacts.IN_VISIBLE_GROUP + "=1";
+ String orderBy = Contacts._ID + " ASC";
+
+ // Get the contact _ID and name
+ p = mResolver.query(uri, projection, selection, null, orderBy);
+ if (p != null && p.getCount() >= 1) {
+ p.moveToFirst();
+ contactId = p.getString(p.getColumnIndex(Contacts._ID));
+ contactName = p.getString(p.getColumnIndex(Contacts.DISPLAY_NAME));
+ }
+ p.close();
+
+ // Bail out if we are unable to find a contact, based on the phone number
+ if(contactId == null) {
+ phoneNumbers = new String[1];
+ phoneNumbers[0] = phone;
+ }
+ else {
+ // Fetch all contact phone numbers
+ p = mResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
+ new String[]{contactId},
+ null);
+ if(p != null) {
+ int i = 0;
+ phoneNumbers = new String[p.getCount()];
+ while (p != null && p.moveToNext()) {
+ String number = p.getString(
+ p.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
+ phoneNumbers[i++] = number;
+ }
+ p.close();
+ }
+
+ // Fetch contact e-mail addresses
+ p = mResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, null,
+ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
+ new String[]{contactId},
+ null);
+ if(p != null) {
+ int i = 0;
+ emailAddresses = new String[p.getCount()];
+ while (p != null && p.moveToNext()) {
+ String emailAddress = p.getString(
+ p.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS));
+ emailAddresses[i++] = emailAddress;
+ }
+ p.close();
+ }
+ }
+ if(incoming == true)
+ message.addOriginator(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name
+ else
+ message.addRecipient(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name
+ }
+
+ public static final int MAP_MESSAGE_CHARSET_NATIVE = 0;
+ public static final int MAP_MESSAGE_CHARSET_UTF8 = 1;
+
+ public byte[] getSmsMessage(long id, int charset) throws UnsupportedEncodingException{
+ int type, threadId;
+ long time = -1;
+ String msgBody;
+ BluetoothMapbMessageSms message = new BluetoothMapbMessageSms();
+ TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, "_ID = " + id, null, null);
+
+ if(c != null && c.moveToFirst())
+ {
+
+ if(V) Log.d(TAG,"c.count: " + c.getCount());
+
+ if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
+ message.setType(TYPE.SMS_GSM);
+ } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
+ message.setType(TYPE.SMS_CDMA);
+ }
+
+ String read = c.getString(c.getColumnIndex(Sms.READ));
+ if (read.equalsIgnoreCase("1"))
+ message.setStatus(true);
+ else
+ message.setStatus(false);
+
+ type = c.getInt(c.getColumnIndex(Sms.TYPE));
+ threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
+ message.setFolder(getFolderName(type, threadId));
+
+ msgBody = c.getString(c.getColumnIndex(Sms.BODY));
+
+ String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
+
+ if(type == 1) // Inbox message needs to set the vCard as originator
+ setVCardFromPhoneNumber(message, phone, true);
+ else // Other messages sets the vCard as the recipient
+ setVCardFromPhoneNumber(message, phone, false);
+
+ if(charset == MAP_MESSAGE_CHARSET_NATIVE) {
+ if(type == 1)
+ message.setSmsBodyPdus(BluetoothMapSmsPdu.getSubmitPdus(msgBody, phone));
+ else
+ message.setSmsBodyPdus(BluetoothMapSmsPdu.getDeliverPdus(msgBody, phone, time)); // TODO: No support for GSM
+ } else /*if (charset == MAP_MESSAGE_CHARSET_UTF8)*/ {
+ message.setSmsBody(msgBody);
+ }
+
+ c.close();
+
+ return message.encode();
+ }
+ throw new IllegalArgumentException("SMS handle not found");
+ }
+
+ private void extractMmsAddresses(long id, BluetoothMapbMessageMmsEmail message) {
+ final String[] projection = null;
+ String selection = new String("msg_id=" + id);
+ String uriStr = String.format("content://mms/%d/addr", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ Cursor c = mResolver.query(
+ uriAddress,
+ projection,
+ selection,
+ null, null);
+ /* TODO: Change the setVCard...() to return the vCard, and use the name in message.addXxx() */
+ if (c.moveToFirst()) {
+ do {
+ String address = c.getString(c.getColumnIndex("address"));
+ Integer type = c.getInt(c.getColumnIndex("type"));
+ switch(type) {
+ case MMS_FROM:
+ setVCardFromPhoneNumber(message, address, false);
+ message.addFrom(null, address);
+ break;
+ case MMS_TO:
+ setVCardFromPhoneNumber(message, address, true);
+ message.addTo(null, address);
+ break;
+ case MMS_CC:
+ setVCardFromPhoneNumber(message, address, true);
+ message.addCc(null, address);
+ break;
+ case MMS_BCC:
+ setVCardFromPhoneNumber(message, address, true);
+ message.addBcc(null, address);
+ default:
+ break;
+ }
+ } while(c.moveToNext());
+ }
+ }
+
+ private byte[] readMmsDataPart(long partid) {
+ String uriStr = String.format("content://mms/part/%d", partid);
+ Uri uriAddress = Uri.parse(uriStr);
+ InputStream is = null;
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ int bufferSize = 8192;
+ byte[] buffer = new byte[bufferSize];
+ byte[] retVal = null;
+
+ try {
+ is = mResolver.openInputStream(uriAddress);
+ int len = 0;
+ while ((len = is.read(buffer)) != -1) {
+ os.write(buffer, 0, len); // We need to specify the len, as it can be != bufferSize
+ }
+ retVal = os.toByteArray();
+ } catch (IOException e) {
+ // do nothing for now
+ Log.w(TAG,"Error reading part data",e);
+ } finally {
+ try {
+ os.close();
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ return retVal;
+ }
+
+ private void extractMmsParts(long id, BluetoothMapbMessageMmsEmail message)
+ {
+ final String[] projection = null;
+ String selection = new String("mid=" + id);
+ String uriStr = String.format("content://mms/%d/part", id);
+ Uri uriAddress = Uri.parse(uriStr);
+ BluetoothMapbMessageMmsEmail.MimePart part;
+ Cursor c = mResolver.query(
+ uriAddress,
+ projection,
+ selection,
+ null, null);
+
+ if (c.moveToFirst()) {
+ do {
+ Long partId = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String contentType = c.getString(c.getColumnIndex("ct"));
+ String name = c.getString(c.getColumnIndex("name"));
+ String charset = c.getString(c.getColumnIndex("chset"));
+ String filename = c.getString(c.getColumnIndex("fn"));
+ String text = c.getString(c.getColumnIndex("text"));
+ Integer fd = c.getInt(c.getColumnIndex("_data"));
+
+ if(D)Log.d(TAG, " _id : " + partId +
+ "\n ct : " + contentType +
+ "\n partname : " + name +
+ "\n charset : " + charset +
+ "\n filename : " + filename +
+ "\n text : " + text +
+ "\n fd : " + fd);
+ part = message.addMimePart();
+ part.contentType = contentType;
+ part.partName = name;
+ try {
+ if(text != null) {
+ part.data = text.getBytes("UTF-8");
+ part.charsetName = "utf-8";
+ }
+ else {
+ part.data = readMmsDataPart(partId);
+ if(charset != null)
+ part.charsetName = CharacterSets.getMimeName(Integer.parseInt(charset));
+ }
+ } catch (NumberFormatException e) {
+ Log.d(TAG,"extractMmsParts",e);
+ part.data = null;
+ part.charsetName = null;
+ } catch (UnsupportedEncodingException e) {
+ Log.d(TAG,"extractMmsParts",e);
+ part.data = null;
+ part.charsetName = null;
+ } finally {
+ }
+ part.fileName = filename;
+ } while(c.moveToNext());
+ }
+ message.updateCharset();
+ }
+
+ public byte[] getMmsMessage(long id) throws UnsupportedEncodingException {
+ int msgBox, threadId;
+ BluetoothMapbMessageMmsEmail message = new BluetoothMapbMessageMmsEmail();
+ Cursor c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION, "_ID = " + id, null, null);
+ if(c != null && c.moveToFirst())
+ {
+ message.setType(TYPE.MMS);
+
+ // The MMS info:
+ String read = c.getString(c.getColumnIndex(Mms.READ));
+ if (read.equalsIgnoreCase("1"))
+ message.setStatus(true);
+ else
+ message.setStatus(false);
+
+ msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
+ message.setFolder(getFolderName(msgBox, threadId));
+
+ message.setSubject(c.getString(c.getColumnIndex(Mms.SUBJECT)));
+ message.setMessageId(c.getString(c.getColumnIndex(Mms.MESSAGE_ID)));
+ message.setContentType(c.getString(c.getColumnIndex(Mms.CONTENT_TYPE)));
+ message.setDate(c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L);
+ // c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)); - TODO: Do we need this
+ // c.getLong(c.getColumnIndex(Mms.DATE_SENT)); - this is never used
+ // c.getInt(c.getColumnIndex(Mms.STATUS)); - don't know what this is
+
+ // The parts
+ extractMmsParts(id, message);
+
+ // The addresses
+ extractMmsAddresses(id, message);
+
+ c.close();
+
+ return message.encode();
+ }
+ else if(c != null) {
+ c.close();
+ }
+
+ throw new IllegalArgumentException("MMS handle not found");
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
new file mode 100644
index 000000000..ed090ea1d
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
@@ -0,0 +1,1168 @@
+/*
+* 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 java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.BaseColumns;
+import android.provider.Telephony;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.Sms.Inbox;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart;
+import com.google.android.mms.pdu.PduHeaders;
+
+public class BluetoothMapContentObserver {
+ private static final String TAG = "BluetoothMapContentObserver";
+
+ private static final boolean D = true;
+ private static final boolean V = true;
+
+ private Context mContext;
+ private ContentResolver mResolver;
+ private BluetoothMnsObexClient mMnsClient;
+ private int mMasId;
+
+ public static final int DELETED_THREAD_ID = -1;
+
+ /* X-Mms-Message-Type field types. These are from PduHeaders.java */
+ public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
+
+ private TYPE mSmsType;
+
+ static final String[] SMS_PROJECTION = new String[] {
+ BaseColumns._ID,
+ Sms.THREAD_ID,
+ Sms.ADDRESS,
+ Sms.BODY,
+ Sms.DATE,
+ Sms.READ,
+ Sms.TYPE,
+ Sms.STATUS,
+ Sms.LOCKED,
+ Sms.ERROR_CODE,
+ };
+
+ static final String[] MMS_PROJECTION = new String[] {
+ BaseColumns._ID,
+ Mms.THREAD_ID,
+ Mms.MESSAGE_ID,
+ Mms.MESSAGE_SIZE,
+ Mms.SUBJECT,
+ Mms.CONTENT_TYPE,
+ Mms.TEXT_ONLY,
+ Mms.DATE,
+ Mms.DATE_SENT,
+ Mms.READ,
+ Mms.MESSAGE_BOX,
+ Mms.MESSAGE_TYPE,
+ Mms.STATUS,
+ };
+
+ public BluetoothMapContentObserver(final Context context) {
+ mContext = context;
+ mResolver = mContext.getContentResolver();
+
+ mSmsType = getSmsType();
+ }
+
+ private TYPE getSmsType() {
+ TYPE smsType = null;
+ TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+
+ if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
+ smsType = TYPE.SMS_GSM;
+ } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
+ smsType = TYPE.SMS_CDMA;
+ }
+
+ return smsType;
+ }
+
+ private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
+ + " Uri: " + uri.toString() + " selfchange: " + selfChange);
+
+ handleMsgListChanges();
+ }
+ };
+
+ private static final String folderSms[] = {
+ "",
+ "inbox",
+ "sent",
+ "draft",
+ "outbox",
+ "outbox",
+ "outbox",
+ "inbox",
+ "inbox",
+ };
+
+ private static final String folderMms[] = {
+ "",
+ "inbox",
+ "sent",
+ "draft",
+ "outbox",
+ };
+
+ private class Event {
+ String eventType;
+ long handle;
+ String folder;
+ String oldFolder;
+ TYPE msgType;
+
+ public Event(String eventType, long handle, String folder,
+ String oldFolder, TYPE msgType) {
+ String PATH = "telecom/msg/";
+ this.eventType = eventType;
+ this.handle = handle;
+ if (folder != null) {
+ this.folder = PATH + folder;
+ } else {
+ this.folder = null;
+ }
+ if (oldFolder != null) {
+ this.oldFolder = PATH + oldFolder;
+ } else {
+ this.oldFolder = null;
+ }
+ this.msgType = msgType;
+ }
+
+ public byte[] encode() throws UnsupportedEncodingException {
+ StringWriter sw = new StringWriter();
+ XmlSerializer xmlEvtReport = Xml.newSerializer();
+ try {
+ xmlEvtReport.setOutput(sw);
+ xmlEvtReport.startDocument(null, null);
+ xmlEvtReport.text("\n");
+ xmlEvtReport.startTag("", "Map-event-report");
+ xmlEvtReport.attribute("", "version", "1.0");
+
+ xmlEvtReport.startTag("", "event");
+ xmlEvtReport.attribute("", "type", eventType);
+ xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType));
+ if (folder != null) {
+ xmlEvtReport.attribute("", "folder", folder);
+ }
+ if (oldFolder != null) {
+ xmlEvtReport.attribute("", "old_folder", oldFolder);
+ }
+ xmlEvtReport.attribute("", "msg_type", msgType.name());
+ xmlEvtReport.endTag("", "event");
+
+ xmlEvtReport.endTag("", "Map-event-report");
+ xmlEvtReport.endDocument();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ if (V) System.out.println(sw.toString());
+
+ return sw.toString().getBytes("UTF-8");
+ }
+ }
+
+ private class Msg {
+ long id;
+ int type;
+
+ public Msg(long id, int type) {
+ this.id = id;
+ this.type = type;
+ }
+ }
+
+ private Map<Long, Msg> mMsgListSms =
+ Collections.synchronizedMap(new HashMap<Long, Msg>());
+
+ private Map<Long, Msg> mMsgListMms =
+ Collections.synchronizedMap(new HashMap<Long, Msg>());
+
+ public void registerObserver(BluetoothMnsObexClient mns, int masId) {
+ if (V) Log.d(TAG, "registerObserver");
+ /* Use MmsSms Uri since the Sms Uri is not notified on deletes */
+ mMasId = masId;
+ mMnsClient = mns;
+ mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
+ initMsgList();
+ }
+
+ public void unregisterObserver() {
+ if (V) Log.d(TAG, "unregisterObserver");
+ mResolver.unregisterContentObserver(mObserver);
+ }
+
+ private void sendEvent(Event evt) {
+ Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " "
+ + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name());
+
+ if (mMnsClient == null) {
+ Log.d(TAG, "sendEvent: No MNS client registered - don't send event");
+ return;
+ }
+
+ try {
+ mMnsClient.sendEvent(evt.encode(), mMasId);
+ } catch (UnsupportedEncodingException ex) {
+ /* do nothing */
+ }
+ }
+
+ private void initMsgList() {
+ if (V) Log.d(TAG, "initMsgList");
+
+ mMsgListSms.clear();
+ mMsgListMms.clear();
+
+ HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
+
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+
+ Msg msg = new Msg(id, type);
+ msgListSms.put(id, msg);
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ mMsgListSms = msgListSms;
+
+ HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
+
+ c = mResolver.query(Mms.CONTENT_URI,
+ MMS_PROJECTION, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+
+ Msg msg = new Msg(id, type);
+ msgListMms.put(id, msg);
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ mMsgListMms = msgListMms;
+ }
+
+ private void handleMsgListChangesSms() {
+ if (V) Log.d(TAG, "handleMsgListChangesSms");
+
+ HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
+
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, null, null, null);
+
+ synchronized(mMsgListSms) {
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+
+ Msg msg = mMsgListSms.remove(id);
+
+ if (msg == null) {
+ /* New message */
+ msg = new Msg(id, type);
+ msgListSms.put(id, msg);
+
+ if (folderSms[type].equals("inbox")) {
+ Event evt = new Event("NewMessage", id, folderSms[type],
+ null, mSmsType);
+ sendEvent(evt);
+ }
+ } else {
+ /* Existing message */
+ if (type != msg.type) {
+ Log.d(TAG, "new type: " + type + " old type: " + msg.type);
+ Event evt = new Event("MessageShift", id, folderSms[type],
+ folderSms[msg.type], mSmsType);
+ sendEvent(evt);
+ msg.type = type;
+ }
+ msgListSms.put(id, msg);
+ }
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ for (Msg msg : mMsgListSms.values()) {
+ Event evt = new Event("MessageDeleted", msg.id, "deleted",
+ folderSms[msg.type], mSmsType);
+ sendEvent(evt);
+ }
+
+ mMsgListSms = msgListSms;
+ }
+ }
+
+ private void handleMsgListChangesMms() {
+ if (V) Log.d(TAG, "handleMsgListChangesMms");
+
+ HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
+
+ Cursor c = mResolver.query(Mms.CONTENT_URI,
+ MMS_PROJECTION, null, null, null);
+
+ synchronized(mMsgListMms) {
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
+
+ Msg msg = mMsgListMms.remove(id);
+
+ if (msg == null) {
+ /* New message - only notify on retrieve conf */
+ if (folderMms[type].equals("inbox") &&
+ mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
+ continue;
+ }
+
+ msg = new Msg(id, type);
+ msgListMms.put(id, msg);
+
+ if (folderMms[type].equals("inbox")) {
+ Event evt = new Event("NewMessage", id, folderMms[type],
+ null, TYPE.MMS);
+ sendEvent(evt);
+ }
+ } else {
+ /* Existing message */
+ if (type != msg.type) {
+ Log.d(TAG, "new type: " + type + " old type: " + msg.type);
+ Event evt = new Event("MessageShift", id, folderMms[type],
+ folderMms[msg.type], TYPE.MMS);
+ sendEvent(evt);
+ msg.type = type;
+
+ if (folderMms[type].equals("sent")) {
+ evt = new Event("SendingSuccess", id,
+ folderSms[type], null, TYPE.MMS);
+ sendEvent(evt);
+ }
+ }
+ msgListMms.put(id, msg);
+ }
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ for (Msg msg : mMsgListMms.values()) {
+ Event evt = new Event("MessageDeleted", msg.id, "deleted",
+ folderMms[msg.type], TYPE.MMS);
+ sendEvent(evt);
+ }
+
+ mMsgListMms = msgListMms;
+ }
+ }
+
+ private void handleMsgListChanges() {
+ handleMsgListChangesSms();
+ handleMsgListChangesMms();
+ }
+
+ private boolean deleteMessageMms(long handle) {
+ boolean res = false;
+ Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+ if (c != null && c.moveToFirst()) {
+ /* Move to deleted folder, or delete if already in deleted folder */
+ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
+ if (threadId != DELETED_THREAD_ID) {
+ /* Set deleted thread id */
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Mms.THREAD_ID, DELETED_THREAD_ID);
+ mResolver.update(uri, contentValues, null, null);
+ } else {
+ /* Delete from observer message list to avoid delete notifications */
+ mMsgListMms.remove(handle);
+ /* Delete message */
+ mResolver.delete(uri, null, null);
+ }
+ res = true;
+ }
+ if (c != null) {
+ c.close();
+ }
+ return res;
+ }
+
+ private void updateThreadIdMms(Uri uri, long threadId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Mms.THREAD_ID, threadId);
+ mResolver.update(uri, contentValues, null, null);
+ }
+
+ private boolean unDeleteMessageMms(long handle) {
+ boolean res = false;
+ Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
+ if (threadId == DELETED_THREAD_ID) {
+ /* Restore thread id from address, or if no thread for address
+ * create new thread by insert and remove of fake message */
+ String address;
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ if (msgBox == Mms.MESSAGE_BOX_INBOX) {
+ address = BluetoothMapContent.getAddressMms(mResolver, id,
+ BluetoothMapContent.MMS_FROM);
+ } else {
+ address = BluetoothMapContent.getAddressMms(mResolver, id,
+ BluetoothMapContent.MMS_TO);
+ }
+ Set<String> recipients = new HashSet<String>();
+ recipients.addAll(Arrays.asList(address));
+ updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ } else {
+ Log.d(TAG, "Message not in deleted folder: handle " + handle
+ + " threadId " + threadId);
+ }
+ res = true;
+ }
+ if (c != null) {
+ c.close();
+ }
+ return res;
+ }
+
+ private boolean deleteMessageSms(long handle) {
+ boolean res = false;
+ Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ /* Move to deleted folder, or delete if already in deleted folder */
+ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
+ if (threadId != DELETED_THREAD_ID) {
+ /* Set deleted thread id */
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID);
+ mResolver.update(uri, contentValues, null, null);
+ } else {
+ /* Delete from observer message list to avoid delete notifications */
+ mMsgListSms.remove(handle);
+ /* Delete message */
+ mResolver.delete(uri, null, null);
+ }
+ res = true;
+ }
+ if (c != null) {
+ c.close();
+ }
+ return res;
+ }
+
+ private void updateThreadIdSms(Uri uri, long threadId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Sms.THREAD_ID, threadId);
+ mResolver.update(uri, contentValues, null, null);
+ }
+
+ private boolean unDeleteMessageSms(long handle) {
+ boolean res = false;
+ Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
+ if (threadId == DELETED_THREAD_ID) {
+ String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ Set<String> recipients = new HashSet<String>();
+ recipients.addAll(Arrays.asList(address));
+ updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ } else {
+ Log.d(TAG, "Message not in deleted folder: handle " + handle
+ + " threadId " + threadId);
+ }
+ res = true;
+ }
+ if (c != null) {
+ c.close();
+ }
+ return res;
+ }
+
+ public boolean setMessageStatusDeleted(long handle, TYPE type, int statusValue) {
+ boolean res = false;
+ if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle
+ + " type " + type + " value " + statusValue);
+
+ if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
+ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
+ res = deleteMessageSms(handle);
+ } else if (type == TYPE.MMS) {
+ res = deleteMessageMms(handle);
+ }
+ } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
+ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
+ res = unDeleteMessageSms(handle);
+ } else if (type == TYPE.MMS) {
+ res = unDeleteMessageMms(handle);
+ }
+ }
+ return res;
+ }
+
+ public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) {
+ boolean res = true;
+
+ if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle
+ + " type " + type + " value " + statusValue);
+
+ /* Approved MAP spec errata 3445 states that read status initiated */
+ /* by the MCE shall change the MSE read status. */
+
+ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
+ Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Sms.READ, statusValue);
+ mResolver.update(uri, contentValues, null, null);
+ } else if (type == TYPE.MMS) {
+ Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
+ Cursor c = mResolver.query(uri, null, null, null, null);
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Mms.READ, statusValue);
+ mResolver.update(uri, contentValues, null, null);
+ }
+
+ return res;
+ }
+
+ private class PushMsgInfo {
+ long id;
+ int transparent;
+ int retry;
+ String phone;
+ Uri uri;
+ int parts;
+ int partsSent;
+ int partsDelivered;
+ boolean resend;
+
+ public PushMsgInfo(long id, int transparent,
+ int retry, String phone, Uri uri) {
+ this.id = id;
+ this.transparent = transparent;
+ this.retry = retry;
+ this.phone = phone;
+ this.uri = uri;
+ this.resend = false;
+ };
+ }
+
+ private Map<Long, PushMsgInfo> mPushMsgList =
+ Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
+
+ public long pushMessage(BluetoothMapbMessage msg, String folder,
+ BluetoothMapAppParams ap) throws IllegalArgumentException {
+ if (D) Log.d(TAG, "pushMessage");
+ ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients();
+ int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ?
+ 0 : ap.getTransparent();
+ int retry = ap.getRetry();
+ int charset = ap.getCharset();
+ long handle = -1;
+
+ if (recipientList == null) {
+ Log.d(TAG, "empty recipient list");
+ return -1;
+ }
+
+ for (BluetoothMapbMessage.vCard recipient : recipientList) {
+ if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
+ {
+ /* Only send to first address */
+ String phone = recipient.getFirstPhoneNumber();
+ boolean read = false;
+ boolean deliveryReport = true;
+
+ switch(msg.getType()){
+ case MMS:
+ {
+ /* Send message if folder is outbox */
+ if (folder.equals("outbox")) {
+ handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg);
+ }
+ break;
+ }
+ case SMS_GSM: //fall-through
+ case SMS_CDMA:
+ {
+ /* Add the message to the database */
+ String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
+ Uri contentUri = Uri.parse("content://sms/" + folder);
+ Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
+ "", System.currentTimeMillis(), read, deliveryReport);
+
+ if (uri == null) {
+ Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
+ return -1;
+ }
+
+ handle = Long.parseLong(uri.getLastPathSegment());
+
+ /* Send message if folder is outbox */
+ if (folder.equals("outbox")) {
+ PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
+ retry, phone, uri);
+ mPushMsgList.put(handle, msgInfo);
+ sendMessage(msgInfo, msgBody);
+ }
+ break;
+ }
+ case EMAIL:
+ {
+ break;
+ }
+ }
+
+ }
+ }
+
+ /* If multiple recipients return handle of last */
+ return handle;
+ }
+
+
+
+ public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) {
+ /*
+ *strategy:
+ *1) parse message into parts
+ *if folder is outbox/drafts:
+ *2) push message to draft
+ *if folder is outbox:
+ *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
+ *4) send intent to mms app in order to wake it up.
+ *else if folder !outbox:
+ *1) push message to folder
+ * */
+ if (folder != null && (folder.equalsIgnoreCase("outbox")|| folder.equalsIgnoreCase("drafts"))) {
+ long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
+ /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
+ if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) {
+ moveDraftToOutbox(handle);
+
+ Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
+ Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
+ mContext.sendBroadcast(sendIntent);
+ }
+ return handle;
+ } else {
+ /* not allowed to push mms to anything but outbox/drafts */
+ throw new IllegalArgumentException("Cannot push message to other folders than outbox/drafts");
+ }
+
+ }
+
+
+ private void moveDraftToOutbox(long handle) {
+ ContentResolver contentResolver = mContext.getContentResolver();
+ /*Move message by changing the msg_box value in the content provider database */
+ if (handle != -1) {
+ String whereClause = " _id= " + handle;
+ Uri uri = Uri.parse("content://mms");
+ Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null);
+ if (queryResult != null) {
+ if (queryResult.getCount() > 0) {
+ queryResult.moveToFirst();
+ ContentValues data = new ContentValues();
+ /* set folder to be outbox */
+ data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX);
+ contentResolver.update(uri, data, whereClause, null);
+ Log.d(TAG, "moved draft MMS to outbox");
+ }
+ queryResult.close();
+ }else {
+ Log.d(TAG, "Could not move draft to outbox ");
+ }
+ }
+ }
+ private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) {
+ /**
+ * strategy:
+ * 1) parse msg into parts + header
+ * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
+ * 3) push parts into content://mms/parts/ table
+ * 3)
+ */
+
+ ContentValues values = new ContentValues();
+ values.put("msg_box", folder);
+
+ values.put("read", 0);
+ values.put("seen", 0);
+ values.put("sub_cs", 106);
+ values.put("ct_t", "application/vnd.wap.multipart.related");
+ values.put("exp", 604800);
+ values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
+ values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ);
+ values.put("v", PduHeaders.CURRENT_MMS_VERSION);
+ values.put("pri", PduHeaders.PRIORITY_NORMAL);
+ values.put("rr", PduHeaders.VALUE_NO);
+ values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis()));
+ values.put("d_rpt", PduHeaders.VALUE_NO);
+ values.put("locked", 0);
+ // Get thread id
+ Set<String> recipients = new HashSet<String>();
+ recipients.addAll(Arrays.asList(to_address));
+ values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ Uri uri = Uri.parse("content://mms");
+
+ ContentResolver cr = mContext.getContentResolver();
+ uri = cr.insert(uri, values);
+
+ if (uri == null) {
+ // unable to insert MMS
+ Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
+ return -1;
+ }
+
+ long handle = Long.parseLong(uri.getLastPathSegment());
+ if (V){
+ Log.v(TAG, " NEW URI " + uri.toString());
+ }
+ try {
+ for(MimePart part : msg.getMimeParts()) {
+ values.clear();
+ if(part.contentType != null && part.contentType.toUpperCase().contains("TEXT")) {
+ values.put("seq", 0);
+ values.put("ct", "text/plain");
+ values.put("name", "null");
+ values.put("chset", 106);
+ values.put("cd", "null");
+ values.put("fn", part.partName);
+ values.put("name", part.partName);
+ values.put("cid", "<smil>");
+ values.put("cl", part.partName);
+ values.put("ctt_s", "null");
+ values.put("ctt_t", "null");
+ values.put("_data", "null");
+ values.put("text", new String(part.data, "UTF-8"));
+ uri = Uri.parse("content://mms/" + handle + "/part");
+ uri = cr.insert(uri, values);
+
+ } else if (part.contentType != null && part.contentType.toUpperCase().contains("SMIL")){
+
+ values.put("seq", -1);
+ values.put("ct", "application/smil");
+ values.put("cid", "<smil>");
+ values.put("cl", "smil.xml");
+ values.put("fn", "smil.xml");
+ values.put("name", "smil.xml");
+ values.put("text", new String(part.data, "UTF-8"));
+
+ uri = Uri.parse("content://mms/" + handle + "/part");
+ uri = cr.insert(uri, values);
+
+ }else /*VIDEO/AUDIO/IMAGE*/ {
+ writeMmsDataPart(handle, part.contentType, part.partName, part.data);
+ }
+ if (uri != null && V){
+ Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString());
+ }
+ }
+ } catch (UnsupportedEncodingException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ values.clear();
+ values.put("contact_id", "null");
+ values.put("address", "insert-address-token");
+ values.put("type", BluetoothMapContent.MMS_FROM);
+ values.put("charset", 106);
+
+ uri = Uri.parse("content://mms/" + handle + "/addr");
+ uri = cr.insert(uri, values);
+ if (uri != null && V){
+ Log.v(TAG, " NEW URI " + uri.toString());
+ }
+
+ values.clear();
+ values.put("contact_id", "null");
+ values.put("address", to_address);
+ values.put("type", BluetoothMapContent.MMS_TO);
+ values.put("charset", 106);
+
+ uri = Uri.parse("content://mms/" + handle + "/addr");
+ uri = cr.insert(uri, values);
+ if (uri != null && V){
+ Log.v(TAG, " NEW URI " + uri.toString());
+ }
+ return handle;
+ }
+
+
+ private void writeMmsDataPart(long handle, String contentType, String name, byte[] data) throws IOException{
+ ContentValues values = new ContentValues();
+ values.put("mid", handle);
+ values.put("ct", contentType);
+ values.put("cid", "<smil>");
+ values.put("cl", name);
+ values.put("fn", name);
+ values.put("name", name);
+ Uri partUri = Uri.parse("content://mms/" + handle + "/part");
+ Uri res = mResolver.insert(partUri, values);
+
+ // Add data to part
+ OutputStream os = mResolver.openOutputStream(res);
+ ByteArrayInputStream is = new ByteArrayInputStream(data);
+ byte[] buffer = new byte[256];
+ for (int len=0; (len=is.read(buffer)) != -1;)
+ {
+ os.write(buffer, 0, len);
+ }
+ os.close();
+ is.close();
+ }
+
+
+ public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
+
+ SmsManager smsMng = SmsManager.getDefault();
+ ArrayList<String> parts = smsMng.divideMessage(msgBody);
+ msgInfo.parts = parts.size();
+
+ ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
+ ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
+
+ for (int i = 0; i < msgInfo.parts; i++) {
+ Intent intent;
+ intent = new Intent(ACTION_MESSAGE_DELIVERY, null);
+ intent.putExtra("HANDLE", msgInfo.id);
+ deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT));
+
+ intent = new Intent(ACTION_MESSAGE_SENT, null);
+ intent.putExtra("HANDLE", msgInfo.id);
+ sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT));
+ }
+
+ Log.d(TAG, "sendMessage to " + msgInfo.phone);
+
+ smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
+ deliveryIntents);
+ }
+
+ private static final String ACTION_MESSAGE_DELIVERY =
+ "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
+ private static final String ACTION_MESSAGE_SENT =
+ "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
+
+ private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
+
+ private class SmsBroadcastReceiver extends BroadcastReceiver {
+ private final String[] ID_PROJECTION = new String[] { Sms._ID };
+ private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status");
+
+ public void register() {
+ Handler handler = new Handler();
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
+ intentFilter.addAction(ACTION_MESSAGE_SENT);
+ mContext.registerReceiver(this, intentFilter, null, handler);
+ }
+
+ public void unregister() {
+ try {
+ mContext.unregisterReceiver(this);
+ } catch (IllegalArgumentException e) {
+ /* do nothing */
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ long handle = intent.getLongExtra("HANDLE", -1);
+ PushMsgInfo msgInfo = mPushMsgList.get(handle);
+
+ Log.d(TAG, "onReceive: action" + action);
+
+ if (msgInfo == null) {
+ Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
+ return;
+ }
+
+ if (action.equals(ACTION_MESSAGE_SENT)) {
+ msgInfo.partsSent++;
+ if (msgInfo.partsSent == msgInfo.parts) {
+ actionMessageSent(context, intent, msgInfo);
+ }
+ } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
+ msgInfo.partsDelivered++;
+ if (msgInfo.partsDelivered == msgInfo.parts) {
+ actionMessageDelivery(context, intent, msgInfo);
+ }
+ } else {
+ Log.d(TAG, "onReceive: Unknown action " + action);
+ }
+ }
+
+ private void actionMessageSent(Context context, Intent intent,
+ PushMsgInfo msgInfo) {
+ int result = getResultCode();
+ boolean delete = false;
+
+ if (result == Activity.RESULT_OK) {
+ Log.d(TAG, "actionMessageSent: result OK");
+ if (msgInfo.transparent == 0) {
+ if (!Sms.moveMessageToFolder(context, msgInfo.uri,
+ Sms.MESSAGE_TYPE_SENT, 0)) {
+ Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT");
+ }
+ } else {
+ delete = true;
+ }
+
+ Event evt = new Event("SendingSuccess", msgInfo.id,
+ folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
+ sendEvent(evt);
+
+ } else {
+ if (msgInfo.retry == 1) {
+ /* Notify failure, but keep message in outbox for resending */
+ msgInfo.resend = true;
+ Event evt = new Event("SendingFailure", msgInfo.id,
+ folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
+ sendEvent(evt);
+ } else {
+ if (msgInfo.transparent == 0) {
+ if (!Sms.moveMessageToFolder(context, msgInfo.uri,
+ Sms.MESSAGE_TYPE_FAILED, 0)) {
+ Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
+ }
+ } else {
+ delete = true;
+ }
+
+ Event evt = new Event("SendingFailure", msgInfo.id,
+ folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
+ sendEvent(evt);
+ }
+ }
+
+ if (delete == true) {
+ /* Delete from Observer message list to avoid delete notifications */
+ mMsgListSms.remove(msgInfo.id);
+
+ /* Delete from DB */
+ mResolver.delete(msgInfo.uri, null, null);
+ }
+ }
+
+ private void actionMessageDelivery(Context context, Intent intent,
+ PushMsgInfo msgInfo) {
+ Uri messageUri = intent.getData();
+ byte[] pdu = intent.getByteArrayExtra("pdu");
+ String format = intent.getStringExtra("format");
+
+ SmsMessage message = SmsMessage.createFromPdu(pdu, format);
+ if (message == null) {
+ Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
+ return;
+ }
+ int status = message.getStatus();
+
+ Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ int messageId = cursor.getInt(0);
+
+ Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
+ boolean isStatusReport = message.isStatusReportMessage();
+
+ Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status +
+ ", isStatusReport=" + isStatusReport);
+
+ ContentValues contentValues = new ContentValues(2);
+
+ contentValues.put(Sms.STATUS, status);
+ contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
+ mResolver.update(updateUri, contentValues, null, null);
+ } else {
+ Log.d(TAG, "Can't find message for status update: " + messageUri);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (status == 0) {
+ Event evt = new Event("DeliverySuccess", msgInfo.id,
+ folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
+ sendEvent(evt);
+ } else {
+ Event evt = new Event("DeliveryFailure", msgInfo.id,
+ folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
+ sendEvent(evt);
+ }
+
+ mPushMsgList.remove(msgInfo.id);
+ }
+ }
+
+ private void registerPhoneServiceStateListener() {
+ TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
+ }
+
+ private void unRegisterPhoneServiceStateListener() {
+ TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
+ }
+
+ private void resendPendingMessages() {
+ /* Send pending messages in outbox */
+ String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
+ Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
+ null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
+ PushMsgInfo msgInfo = mPushMsgList.get(id);
+ if (msgInfo == null || msgInfo.resend == false) {
+ continue;
+ }
+ sendMessage(msgInfo, msgBody);
+ } while (c.moveToNext());
+ c.close();
+ }
+ }
+
+ private void failPendingMessages() {
+ /* Move pending messages from outbox to failed */
+ String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
+ Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
+ null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
+ PushMsgInfo msgInfo = mPushMsgList.get(id);
+ if (msgInfo == null || msgInfo.resend == false) {
+ continue;
+ }
+ Sms.moveMessageToFolder(mContext, msgInfo.uri,
+ Sms.MESSAGE_TYPE_FAILED, 0);
+ } while (c.moveToNext());
+ c.close();
+ }
+ }
+
+ private void removeDeletedMessages() {
+ /* Remove messages from virtual "deleted" folder (thread_id -1) */
+ mResolver.delete(Uri.parse("content://sms/"),
+ "thread_id = " + DELETED_THREAD_ID, null);
+ }
+
+ private PhoneStateListener mPhoneListener = new PhoneStateListener() {
+ @Override
+ public void onServiceStateChanged(ServiceState serviceState) {
+ Log.d(TAG, "Phone service state change: " + serviceState.getState());
+ if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
+ resendPendingMessages();
+ }
+ }
+ };
+
+ public void init() {
+ mSmsBroadcastReceiver.register();
+ registerPhoneServiceStateListener();
+ }
+
+ public void deinit() {
+ mSmsBroadcastReceiver.unregister();
+ unRegisterPhoneServiceStateListener();
+ failPendingMessages();
+ removeDeletedMessages();
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapFolderElement.java b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java
new file mode 100644
index 000000000..d3909ddad
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java
@@ -0,0 +1,133 @@
+/*
+* 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 java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import android.util.Xml;
+
+/**
+ * @author cbonde
+ *
+ */
+public class BluetoothMapFolderElement {
+ private String name;
+ private BluetoothMapFolderElement parent = null;
+ private ArrayList<BluetoothMapFolderElement> subFolders;
+
+ public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent ){
+ this.name = name;
+ this.parent = parrent;
+ subFolders = new ArrayList<BluetoothMapFolderElement>();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Fetch the parent folder.
+ * @return the parent folder or null if we are at the root folder.
+ */
+ public BluetoothMapFolderElement getParent() {
+ return parent;
+ }
+
+ /**
+ * Fetch the root folder.
+ * @return the parent folder or null if we are at the root folder.
+ */
+ public BluetoothMapFolderElement getRoot() {
+ BluetoothMapFolderElement rootFolder = this;
+ while(rootFolder.getParent() != null)
+ rootFolder = rootFolder.getParent();
+ return rootFolder;
+ }
+
+ /**
+ * Add a folder.
+ * @param name the name of the folder to add.
+ * @return the added folder element.
+ */
+ public BluetoothMapFolderElement addFolder(String name){
+ BluetoothMapFolderElement newFolder = new BluetoothMapFolderElement(name, this);
+ subFolders.add(newFolder);
+ return newFolder;
+ }
+
+ /**
+ * Fetch the number of sub folders.
+ * @return returns the number of sub folders.
+ */
+ public int getSubFolderCount(){
+ return subFolders.size();
+ }
+
+ /**
+ * Returns the subFolder element matching the supplied folder name.
+ * @param folderName the name of the subFolder to find.
+ * @return the subFolder element if found {@code null} otherwise.
+ */
+ public BluetoothMapFolderElement getSubFolder(String folderName){
+ for(BluetoothMapFolderElement subFolder : subFolders){
+ if(subFolder.getName().equals(folderName))
+ return subFolder;
+ }
+ return null;
+ }
+
+ public byte[] encode(int offset, int count) throws UnsupportedEncodingException {
+ StringWriter sw = new StringWriter();
+ XmlSerializer xmlMsgElement = Xml.newSerializer();
+ int i, stopIndex;
+ if(offset > subFolders.size())
+ throw new IllegalArgumentException("FolderListingEncode: offset > subFolders.size()");
+
+ stopIndex = offset + count;
+ if(stopIndex > subFolders.size())
+ stopIndex = subFolders.size();
+
+ try {
+ xmlMsgElement.setOutput(sw);
+ xmlMsgElement.startDocument(null, null);
+ xmlMsgElement.text("\n");
+ xmlMsgElement.startTag("", "folder-listing");
+ xmlMsgElement.attribute("", "version", "1.0");
+ for(i = offset; i<stopIndex; i++)
+ {
+ xmlMsgElement.startTag("", "folder");
+ xmlMsgElement.attribute("", "name", subFolders.get(i).getName());
+ xmlMsgElement.endTag("", "folder");
+ }
+ xmlMsgElement.endTag("", "folder-listing");
+ xmlMsgElement.endDocument();
+ } catch (IllegalArgumentException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return sw.toString().getBytes("UTF-8");
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListing.java b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java
new file mode 100644
index 000000000..0e5ba97d7
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java
@@ -0,0 +1,92 @@
+/*
+* 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 java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import android.util.Log;
+import android.util.Xml;
+
+public class BluetoothMapMessageListing {
+
+ private static final String TAG = "BluetoothMapMessageListing";
+ private List<BluetoothMapMessageListingElement> list;
+
+ public BluetoothMapMessageListing(){
+ list = new ArrayList<BluetoothMapMessageListingElement>();
+ }
+ public void add(BluetoothMapMessageListingElement element) {
+ list.add(element);
+ }
+
+ /**
+ * Used to fetch the number of BluetoothMapMessageListingElement elements in the list.
+ * @return the number of elements in the list.
+ */
+ public int getCount() {
+ return list.size();
+ }
+ /**
+ * Encode the list of BluetoothMapMessageListingElement(s) into a UTF-8
+ * formatted XML-string in a trimmed byte array
+ *
+ * @return a reference to the encoded byte array.
+ * @throws UnsupportedEncodingException
+ * if UTF-8 encoding is unsupported on the platform.
+ */
+ public byte[] encode() throws UnsupportedEncodingException {
+ StringWriter sw = new StringWriter();
+ XmlSerializer xmlMsgElement = Xml.newSerializer();
+ try {
+ xmlMsgElement.setOutput(sw);
+ xmlMsgElement.startDocument(null, null);
+ xmlMsgElement.startTag("", "MAP-msg-listing");
+ xmlMsgElement.attribute("", "version", "1.0");
+ // Do the XML encoding of list
+ for (BluetoothMapMessageListingElement element : list) {
+ element.encode(xmlMsgElement); // Append the list element
+ }
+ xmlMsgElement.endTag("", "MAP-msg-listing");
+ xmlMsgElement.endDocument();
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, e.toString());
+ } catch (IllegalStateException e) {
+ Log.w(TAG, e.toString());
+ } catch (IOException e) {
+ Log.w(TAG, e.toString());
+ }
+ return sw.toString().getBytes("UTF-8");
+ }
+
+ public void sort() {
+ Collections.sort(list);
+ }
+
+ public void segment(int count, int offset) {
+ count = Math.min(count, list.size());
+ if (offset + count <= list.size()) {
+ list = list.subList(offset, offset + count);
+ } else {
+ list = null;
+ }
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java
new file mode 100644
index 000000000..1870486bf
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java
@@ -0,0 +1,256 @@
+/*
+* 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 java.io.IOException;
+import java.io.StringWriter;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
+public class BluetoothMapMessageListingElement
+ implements Comparable<BluetoothMapMessageListingElement> {
+
+ private static final String TAG = "BluetoothMapMessageListingElement";
+ private static final boolean D = true;
+ private static final boolean V = true;
+
+ private long handle = 0;
+ private String subject = null;
+ private long dateTime = 0;
+ private String senderName = null;
+ private String senderAddressing = null;
+ private String replytoAddressing = null;
+ private String recipientName = null;
+ private String recipientAddressing = null;
+ private TYPE type = null;
+ private int size = -1;
+ private String text = null;
+ private String receptionStatus = null;
+ private int attachmentSize = -1;
+ private String priority = null;
+ private String read = null;
+ private String sent = null;
+ private String protect = null;
+
+ public long getHandle() {
+ return handle;
+ }
+
+ public void setHandle(long handle) {
+ this.handle = handle;
+ }
+
+ public long getDateTime() {
+ return dateTime;
+ }
+
+ public String getDateTimeString() {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = new Date(dateTime);
+ return format.format(date); // Format to YYYYMMDDTHHMMSS local time
+ }
+
+ public void setDateTime(long dateTime) {
+ this.dateTime = dateTime;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public String getSenderName() {
+ return senderName;
+ }
+
+ public void setSenderName(String senderName) {
+ this.senderName = senderName;
+ }
+
+ public String getSenderAddressing() {
+ return senderAddressing;
+ }
+
+ public void setSenderAddressing(String senderAddressing) {
+ /* TODO: This should depend on the type - for email, the addressing is an email address
+ * Consider removing this again - to allow strings.
+ */
+ this.senderAddressing = PhoneNumberUtils.extractNetworkPortion(senderAddressing);
+ if(this.senderAddressing == null || this.senderAddressing.length() < 2){
+ this.senderAddressing = "11"; // Ensure we have at least two digits to
+ }
+ }
+
+ public String getReplyToAddressing() {
+ return replytoAddressing;
+ }
+
+ public void setReplytoAddressing(String replytoAddressing) {
+ this.replytoAddressing = replytoAddressing;
+ }
+
+ public String getRecipientName() {
+ return recipientName;
+ }
+
+ public void setRecipientName(String recipientName) {
+ this.recipientName = recipientName;
+ }
+
+ public String getRecipientAddressing() {
+ return recipientAddressing;
+ }
+
+ public void setRecipientAddressing(String recipientAddressing) {
+ this.recipientAddressing = recipientAddressing;
+ }
+
+ public TYPE getType() {
+ return type;
+ }
+
+ public void setType(TYPE type) {
+ this.type = type;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public void setSize(int size) {
+ this.size = size;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public String getReceptionStatus() {
+ return receptionStatus;
+ }
+
+ public void setReceptionStatus(String receptionStatus) {
+ this.receptionStatus = receptionStatus;
+ }
+
+ public int getAttachmentSize() {
+ return attachmentSize;
+ }
+
+ public void setAttachmentSize(int attachmentSize) {
+ this.attachmentSize = attachmentSize;
+ }
+
+ public String getPriority() {
+ return priority;
+ }
+
+ public void setPriority(String priority) {
+ this.priority = priority;
+ }
+
+ public String getRead() {
+ return read;
+ }
+
+ public void setRead(String read) {
+ this.read = read;
+ }
+
+ public String getSent() {
+ return sent;
+ }
+
+ public void setSent(String sent) {
+ this.sent = sent;
+ }
+
+ public String getProtect() {
+ return protect;
+ }
+
+ public void setProtect(String protect) {
+ this.protect = protect;
+ }
+
+ public int compareTo(BluetoothMapMessageListingElement e) {
+ if (this.dateTime < e.dateTime) {
+ return 1;
+ } else if (this.dateTime > e.dateTime) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+
+ /* Encode the MapMessageListingElement into the StringBuilder reference.
+ * */
+ public void encode(XmlSerializer xmlMsgElement) throws IllegalArgumentException,
+ IllegalStateException, IOException
+ {
+ // contruct the XML tag for a single msg in the msglisting
+ xmlMsgElement.startTag("", "msg");
+ xmlMsgElement.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, type));
+ xmlMsgElement.attribute("", "subject", subject);
+ xmlMsgElement.attribute("", "datetime", this.getDateTimeString());
+ if (senderName != null)
+ xmlMsgElement.attribute("", "sender_name", senderName);
+ if (senderAddressing != null)
+ xmlMsgElement.attribute("", "sender_addressing", senderAddressing);
+ if (replytoAddressing != null)
+ xmlMsgElement.attribute("", "replyto_addressing",replytoAddressing);
+ if (recipientName != null)
+ xmlMsgElement.attribute("", "recipient_name",recipientName);
+ if (recipientAddressing != null)
+ xmlMsgElement.attribute("", "recipient_addressing", recipientAddressing);
+ if (type != null)
+ xmlMsgElement.attribute("", "type", type.name());
+ if (size != -1)
+ xmlMsgElement.attribute("", "size", Integer.toString(size));
+ if (text != null)
+ xmlMsgElement.attribute("", "text", text);
+ if (receptionStatus != null)
+ xmlMsgElement.attribute("", "reception_status", receptionStatus);
+ if (attachmentSize != -1)
+ xmlMsgElement.attribute("", "attachment_size", Integer.toString(attachmentSize));
+ if (priority != null)
+ xmlMsgElement.attribute("", "priority", priority);
+ if (read != null)
+ xmlMsgElement.attribute("", "read", read);
+ if (sent != null)
+ xmlMsgElement.attribute("", "sent", sent);
+ if (protect != null)
+ xmlMsgElement.attribute("", "protect", protect);
+ xmlMsgElement.endTag("", "msg");
+ }
+}
+
+
diff --git a/src/com/android/bluetooth/map/BluetoothMapObexServer.java b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
new file mode 100644
index 000000000..c1a82c2d4
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
@@ -0,0 +1,695 @@
+/*
+* 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 java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Calendar;
+
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerRequestHandler;
+
+import com.android.bluetooth.map.BluetoothMapUtils;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+public class BluetoothMapObexServer extends ServerRequestHandler {
+
+ private static final String TAG = "BluetoothMapObexServer";
+
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ private static final int UUID_LENGTH = 16;
+
+ // 128 bit UUID for MAP
+ private static final byte[] MAP_TARGET = new byte[] {
+ (byte)0xBB, (byte)0x58, (byte)0x2B, (byte)0x40,
+ (byte)0x42, (byte)0x0C, (byte)0x11, (byte)0xDB,
+ (byte)0xB0, (byte)0xDE, (byte)0x08, (byte)0x00,
+ (byte)0x20, (byte)0x0C, (byte)0x9A, (byte)0x66
+ };
+
+ /* Message types */
+ private static final String TYPE_GET_FOLDER_LISTING = "x-obex/folder-listing";
+ private static final String TYPE_GET_MESSAGE_LISTING = "x-bt/MAP-msg-listing";
+ private static final String TYPE_MESSAGE = "x-bt/message";
+ private static final String TYPE_SET_MESSAGE_STATUS = "x-bt/messageStatus";
+ private static final String TYPE_SET_NOTIFICATION_REGISTRATION = "x-bt/MAP-NotificationRegistration";
+ private static final String TYPE_SEND_EVENT = "x-bt/MAP-event-report";
+ private static final String TYPE_MESSAGE_UPDATE = "x-bt/MAP-messageUpdate";
+
+ private BluetoothMapFolderElement mCurrentFolder;
+
+ private Handler mCallback = null;
+
+ private Context mContext;
+
+ public static boolean sIsAborted = false;
+
+ BluetoothMapContent mOutContent;
+
+ public BluetoothMapObexServer(Handler callback, Context context) {
+ super();
+ mCallback = callback;
+ mContext = context;
+ mOutContent = new BluetoothMapContent(mContext);
+
+ buildFolderStructure(); /* Build the default folder structure, and set
+ mCurrentFolder to root folder */
+ }
+
+ /**
+ * Build the default minimal folder structure, as defined in the MAP specification.
+ */
+ private void buildFolderStructure(){
+ mCurrentFolder = new BluetoothMapFolderElement("root", null); // This will be the root element
+ BluetoothMapFolderElement tmpFolder;
+ tmpFolder = mCurrentFolder.addFolder("telecom"); // root/telecom
+ tmpFolder = tmpFolder.addFolder("msg"); // root/telecom/msg
+ tmpFolder.addFolder("inbox"); // root/telecom/msg/inbox
+ tmpFolder.addFolder("outbox");
+ tmpFolder.addFolder("sent");
+ tmpFolder.addFolder("deleted");
+ tmpFolder.addFolder("draft");
+ }
+
+ @Override
+ public int onConnect(final HeaderSet request, HeaderSet reply) {
+ if (D) Log.d(TAG, "onConnect():");
+ if (V) logHeader(request);
+ try {
+ byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET);
+ if (uuid == null) {
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+ if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid));
+
+ if (uuid.length != UUID_LENGTH) {
+ Log.w(TAG, "Wrong UUID length");
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+ for (int i = 0; i < UUID_LENGTH; i++) {
+ if (uuid[i] != MAP_TARGET[i]) {
+ Log.w(TAG, "Wrong UUID");
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+ }
+ reply.setHeader(HeaderSet.WHO, uuid);
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+ }
+
+ try {
+ byte[] remote = (byte[])request.getHeader(HeaderSet.WHO);
+ if (remote != null) {
+ if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote));
+ reply.setHeader(HeaderSet.TARGET, remote);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+ }
+
+ if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " +
+ "MSG_SESSION_ESTABLISHED msg.");
+
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_SESSION_ESTABLISHED;
+ msg.sendToTarget();
+
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ @Override
+ public void onDisconnect(final HeaderSet req, final HeaderSet resp) {
+ if (D) Log.d(TAG, "onDisconnect(): enter");
+ if (V) logHeader(req);
+
+ resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
+ if (mCallback != null) {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_SESSION_DISCONNECTED;
+ msg.sendToTarget();
+ if (V) Log.v(TAG, "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out.");
+ }
+ }
+
+ @Override
+ public int onAbort(HeaderSet request, HeaderSet reply) {
+ if (D) Log.d(TAG, "onAbort(): enter.");
+ sIsAborted = true;
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ @Override
+ public int onPut(final Operation op) {
+ if (D) Log.d(TAG, "onPut(): enter");
+ HeaderSet request = null;
+ String type, name;
+ byte[] appParamRaw;
+ BluetoothMapAppParams appParams = null;
+
+ try {
+ request = op.getReceivedHeader();
+ type = (String)request.getHeader(HeaderSet.TYPE);
+ name = (String)request.getHeader(HeaderSet.NAME);
+ appParamRaw = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER);
+ if(appParamRaw != null)
+ appParams = new BluetoothMapAppParams(appParamRaw);
+ } catch (Exception e) {
+ Log.e(TAG, "request headers error");
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ if(D) Log.d(TAG,"type = " + type + ", name = " + name);
+ if (type.equals(TYPE_MESSAGE_UPDATE)) {
+ if(V) {
+ Log.d(TAG,"TYPE_MESSAGE_UPDATE:");
+ }
+ return ResponseCodes.OBEX_HTTP_OK;
+ }else if(type.equals(TYPE_SET_NOTIFICATION_REGISTRATION)) {
+ if(V) {
+ Log.d(TAG,"TYPE_SET_NOTIFICATION_REGISTRATION: NotificationStatus: " + appParams.getNotificationStatus());
+ }
+ return setNotificationRegistration(appParams);
+ }else if(type.equals(TYPE_SET_MESSAGE_STATUS)) {
+ if(V) {
+ Log.d(TAG,"TYPE_SET_MESSAGE_STATUS: StatusIndicator: " + appParams.getStatusIndicator() + ", StatusValue: " + appParams.getStatusValue());
+ }
+ return setMessageStatus(name, appParams);
+ } else if (type.equals(TYPE_MESSAGE)) {
+ if(V) {
+ Log.d(TAG,"TYPE_MESSAGE: Transparet: " + appParams.getTransparent() + ", Retry: " + appParams.getRetry());
+ Log.d(TAG," charset: " + appParams.getCharset());
+ }
+ return pushMessage(op, name, appParams);
+
+ }
+
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ private int setNotificationRegistration(BluetoothMapAppParams appParams) {
+ // Forward the request to the MNS thread as a message - including the MAS instance ID.
+ Handler mns = BluetoothMnsObexClient.getMessageHandler();
+ if(mns != null) {
+ Message msg = Message.obtain(mns);
+ msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
+ msg.arg1 = 0; // TODO: Add correct MAS ID, as specified in the SDP record.
+ msg.arg2 = appParams.getNotificationStatus();
+ msg.sendToTarget();
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // This should not happen.
+ }
+ }
+
+ private int pushMessage(final Operation op, String folderName, BluetoothMapAppParams appParams) {
+ if(appParams.getCharset() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
+ if(D) Log.d(TAG, "Missing charset - unable to decode message content. appParams.getCharset() = " + appParams.getCharset());
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ }
+ try {
+ if(folderName == null || folderName.equals("")) {
+ folderName = mCurrentFolder.getName();
+ }
+ if(!folderName.equals("outbox") && !folderName.equals("draft")) {
+ if(D) Log.d(TAG, "Push message only allowed to outbox and draft. folderName: " + folderName);
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+ /* TODO:
+ * - Read out the message - OK
+ * - Decode into a bMessage - OK
+ * - push to draft or send.
+ */
+ InputStream bMsgStream;
+ BluetoothMapbMessage message;
+ bMsgStream = op.openInputStream();
+ message = BluetoothMapbMessage.parse(bMsgStream, appParams.getCharset()); // Decode the messageBody
+ // Send message
+ BluetoothMapContentObserver observer = BluetoothMnsObexClient.getContentObserver();
+ if (observer == null) {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
+ }
+
+ long handle = observer.pushMessage(message, folderName, appParams);
+ if (D) Log.d(TAG, "pushMessage handle: " + handle);
+ if (handle < 0) {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
+ }
+ HeaderSet replyHeaders = new HeaderSet();
+ String handleStr = BluetoothMapUtils.getMapHandle(handle, message.getType());
+ if (D) Log.d(TAG, "handleStr: " + handleStr + " message.getType(): " + message.getType());
+ replyHeaders.setHeader(HeaderSet.NAME, handleStr);
+ op.sendHeaders(replyHeaders);
+ } catch (IllegalArgumentException e) {
+ if(D) Log.w(TAG, "Wrongly formatted bMessage received", e);
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ } catch (Exception e) {
+ // TODO: Change to IOException after debug
+ Log.e(TAG, "Exception occured: ", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ private int setMessageStatus(String msgHandle, BluetoothMapAppParams appParams) {
+ int indicator = appParams.getStatusIndicator();
+ int value = appParams.getStatusValue();
+ if(indicator == BluetoothMapAppParams.INVALID_VALUE_PARAMETER ||
+ value == BluetoothMapAppParams.INVALID_VALUE_PARAMETER ||
+ msgHandle == null) {
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ }
+ BluetoothMapContentObserver observer = BluetoothMnsObexClient.getContentObserver();
+ if (observer == null) {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
+ }
+
+ long handle = BluetoothMapUtils.getCpHandle(msgHandle);
+ BluetoothMapUtils.TYPE msgType = BluetoothMapUtils.getMsgTypeFromHandle(msgHandle);
+ if( indicator == BluetoothMapAppParams.STATUS_INDICATOR_DELETED) {
+ if (!observer.setMessageStatusDeleted(handle, msgType, value)) {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }
+ } else /* BluetoothMapAppParams.STATUS_INDICATOR_READE */ {
+ if (!observer.setMessageStatusRead(handle, msgType, value)) {
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }
+ }
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ @Override
+ public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup,
+ final boolean create) {
+ String folderName;
+ BluetoothMapFolderElement folder;
+ try {
+ folderName = (String)request.getHeader(HeaderSet.NAME);
+ } catch (Exception e) {
+ Log.e(TAG, "request headers error");
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ if (V) logHeader(request);
+ if (D) Log.d(TAG, "onSetPath name is " + folderName + " backup: " + backup
+ + "create: " + create);
+
+ if(backup == true){
+ if(mCurrentFolder.getParent() != null)
+ mCurrentFolder = mCurrentFolder.getParent();
+ else
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ if (folderName == null || folderName == "") {
+ if(backup == false)
+ mCurrentFolder = mCurrentFolder.getRoot();
+ }
+ else {
+ folder = mCurrentFolder.getSubFolder(folderName);
+ if(folder != null)
+ mCurrentFolder = folder;
+ else
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+ if (V) Log.d(TAG, "Current Folder: " + mCurrentFolder.getName());
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ @Override
+ public void onClose() {
+ if (mCallback != null) {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_SERVERSESSION_CLOSE;
+ msg.sendToTarget();
+ if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out.");
+ }
+ }
+
+ @Override
+ public int onGet(Operation op) {
+ sIsAborted = false;
+ HeaderSet request;
+ String type;
+ String name;
+ byte[] appParamRaw = null;
+ BluetoothMapAppParams appParams = null;
+ try {
+ request = op.getReceivedHeader();
+ type = (String)request.getHeader(HeaderSet.TYPE);
+ name = (String)request.getHeader(HeaderSet.NAME);
+ appParamRaw = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER);
+ if(appParamRaw != null)
+ appParams = new BluetoothMapAppParams(appParamRaw);
+
+ if (V) logHeader(request);
+ if (D) Log.d(TAG, "OnGet type is " + type + " name is " + name);
+
+ if (type == null) {
+ if (V) Log.d(TAG, "type is null?" + type);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ if (type.equals(TYPE_GET_FOLDER_LISTING)) {
+ if (V && appParams != null) {
+ Log.d(TAG,"TYPE_GET_FOLDER_LISTING: MaxListCount = " + appParams.getMaxListCount() +
+ ", ListStartOffset = " + appParams.getStartOffset());
+ }
+ return sendFolderListingRsp(op, appParams); // Block until all packets have been send.
+ }
+ else if (type.equals(TYPE_GET_MESSAGE_LISTING)){
+ if (V && appParams != null) {
+ Log.d(TAG,"TYPE_GET_MESSAGE_LISTING: MaxListCount = " + appParams.getMaxListCount() +
+ ", ListStartOffset = " + appParams.getStartOffset());
+ Log.d(TAG,"SubjectLength = " + appParams.getSubjectLength() + ", ParameterMask = " +
+ appParams.getParameterMask());
+ Log.d(TAG,"FilterMessageType = " + appParams.getFilterMessageType() +
+ ", FilterPeriodBegin = " + appParams.getFilterPeriodBegin());
+ Log.d(TAG,"FilterPeriodEnd = " + appParams.getFilterPeriodBegin() +
+ ", FilterReadStatus = " + appParams.getFilterReadStatus());
+ Log.d(TAG,"FilterRecipient = " + appParams.getFilterRecipient() +
+ ", FilterOriginator = " + appParams.getFilterOriginator());
+ Log.d(TAG,"FilterPriority = " + appParams.getFilterPriority());
+ }
+ return sendMessageListingRsp(op, appParams, name); // Block until all packets have been send.
+ }
+ else if (type.equals(TYPE_MESSAGE)){
+ if (V && appParams != null) {
+ Log.d(TAG,"TYPE_MESSAGE (GET): Attachment = " + appParams.getAttachment() +
+ ", Charset = " + appParams.getCharset() +
+ ", FractionRequest = " + appParams.getFractionRequest());
+ }
+ return sendGetMessageRsp(op, name, appParams); // Block until all packets have been send.
+ }
+ else {
+ Log.w(TAG, "unknown type request: " + type);
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+ } catch (Exception e) {
+ // TODO: Move to the part that actually throws exceptions, and change to the correat exception type
+ Log.e(TAG, "request headers error, Exception:", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+ }
+
+ /**
+ * Generate and send the message listing response based on an application
+ * parameter header. This function call will block until complete or aborted
+ * by the peer. Fragmentation of packets larger than the obex packet size
+ * will be handled by this function.
+ *
+ * @param op
+ * The OBEX operation.
+ * @param appParams
+ * The application parameter header
+ * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or
+ * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error.
+ */
+ private int sendMessageListingRsp(Operation op, BluetoothMapAppParams appParams, String folderName){
+ OutputStream outStream = null;
+ byte[] outBytes = null;
+ int maxChunkSize, bytesToWrite, bytesWritten = 0, listSize;
+ HeaderSet replyHeaders = new HeaderSet();
+ BluetoothMapAppParams outAppParams = new BluetoothMapAppParams();
+ BluetoothMapMessageListing outList;
+ if (folderName == null) {
+ folderName = mCurrentFolder.getName();
+ }
+ if (appParams == null){
+ appParams = new BluetoothMapAppParams();
+ appParams.setMaxListCount(1024);
+ appParams.setStartOffset(0);
+ }
+
+ // TODO: Check to see if we only need to send the size - hence no need to encode.
+ try {
+ // Open the OBEX body stream
+ outStream = op.openOutputStream();
+
+ if(appParams.getMaxListCount() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
+ appParams.setMaxListCount(1024);
+
+ if(appParams.getStartOffset() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
+ appParams.setStartOffset(0);
+
+ if(appParams.getMaxListCount() != 0) {
+ outList = mOutContent.msgListing(folderName, appParams);
+ // Generate the byte stream
+ outAppParams.setMessageListingSize(outList.getCount());
+// if(outList.getCount() != 0) {
+ outBytes = outList.encode();
+// } else {
+// op.noBodyHeader();
+ // TODO: Remove after test
+ //Log.w(TAG,"sendMessageListingRsp: Empty list - sending OBEX_HTTP_BAD_REQUEST");
+ //return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+// }
+ }
+ else {
+ listSize = mOutContent.msgListingSize(folderName, appParams);
+ outAppParams.setMessageListingSize(listSize);
+ op.noBodyHeader();
+ }
+
+ // Build the application parameter header
+ outAppParams.setNewMessage(0); // TODO: set depending on new messages
+ outAppParams.setMseTime(Calendar.getInstance().getTime().getTime());
+
+ replyHeaders.setHeader(HeaderSet.APPLICATION_PARAMETER, outAppParams.EncodeParams());
+
+ op.sendHeaders(replyHeaders);
+
+ } catch (IOException e) {
+ Log.w(TAG,"sendMessageListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG,"sendMessageListingRsp: IllegalArgumentException - sending OBEX_HTTP_BAD_REQUEST", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } catch (Exception e){
+ Log.w(TAG, "Exception:", e);
+ // TODO: REMOVE AFTER TEST!!
+ }
+
+ maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers.
+ if (outBytes != null) {
+ try {
+ while (bytesWritten < outBytes.length && sIsAborted == false) {
+ bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
+ outStream.write(outBytes, bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ if(V) Log.w(TAG,e);
+ // We were probably aborted or disconnected
+ } catch (Exception e){
+ if(V) Log.w(TAG,e);
+ // TODO: REMOVE AFTER TEST!!
+ } finally {
+ if (outStream != null) {
+ try {
+ outStream.close();
+ } catch (IOException e) {
+ // If an error occurs during close, there is no more cleanup to do
+ }
+ }
+ }
+ if (bytesWritten != outBytes.length)
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } else {
+ try {
+ outStream.close();
+ } catch (IOException e) {
+ // If an error occurs during close, there is no more cleanup to do
+ }
+ }
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ /**
+ * Generate and send the Folder listing response based on an application
+ * parameter header. This function call will block until complete or aborted
+ * by the peer. Fragmentation of packets larger than the obex packet size
+ * will be handled by this function.
+ *
+ * @param op
+ * The OBEX operation.
+ * @param appParams
+ * The application parameter header
+ * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or
+ * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error.
+ */
+ private int sendFolderListingRsp(Operation op, BluetoothMapAppParams appParams){
+ OutputStream outStream = null;
+ byte[] outBytes = null;
+ BluetoothMapAppParams outAppParams = new BluetoothMapAppParams();
+ int maxChunkSize, bytesWritten = 0;
+ HeaderSet replyHeaders = new HeaderSet();
+ int bytesToWrite, maxListCount, listStartOffset;
+ if(appParams == null){
+ appParams = new BluetoothMapAppParams();
+ appParams.setMaxListCount(1024);
+ }
+
+ if(V)
+ Log.v(TAG,"sendFolderList for " + mCurrentFolder.getName());
+
+ try {
+ maxListCount = appParams.getMaxListCount();
+ listStartOffset = appParams.getStartOffset();
+
+ if(listStartOffset == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
+ listStartOffset = 0;
+
+ if(maxListCount == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
+ maxListCount = 1024;
+
+ if(maxListCount != 0)
+ {
+ outBytes = mCurrentFolder.encode(listStartOffset, maxListCount);
+ outStream = op.openOutputStream();
+ }
+
+ // Build and set the application parameter header
+ outAppParams.setFolderListingSize(mCurrentFolder.getSubFolderCount());
+ replyHeaders.setHeader(HeaderSet.APPLICATION_PARAMETER, outAppParams.EncodeParams());
+ op.sendHeaders(replyHeaders);
+
+ } catch (IOException e1) {
+ Log.w(TAG,"sendFolderListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST Exception:", e1);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } catch (IllegalArgumentException e1) {
+ Log.w(TAG,"sendFolderListingRsp: IllegalArgumentException - sending OBEX_HTTP_BAD_REQUEST Exception:", e1);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers.
+
+ if(outBytes != null) {
+ try {
+ while (bytesWritten < outBytes.length && sIsAborted == false) {
+ bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
+ outStream.write(outBytes, bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ // We were probably aborted or disconnected
+ } finally {
+ if(outStream != null) {
+ try {
+ outStream.close();
+ } catch (IOException e) {
+ // If an error occurs during close, there is no more cleanup to do
+ }
+ }
+ }
+ if(V)
+ Log.v(TAG,"sendFolderList sent " + bytesWritten + " bytes out of "+ outBytes.length);
+ if(bytesWritten == outBytes.length)
+ return ResponseCodes.OBEX_HTTP_OK;
+ else
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+ /**
+ * Generate and send the Folder listing response based on an application
+ * parameter header. This function call will block until complete or aborted
+ * by the peer. Fragmentation of packets larger than the obex packet size
+ * will be handled by this function.
+ *
+ * @param op
+ * The OBEX operation.
+ * @param appParams
+ * The application parameter header
+ * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or
+ * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error.
+ */
+ private int sendGetMessageRsp(Operation op, String name, BluetoothMapAppParams appParams){
+ OutputStream outStream ;
+ byte[] outBytes;
+ int maxChunkSize, bytesToWrite, bytesWritten = 0;
+ long msgHandle;
+
+ try {
+ outBytes = mOutContent.getMessage(name, appParams.getCharset());
+ outStream = op.openOutputStream();
+
+ } catch (IOException e) {
+ Log.w(TAG,"sendGetMessageRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG,"sendGetMessageRsp: IllegalArgumentException (e.g. invalid handle) - sending OBEX_HTTP_BAD_REQUEST", e);
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers.
+
+ if(outBytes != null) {
+ try {
+ while (bytesWritten < outBytes.length && sIsAborted == false) {
+ bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
+ outStream.write(outBytes, bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ // We were probably aborted or disconnected
+ } finally {
+ if(outStream != null) {
+ try {
+ outStream.close();
+ } catch (IOException e) {
+ // If an error occurs during close, there is no more cleanup to do
+ }
+ }
+ }
+ if(bytesWritten == outBytes.length)
+ return ResponseCodes.OBEX_HTTP_OK;
+ else
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
+
+
+ public static final void logHeader(HeaderSet hs) {
+ Log.v(TAG, "Dumping HeaderSet " + hs.toString());
+ try {
+ Log.v(TAG, "CONNECTION_ID : " + hs.getHeader(HeaderSet.CONNECTION_ID));
+ Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME));
+ Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE));
+ Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET));
+ Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO));
+ Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER));
+ } catch (IOException e) {
+ Log.e(TAG, "dump HeaderSet error " + e);
+ }
+ Log.v(TAG, "NEW!!! Dumping HeaderSet END");
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapReceiver.java b/src/com/android/bluetooth/map/BluetoothMapReceiver.java
new file mode 100644
index 000000000..7363e0048
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapReceiver.java
@@ -0,0 +1,64 @@
+/*
+* 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.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BluetoothMapReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "BluetoothMapReceiver";
+
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (V) Log.v(TAG, "MapReceiver onReceive ");
+
+ Intent in = new Intent();
+ in.putExtras(intent);
+ in.setClass(context, BluetoothMapService.class);
+ String action = intent.getAction();
+ in.putExtra("action", action);
+ if (V) Log.v(TAG,"***********action = " + action);
+
+ boolean startService = true;
+ if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ in.putExtra(BluetoothAdapter.EXTRA_STATE, state);
+ if (V) Log.v(TAG,"***********state = " + state);
+ if ((state == BluetoothAdapter.STATE_TURNING_ON)
+ || (state == BluetoothAdapter.STATE_OFF)) {
+ //FIX: We turn on MAP after BluetoothAdapter.STATE_ON,
+ //but we turn off MAP right after BluetoothAdapter.STATE_TURNING_OFF
+ startService = false;
+ }
+ } else {
+ // Don't forward intent unless device has bluetooth and bluetooth is enabled.
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null || !adapter.isEnabled()) {
+ startService = false;
+ }
+ }
+ if (startService) {
+ if (V) Log.v(TAG,"***********Calling start service!!!! with action = " + in.getAction());
+ context.startService(in);
+ }
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java b/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java
new file mode 100644
index 000000000..90437d8f7
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java
@@ -0,0 +1,72 @@
+/*
+* 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.bluetooth.BluetoothSocket;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.obex.ObexTransport;
+
+public class BluetoothMapRfcommTransport implements ObexTransport {
+ private BluetoothSocket mSocket = null;
+
+ public BluetoothMapRfcommTransport(BluetoothSocket rfs) {
+ super();
+ this.mSocket = rfs;
+ }
+
+ public void close() throws IOException {
+ mSocket.close();
+ }
+
+ public DataInputStream openDataInputStream() throws IOException {
+ return new DataInputStream(openInputStream());
+ }
+
+ public DataOutputStream openDataOutputStream() throws IOException {
+ return new DataOutputStream(openOutputStream());
+ }
+
+ public InputStream openInputStream() throws IOException {
+ return mSocket.getInputStream();
+ }
+
+ public OutputStream openOutputStream() throws IOException {
+ return mSocket.getOutputStream();
+ }
+
+ public void connect() throws IOException {
+ }
+
+ public void create() throws IOException {
+ }
+
+ public void disconnect() throws IOException {
+ }
+
+ public void listen() throws IOException {
+ }
+
+ public boolean isConnected() throws IOException {
+ return true;
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapService.java b/src/com/android/bluetooth/map/BluetoothMapService.java
new file mode 100644
index 000000000..e4da10f11
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapService.java
@@ -0,0 +1,778 @@
+/*
+* 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 java.io.IOException;
+
+import javax.obex.ServerSession;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.IBluetooth;
+import android.bluetooth.IBluetoothMap;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.BluetoothMap;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+
+
+public class BluetoothMapService extends Service {
+ private static final String TAG = "BluetoothMapService";
+
+ /**
+ * To enable MAP DEBUG/VERBOSE logging - run below cmd in adb shell, and
+ * restart com.android.bluetooth process. only enable DEBUG log:
+ * "setprop log.tag.BluetoothMapService DEBUG"; enable both VERBOSE and
+ * DEBUG log: "setprop log.tag.BluetoothMapService VERBOSE"
+ */
+
+ public static final boolean DEBUG = true;
+
+ public static final boolean VERBOSE = true;
+
+ /**
+ * Intent indicating incoming obex authentication request which is from
+ * PCE(Carkit)
+ */
+ public static final String AUTH_CHALL_ACTION = "com.android.bluetooth.map.authchall";
+
+ /**
+ * Intent indicating obex session key input complete by user which is sent
+ * from BluetoothMapActivity
+ */
+ public static final String AUTH_RESPONSE_ACTION = "com.android.bluetooth.map.authresponse";
+
+ /**
+ * Intent indicating user canceled obex authentication session key input
+ * which is sent from BluetoothMapActivity
+ */
+ public static final String AUTH_CANCELLED_ACTION = "com.android.bluetooth.map.authcancelled";
+
+ /**
+ * Intent indicating timeout for user confirmation, which is sent to
+ * BluetoothMapActivity
+ */
+ public static final String USER_CONFIRM_TIMEOUT_ACTION =
+ "com.android.bluetooth.map.userconfirmtimeout";
+
+ /**
+ * Intent Extra name indicating session key which is sent from
+ * BluetoothMapActivity
+ */
+ public static final String EXTRA_SESSION_KEY = "com.android.bluetooth.map.sessionkey";
+
+ public static final String THIS_PACKAGE_NAME = "com.android.bluetooth";
+
+ public static final int MSG_SERVERSESSION_CLOSE = 5000;
+
+ public static final int MSG_SESSION_ESTABLISHED = 5001;
+
+ public static final int MSG_SESSION_DISCONNECTED = 5002;
+
+ public static final int MSG_OBEX_AUTH_CHALL = 5003;
+
+ private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
+
+ private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
+
+ private static final int START_LISTENER = 1;
+
+ private static final int USER_TIMEOUT = 2;
+
+ private static final int AUTH_TIMEOUT = 3;
+
+
+ private static final int USER_CONFIRM_TIMEOUT_VALUE = 30000;
+
+
+ // Ensure not conflict with Opp notification ID
+ private static final int NOTIFICATION_ID_ACCESS = -1000001;
+
+ private static final int NOTIFICATION_ID_AUTH = -1000002;
+
+ private PowerManager.WakeLock mWakeLock = null;
+
+ private BluetoothAdapter mAdapter;
+
+ private SocketAcceptThread mAcceptThread = null;
+
+ private BluetoothMapAuthenticator mAuth = null;
+
+ private BluetoothMapObexServer mMapServer;
+
+ private ServerSession mServerSession = null;
+
+ private BluetoothMnsObexClient mBluetoothMnsObexClient = null;
+
+ private BluetoothServerSocket mServerSocket = null;
+
+ private BluetoothSocket mConnSocket = null;
+
+ private BluetoothDevice mRemoteDevice = null;
+
+ private static String sLocalPhoneNum = null;
+
+ private static String sLocalPhoneName = null;
+
+ private static String sRemoteDeviceName = null;
+
+ private boolean mHasStarted = false;
+
+ private volatile boolean mInterrupted;
+
+ private int mState;
+
+ private int mStartId = -1;
+
+ //private IBluetooth mBluetoothService;
+
+ private boolean isWaitingAuthorization = false;
+
+ // package and class name to which we send intent to check phone book access permission
+ private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
+ private static final String ACCESS_AUTHORITY_CLASS =
+ "com.android.settings.bluetooth.BluetoothPermissionRequest";
+
+ public BluetoothMapService() {
+ mState = BluetoothMap.STATE_DISCONNECTED;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (VERBOSE) Log.v(TAG, "Map Service onCreate");
+
+ mInterrupted = false;
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (!mHasStarted) {
+ mHasStarted = true;
+ if (VERBOSE) Log.v(TAG, "Starting MAP service");
+
+ int state = mAdapter.getState();
+ if (state == BluetoothAdapter.STATE_ON) {
+ // start RFCOMM listener
+ mSessionStatusHandler.sendMessage(mSessionStatusHandler
+ .obtainMessage(START_LISTENER));
+ }
+ }
+ }
+ // incoming Start intent handler
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mStartId = startId;
+ if (mAdapter == null) {
+ Log.w(TAG, "Stopping BluetoothMapService: "
+ + "device does not have BT or device is not ready");
+ // Release all resources
+ closeService();
+ } else {
+ // No need to handle the null intent case, because we have
+ // all restart work done in onCreate()
+ if (intent != null) {
+ parseIntent(intent);
+ }
+ }
+ return START_NOT_STICKY;
+ }
+
+ // process the intent from receiver
+ private void parseIntent(final Intent intent) {
+ String action = intent.getStringExtra("action");
+ if (VERBOSE) Log.v(TAG, "action: " + action);
+
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (VERBOSE) Log.v(TAG, "state: " + state);
+
+ boolean removeTimeoutMsg = true;
+ // BT status have been changed check new state
+ if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+ if (state == BluetoothAdapter.STATE_TURNING_OFF) {
+ // Send any pending timeout now, as this service will be destroyed.
+ if (mSessionStatusHandler.hasMessages(USER_TIMEOUT)) {
+ Intent timeoutIntent =
+ new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
+ timeoutIntent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+ sendBroadcast(timeoutIntent, BLUETOOTH_ADMIN_PERM);
+ }
+ // Release all resources
+ closeService();
+ } else {
+ removeTimeoutMsg = false;
+ }
+ // Authorization answer intent
+ } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
+ if (!isWaitingAuthorization) {
+ // this reply is not for us
+ return;
+ }
+
+ isWaitingAuthorization = false;
+
+ if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
+ BluetoothDevice.CONNECTION_ACCESS_NO) ==
+ BluetoothDevice.CONNECTION_ACCESS_YES) {
+ //bluetooth connection accepted by user
+ if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
+ boolean result = mRemoteDevice.setTrust(true);
+ if (VERBOSE) Log.v(TAG, "setTrust() result=" + result);
+ }
+ try {
+ if (mConnSocket != null) {
+ // start obex server and rfcomm connection
+ startObexServerSession();
+ } else {
+ stopObexServerSession();
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "Caught the error: " + ex.toString());
+ }
+ } else {
+ stopObexServerSession();
+ }
+ } else if (action.equals(AUTH_RESPONSE_ACTION)) {
+ String sessionkey = intent.getStringExtra(EXTRA_SESSION_KEY);
+ //send auth request
+ notifyAuthKeyInput(sessionkey);
+ } else if (action.equals(AUTH_CANCELLED_ACTION)) {
+ //user cancelled auth request
+ notifyAuthCancelled();
+ } else {
+ removeTimeoutMsg = false;
+ }
+
+ if (removeTimeoutMsg) {
+ mSessionStatusHandler.removeMessages(USER_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (VERBOSE) Log.v(TAG, "Map Service onDestroy");
+
+ super.onDestroy();
+ setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ closeService();
+ if(mSessionStatusHandler != null) {
+ mSessionStatusHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (VERBOSE) Log.v(TAG, "Map Service onBind");
+ return mBinder;
+ }
+
+ private void startRfcommSocketListener() {
+ if (VERBOSE) Log.v(TAG, "Map Service startRfcommSocketListener");
+
+ if (mAcceptThread == null) {
+ mAcceptThread = new SocketAcceptThread();
+ mAcceptThread.setName("BluetoothMapAcceptThread");
+ mAcceptThread.start();
+ }
+ }
+
+ private final boolean initSocket() {
+ if (VERBOSE) Log.v(TAG, "Map Service initSocket");
+
+ boolean initSocketOK = true;
+ final int CREATE_RETRY_TIME = 10;
+
+ // It's possible that create will fail in some cases. retry for 10 times
+ for (int i = 0; i < CREATE_RETRY_TIME && !mInterrupted; i++) {
+ try {
+ // It is mandatory for PSE to support initiation of bonding and
+ // encryption.
+ mServerSocket = mAdapter.listenUsingEncryptedRfcommWithServiceRecord
+ ("OBEX Message Access Server", BluetoothUuid.MAP.getUuid());
+
+ } catch (IOException e) {
+ Log.e(TAG, "Error create RfcommServerSocket " + e.toString());
+ initSocketOK = false;
+ }
+ if (!initSocketOK) {
+ // Need to break out of this loop if BT is being turned off.
+ if (mAdapter == null) break;
+ int state = mAdapter.getState();
+ if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
+ (state != BluetoothAdapter.STATE_ON)) {
+ Log.w(TAG, "initServerSocket failed as BT is (being) turned off");
+ break;
+ }
+ synchronized (this) {
+ try {
+ if (VERBOSE) Log.v(TAG, "wait 300 ms");
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "socketAcceptThread thread was interrupted (3)");
+ mInterrupted = true;
+ }
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (initSocketOK) {
+ if (VERBOSE) Log.v(TAG, "Succeed to create listening socket ");
+
+ } else {
+ Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
+ }
+ return initSocketOK;
+ }
+
+ private final void closeSocket(boolean server, boolean accept) throws IOException {
+ if (server == true) {
+ // Stop the possible trying to init serverSocket
+ mInterrupted = true;
+
+ if (mServerSocket != null) {
+ mServerSocket.close();
+ mServerSocket = null;
+ }
+ }
+
+ if (accept == true) {
+ if (mConnSocket != null) {
+ mConnSocket.close();
+ mConnSocket = null;
+ }
+ }
+ }
+
+ private final void closeService() {
+ if (VERBOSE) Log.v(TAG, "Map Service closeService in");
+
+ try {
+ closeSocket(true, true);
+ } catch (IOException ex) {
+ Log.e(TAG, "CloseSocket error: " + ex);
+ }
+
+ if (mAcceptThread != null) {
+ try {
+ mAcceptThread.shutdown();
+ mAcceptThread.join();
+ mAcceptThread = null;
+ } catch (InterruptedException ex) {
+ Log.w(TAG, "mAcceptThread close error", ex);
+ }
+ }
+ if (mServerSession != null) {
+ mServerSession.close();
+ mServerSession = null;
+ }
+ if (mBluetoothMnsObexClient != null) {
+ try {
+ mBluetoothMnsObexClient.interrupt();
+ mBluetoothMnsObexClient.join();
+ mBluetoothMnsObexClient = null;
+ } catch (InterruptedException ex) {
+ Log.w(TAG, "mBluetoothMnsObexClient close error", ex);
+ }
+ }
+// mBluetoothMnsObexClient.shutdown
+
+ mHasStarted = false;
+ if (mStartId != -1 && stopSelfResult(mStartId)) {
+ if (VERBOSE) Log.v(TAG, "successfully stopped map service");
+ mStartId = -1;
+ }
+ if (VERBOSE) Log.v(TAG, "Map Service closeService out");
+ }
+
+ private final void startObexServerSession() throws IOException {
+ if (VERBOSE) Log.v(TAG, "Map Service startObexServerSession");
+
+ // acquire the wakeLock before start Obex transaction thread
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "StartingObexMapTransaction");
+ mWakeLock.setReferenceCounted(false);
+ mWakeLock.acquire();
+ }
+
+
+ mMapServer = new BluetoothMapObexServer(mSessionStatusHandler, this);
+ synchronized (this) {
+ // We need to get authentication now that obex server is up
+ mAuth = new BluetoothMapAuthenticator(mSessionStatusHandler);
+ mAuth.setChallenged(false);
+ mAuth.setCancelled(false);
+ }
+ // setup RFCOMM transport
+ BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket);
+ mServerSession = new ServerSession(transport, mMapServer, mAuth);
+ mBluetoothMnsObexClient = new BluetoothMnsObexClient(this, mRemoteDevice);
+ mBluetoothMnsObexClient.start(); // Initiate the MNS message loop.
+ setState(BluetoothMap.STATE_CONNECTED);
+ if (VERBOSE) {
+ Log.v(TAG, "startObexServerSession() success!");
+ }
+ }
+
+ private void stopObexServerSession() {
+ if (VERBOSE) Log.v(TAG, "Map Service stopObexServerSession");
+
+ // Release the wake lock if obex transaction is over
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+
+ if (mServerSession != null) {
+ mServerSession.close();
+ mServerSession = null;
+ }
+
+ mAcceptThread = null;
+
+ if(mBluetoothMnsObexClient != null) {
+ mBluetoothMnsObexClient.disconnect();
+ mBluetoothMnsObexClient = null;
+ }
+
+ try {
+ closeSocket(false, true);
+ mConnSocket = null;
+ } catch (IOException e) {
+ Log.e(TAG, "closeSocket error: " + e.toString());
+ }
+ // Last obex transaction is finished, we start to listen for incoming
+ // connection again
+ if (mAdapter.isEnabled()) {
+ startRfcommSocketListener();
+ }
+ setState(BluetoothMap.STATE_DISCONNECTED);
+ }
+
+ private void notifyAuthKeyInput(final String key) {
+ synchronized (mAuth) {
+ if (key != null) {
+ mAuth.setSessionKey(key);
+ }
+ mAuth.setChallenged(true);
+ mAuth.notify();
+ }
+ }
+
+ private void notifyAuthCancelled() {
+ synchronized (mAuth) {
+ mAuth.setCancelled(true);
+ mAuth.notify();
+ }
+ }
+
+ /**
+ * A thread that runs in the background waiting for remote rfcomm
+ * connect.Once a remote socket connected, this thread shall be
+ * shutdown.When the remote disconnect,this thread shall run again waiting
+ * for next request.
+ */
+ private class SocketAcceptThread extends Thread {
+
+ private boolean stopped = false;
+
+ @Override
+ public void run() {
+ if (mServerSocket == null) {
+ if (!initSocket()) {
+ closeService();
+ return;
+ }
+ }
+
+ while (!stopped) {
+ try {
+ if (VERBOSE) Log.v(TAG, "Accepting socket connection...");
+ mConnSocket = mServerSocket.accept();
+ if (VERBOSE) Log.v(TAG, "Accepted socket connection...");
+
+ mRemoteDevice = mConnSocket.getRemoteDevice();
+ if (mRemoteDevice == null) {
+ Log.i(TAG, "getRemoteDevice() = null");
+ break;
+ }
+ sRemoteDeviceName = mRemoteDevice.getName();
+ // In case getRemoteName failed and return null
+ if (TextUtils.isEmpty(sRemoteDeviceName)) {
+ sRemoteDeviceName = getString(R.string.defaultname);
+ }
+ boolean trust = mRemoteDevice.getTrustState();
+ if (VERBOSE) Log.v(TAG, "GetTrustState() = " + trust);
+
+ if (trust) {
+ try {
+ if (VERBOSE) Log.v(TAG, "incoming connection accepted from: "
+ + sRemoteDeviceName + " automatically as trusted device");
+ startObexServerSession();
+ } catch (IOException ex) {
+ Log.e(TAG, "catch exception starting obex server session"
+ + ex.toString());
+ }
+ } else {
+ Intent intent = new
+ Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
+ intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+ intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
+ BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
+ intent.putExtra(BluetoothDevice.EXTRA_PACKAGE_NAME, getPackageName());
+ intent.putExtra(BluetoothDevice.EXTRA_CLASS_NAME,
+ BluetoothMapReceiver.class.getName());
+ sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
+ isWaitingAuthorization = true;
+
+ if (VERBOSE) Log.v(TAG, "waiting for authorization for connection from: "
+ + sRemoteDeviceName);
+
+ }
+ stopped = true; // job done ,close this thread;
+ } catch (IOException ex) {
+ stopped=true;
+ if (VERBOSE) Log.v(TAG, "Accept exception: " + ex.toString());
+ }
+ }
+ }
+
+ void shutdown() {
+ stopped = true;
+ interrupt();
+ }
+ }
+
+ private final Handler mSessionStatusHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what);
+
+ switch (msg.what) {
+ case START_LISTENER:
+ if (mAdapter.isEnabled()) {
+ startRfcommSocketListener();
+ } else {
+ closeService();// release all resources
+ }
+ break;
+ case USER_TIMEOUT:
+
+ Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
+ intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+ sendBroadcast(intent);
+ isWaitingAuthorization = false;
+ stopObexServerSession();
+ break;
+ case AUTH_TIMEOUT:
+ Intent i = new Intent(USER_CONFIRM_TIMEOUT_ACTION);
+ sendBroadcast(i);
+ removeMapNotification(NOTIFICATION_ID_AUTH);
+ notifyAuthCancelled();
+ break;
+ case MSG_SERVERSESSION_CLOSE:
+ stopObexServerSession();
+ break;
+ case MSG_SESSION_ESTABLISHED:
+ break;
+ case MSG_SESSION_DISCONNECTED:
+ // handled elsewhere
+ break;
+ case MSG_OBEX_AUTH_CHALL:
+ createMapNotification(AUTH_CHALL_ACTION);
+ mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler
+ .obtainMessage(AUTH_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ private void setState(int state) {
+ setState(state, BluetoothMap.RESULT_SUCCESS);
+ }
+
+ private synchronized void setState(int state, int result) {
+ if (state != mState) {
+ if (DEBUG) Log.d(TAG, "Map state " + mState + " -> " + state + ", result = "
+ + result);
+ int prevState = mState;
+ mState = state;
+ Intent intent = new Intent(BluetoothMap.MAP_STATE_CHANGED_ACTION);
+ intent.putExtra(BluetoothMap.MAP_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothMap.MAP_STATE, mState);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
+ sendBroadcast(intent, BLUETOOTH_PERM);
+ AdapterService s = AdapterService.getAdapterService();
+ if (s != null) {
+ s.onProfileConnectionStateChanged(mRemoteDevice, BluetoothProfile.MAP,
+ mState, prevState);
+ }
+ }
+ }
+
+ private void createMapNotification(String action) {
+
+ NotificationManager nm = (NotificationManager)
+ getSystemService(Context.NOTIFICATION_SERVICE);
+
+ // Create an intent triggered by clicking on the status icon.
+ Intent clickIntent = new Intent();
+ clickIntent.setClass(this, BluetoothMapActivity.class);
+ clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ clickIntent.setAction(action);
+
+ // Create an intent triggered by clicking on the
+ // "Clear All Notifications" button
+ Intent deleteIntent = new Intent();
+ deleteIntent.setClass(this, BluetoothMapReceiver.class);
+
+ Notification notification = null;
+ String name = getRemoteDeviceName();
+
+ if (action.equals(AUTH_CHALL_ACTION)) {
+ deleteIntent.setAction(AUTH_CANCELLED_ACTION);
+ notification = new Notification.Builder(this)
+ .setContentTitle(getString(R.string.auth_notif_title))
+ .setContentText(getString(R.string.auth_notif_message,name))
+ .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
+ .build();
+
+ notification.flags |= Notification.FLAG_AUTO_CANCEL;
+ notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;
+ notification.defaults = Notification.DEFAULT_SOUND;
+ notification.deleteIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, 0);
+ nm.notify(NOTIFICATION_ID_AUTH, notification);
+ }
+ }
+
+ private void removeMapNotification(int id) {
+ NotificationManager nm = (NotificationManager)
+ getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(id);
+ }
+
+ public static String getRemoteDeviceName() {
+ return sRemoteDeviceName;
+ }
+
+ /**
+ * Handlers for incoming service calls
+ */
+ private final IBluetoothMap.Stub mBinder = new IBluetoothMap.Stub() {
+ public int getState() {
+ if (DEBUG) Log.d(TAG, "getState " + mState);
+
+ if (!Utils.checkCaller()) {
+ Log.w(TAG,"getState(): not allowed for non-active user");
+ return BluetoothMap.STATE_DISCONNECTED;
+ }
+
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ return mState;
+ }
+
+ public BluetoothDevice getClient() {
+ if (DEBUG) Log.d(TAG, "getClient" + mRemoteDevice);
+
+ if (!Utils.checkCaller()) {
+ Log.w(TAG,"getClient(): not allowed for non-active user");
+ return null;
+ }
+
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ if (mState == BluetoothMap.STATE_DISCONNECTED) {
+ return null;
+ }
+ return mRemoteDevice;
+ }
+
+ public boolean isConnected(BluetoothDevice device) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG,"isConnected(): not allowed for non-active user");
+ return false;
+ }
+
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ return mState == BluetoothMap.STATE_CONNECTED && mRemoteDevice.equals(device);
+ }
+
+ public boolean connect(BluetoothDevice device) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG,"connect(): not allowed for non-active user");
+ return false;
+ }
+
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+ "Need BLUETOOTH_ADMIN permission");
+ return false;
+ }
+
+ public void disconnect() {
+ if (DEBUG) Log.d(TAG, "disconnect");
+
+ if (!Utils.checkCaller()) {
+ Log.w(TAG,"disconnect(): not allowed for non-active user");
+ return;
+ }
+
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+ "Need BLUETOOTH_ADMIN permission");
+ synchronized (BluetoothMapService.this) {
+ switch (mState) {
+ case BluetoothMap.STATE_CONNECTED:
+ if (mServerSession != null) {
+ mServerSession.close();
+ mServerSession = null;
+ }
+ try {
+ closeSocket(false, true);
+ mConnSocket = null;
+ } catch (IOException ex) {
+ Log.e(TAG, "Caught the error: " + ex);
+ }
+ setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ };
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java
new file mode 100644
index 000000000..7f87cd76b
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java
@@ -0,0 +1,721 @@
+/*
+* 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 static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
+import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Random;
+
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsMessage;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.telephony.*;
+/*import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsConstants;*/
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.SmsMessageBase.SubmitPduBase;
+import com.android.internal.telephony.cdma.sms.*;
+import com.android.internal.telephony.gsm.SmsMessage.SubmitPdu;
+
+public class BluetoothMapSmsPdu {
+
+ private static final String TAG = "BluetoothMapSmsPdu";
+ private static final boolean V = true;
+ private static int INVALID_VALUE = -1;
+ public static int SMS_TYPE_GSM = 1;
+ public static int SMS_TYPE_CDMA = 2;
+
+
+ /* TODO: We need to handle the SC-address mentioned in errata 4335.
+ * Since the definition could be read in three different ways, I have asked
+ * the car working group for clarification, and are awaiting confirmation that
+ * this clarification will go into the MAP spec:
+ * "The native format should be <sc_addr><tpdu> where <sc_addr> is <length><ton><1..10 octet of address>
+ * coded according to 24.011. The IEI is not to be used, as the fixed order of the data makes a type 4 LV
+ * information element sufficient. <length> is a single octet which value is the length of the value-field
+ * in octets including both the <ton> and the <address>."
+ * */
+
+
+ public static class SmsPdu {
+ private byte[] data;
+ private byte[] scAddress = {0}; // At the moment we do not use the scAddress, hence set the length to 0.
+ private int userDataMsgOffset = 0;
+ private int encoding;
+ private int languageTable;
+ private int languageShiftTable;
+ private int type;
+
+ /* Members used for pdu decoding */
+ private int userDataSeptetPadding = INVALID_VALUE;
+ private int msgSeptetCount = 0;
+
+ SmsPdu(byte[] data, int type){
+ this.data = data;
+ this.encoding = INVALID_VALUE;
+ this.type = type;
+ this.languageTable = INVALID_VALUE;
+ this.languageShiftTable = INVALID_VALUE;
+ this.userDataMsgOffset = gsmSubmitGetTpUdOffset(); // Assume no user data header
+ }
+
+ /**
+ * Create a pdu instance based on the data generated on this device.
+ * @param data
+ * @param encoding
+ * @param type
+ * @param languageTable
+ */
+ SmsPdu(byte[]data, int encoding, int type, int languageTable){
+ this.data = data;
+ this.encoding = encoding;
+ this.type = type;
+ this.languageTable = languageTable;
+ }
+ public byte[] getData(){
+ return data;
+ }
+ public byte[] getScAddress(){
+ return scAddress;
+ }
+ public void setEncoding(int encoding) {
+ this.encoding = encoding;
+ }
+ public int getEncoding(){
+ return encoding;
+ }
+ public int getType(){
+ return type;
+ }
+ public int getUserDataMsgOffset() {
+ return userDataMsgOffset;
+ }
+ /** The user data message payload size in bytes - excluding the user data header. */
+ public int getUserDataMsgSize() {
+ return data.length - userDataMsgOffset;
+ }
+
+ public int getLanguageShiftTable() {
+ return languageShiftTable;
+ }
+
+ public int getLanguageTable() {
+ return languageTable;
+ }
+
+ public int getUserDataSeptetPadding() {
+ return userDataSeptetPadding;
+ }
+
+ public int getMsgSeptetCount() {
+ return msgSeptetCount;
+ }
+
+
+ /* PDU parsing/modification functionality */
+ private final static byte TELESERVICE_IDENTIFIER = 0x00;
+ private final static byte SERVICE_CATEGORY = 0x01;
+ private final static byte ORIGINATING_ADDRESS = 0x02;
+ private final static byte ORIGINATING_SUB_ADDRESS = 0x03;
+ private final static byte DESTINATION_ADDRESS = 0x04;
+ private final static byte DESTINATION_SUB_ADDRESS = 0x05;
+ private final static byte BEARER_REPLY_OPTION = 0x06;
+ private final static byte CAUSE_CODES = 0x07;
+ private final static byte BEARER_DATA = 0x08;
+
+ /**
+ * Find and return the offset to the specified parameter ID
+ * @param parameterId The parameter ID to find
+ * @return the offset in number of bytes to the parameterID entry in the pdu data.
+ * The byte at the offset contains the parameter ID, the byte following contains the
+ * parameter length, and offset + 2 is the first byte of the parameter data.
+ */
+ private int cdmaGetParameterOffset(byte parameterId) {
+ ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+ int offset = 0;
+ boolean found = false;
+
+ try {
+ pdu.skip(1); // Skip the message type
+
+ while (pdu.available() > 0) {
+ int currentId = pdu.read();
+ int currentLen = pdu.read();
+
+ if(currentId == parameterId) {
+ found = true;
+ break;
+ }
+ else {
+ pdu.skip(currentLen);
+ offset += 2 + currentLen;
+ }
+ }
+ pdu.close();
+ } catch (Exception e) {
+ Log.e(TAG, "cdmaGetParameterOffset: ", e);
+ }
+
+ if(found)
+ return offset;
+ else
+ return 0;
+ }
+
+ private final static byte BEARER_DATA_MSG_ID = 0x00;
+
+ private int cdmaGetSubParameterOffset(byte subParameterId) {
+ ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+ int offset = 0;
+ boolean found = false;
+ offset = cdmaGetParameterOffset(BEARER_DATA) + 2; // Add to offset the BEARER_DATA parameter id and length bytes
+ pdu.skip(offset);
+ try {
+
+ while (pdu.available() > 0) {
+ int currentId = pdu.read();
+ int currentLen = pdu.read();
+
+ if(currentId == subParameterId) {
+ found = true;
+ break;
+ }
+ else {
+ pdu.skip(currentLen);
+ offset += 2 + currentLen;
+ }
+ }
+ pdu.close();
+ } catch (Exception e) {
+ Log.e(TAG, "cdmaGetParameterOffset: ", e);
+ }
+
+ if(found)
+ return offset;
+ else
+ return 0;
+ }
+
+
+ public void cdmaChangeToDeliverPdu(long date){
+ /* Things to change:
+ * - Message Type in bearer data (Not the overall point-to-point type)
+ * - Change address ID from destination to originating (sub addresses are not used)
+ * - A time stamp is not mandatory.
+ */
+ int offset;
+ offset = cdmaGetParameterOffset(DESTINATION_ADDRESS);
+ data[offset] = ORIGINATING_ADDRESS;
+ offset = cdmaGetParameterOffset(DESTINATION_SUB_ADDRESS);
+ data[offset] = ORIGINATING_SUB_ADDRESS;
+
+ offset = cdmaGetSubParameterOffset(BEARER_DATA_MSG_ID);
+
+// if(data != null && data.length > 2) {
+ int tmp = data[offset+2] & 0xff; // Skip the subParam ID and length, and read the first byte.
+ // Mask out the type
+ tmp &= 0x0f;
+ // Set the new type
+ tmp |= ((BearerData.MESSAGE_TYPE_DELIVER << 4) & 0xf0);
+ // Store the result
+ data[offset+2] = (byte) tmp;
+
+// }
+ //TODO: Error handling.
+ /* TODO: Do we need to change anything in the user data? Not sure if the user data is
+ * just encoded using GSM encoding, or it is an actual GSM submit PDU embedded
+ * in the user data?
+ */
+
+ }
+
+ private static final byte TP_MIT_DELIVER = 0x00; // bit 0 and 1
+ private static final byte TP_MMS_NO_MORE = 0x04; // bit 2
+ private static final byte TP_RP_NO_REPLY_PATH = 0x00; // bit 7
+ private static final byte TP_UDHI_MASK = 0x20; // bit 6
+ private static final byte TP_SRI_NO_REPORT = 0x00; // bit 5
+
+ private int gsmSubmitGetTpPidOffset() {
+ /* calculate the offset to TP_PID and return the TP_PID byte.
+ * The TP-DA has variable length, and the length excludes the 2 byte length and type headers.
+ * The TP-DA is two bytes within the PDU */
+ int offset = 2 + (data[2] & 0xff) + 2; //
+ if((offset > data.length) || (offset > (2 + 12))) // max length of TP_DA is 12 bytes + two byte offset
+ throw new IllegalArgumentException("wrongly formatted gsm submit PDU");
+ return offset;
+ }
+
+ public int gsmSubmitGetTpDcs() {
+ return data[gsmSubmitGetTpDcsOffset()] & 0xff;
+ }
+
+ public boolean gsmSubmitHasUserDataHeader() {
+ return ((data[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK;
+ }
+
+ private int gsmSubmitGetTpDcsOffset() {
+ return gsmSubmitGetTpPidOffset() + 1;
+ }
+
+ private int gsmSubmitGetTpUdlOffset() {
+ switch(((data[0] & 0xff) & (0x08 | 0x04))>>2) {
+ case 0: // Not TP-VP present
+ return gsmSubmitGetTpPidOffset() + 2;
+ case 1: // TP-VP relative format
+ return gsmSubmitGetTpPidOffset() + 2 + 1;
+ case 2: // TP-VP enhanced format
+ case 3: // TP-VP absolute format
+ break;
+ }
+ return gsmSubmitGetTpPidOffset() + 2 + 7;
+ }
+ private int gsmSubmitGetTpUdOffset() {
+ return gsmSubmitGetTpUdlOffset() + 1;
+ }
+
+ public void gsmDecodeUserDataHeader() {
+ ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+
+ pdu.skip(gsmSubmitGetTpUdlOffset());
+ int userDataLength = pdu.read();
+ int userDataHeaderLength = pdu.read();
+
+ // This part is only needed to extract the language info, hence only needed for 7 bit encoding
+ if(encoding == SmsConstants.ENCODING_7BIT)
+ {
+ byte[] udh = new byte[userDataHeaderLength];
+ try {
+ pdu.read(udh);
+ } catch (IOException e) {
+ Log.w(TAG, "unable to read userDataHeader", e);
+ }
+ SmsHeader userDataHeader = SmsHeader.fromByteArray(udh);
+ languageTable = userDataHeader.languageTable;
+ languageShiftTable = userDataHeader.languageShiftTable;
+
+ int headerBits = (userDataHeaderLength + 1) * 8;
+ int headerSeptets = headerBits / 7;
+ headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
+ userDataSeptetPadding = (headerSeptets * 7) - headerBits;
+ msgSeptetCount = userDataLength - headerSeptets;
+ }
+ userDataMsgOffset = gsmSubmitGetTpUdOffset() + userDataHeaderLength + 1; // Add the byte containing the length
+ }
+
+ private void gsmWriteDate(ByteArrayOutputStream header, long time) throws UnsupportedEncodingException {
+ SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss");
+ Date date = new Date(time);
+ String timeStr = format.format(date); // Format to YYMMDDTHHMMSS UTC time
+ byte[] timeChars = timeStr.getBytes("US-ASCII");
+
+ for(int i = 0, n = timeStr.length()/2; i < n; i++) {
+ header.write((timeChars[i+1]-0x30) << 4 | (timeChars[i]-0x30)); // Offset from ascii char to decimal value
+ }
+
+ Calendar cal = Calendar.getInstance();
+ int offset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / (15 * 60 * 1000); /* offset in quarters of an hour */
+ String offsetString;
+ if(offset < 0) {
+ offsetString = String.format("%1$02d", -(offset));
+ char[] offsetChars = offsetString.toCharArray();
+ header.write((offsetChars[1]-0x30) << 4 | 0x40 | (offsetChars[0]-0x30));
+ }
+ else {
+ offsetString = String.format("%1$02d", offset);
+ char[] offsetChars = offsetString.toCharArray();
+ header.write((offsetChars[1]-0x30) << 4 | (offsetChars[0]-0x30));
+ }
+ }
+
+/* private void gsmSubmitExtractUserData() {
+ int userDataLength = data[gsmSubmitGetTpUdlOffset()];
+ userData = new byte[userDataLength];
+ System.arraycopy(userData, 0, data, gsmSubmitGetTpUdOffset(), userDataLength);
+
+ }*/
+
+ /**
+ * Change the GSM Submit Pdu data in this object to a deliver PDU:
+ * - Build the new header with deliver PDU type, originator and time stamp.
+ * - Extract encoding details from the submit PDU
+ * - Extract user data length and user data from the submitPdu
+ * - Build the new PDU
+ * @param date the time stamp to include (The value is the number of milliseconds since Jan. 1, 1970 GMT.)
+ * @param originator the phone number to include in the deliver PDU header. Any undesired characters,
+ * such as '-' will be striped from this string.
+ */
+ public void gsmChangeToDeliverPdu(long date, String originator)
+ {
+ ByteArrayOutputStream newPdu = new ByteArrayOutputStream(22); // 22 is the max length of the deliver pdu header
+ byte[] encodedAddress;
+ int userDataLength = 0;
+ try {
+ newPdu.write(TP_MIT_DELIVER | TP_MMS_NO_MORE | TP_RP_NO_REPLY_PATH | TP_SRI_NO_REPORT
+ | (data[0] & 0xff) & TP_UDHI_MASK);
+ encodedAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(originator);
+ // Insert originator address into the header - this includes the length
+ newPdu.write(encodedAddress);
+ newPdu.write(data[gsmSubmitGetTpPidOffset()]);
+ newPdu.write(data[gsmSubmitGetTpDcsOffset()]);
+ // Generate service center time stamp
+ gsmWriteDate(newPdu, date);
+ userDataLength = (data[gsmSubmitGetTpUdlOffset()] & 0xff);
+ newPdu.write(userDataLength);
+ // Copy the pdu user data - keep in mind that the userDataLength is not the length in bytes for 7-bit encoding.
+ newPdu.write(data, gsmSubmitGetTpUdOffset(), data.length - gsmSubmitGetTpUdOffset());
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ throw new IllegalArgumentException("Failed to change type to deliver PDU."); // TODO: Is this the best way to handle this error? - which cannot occur...
+ }
+ data = newPdu.toByteArray();
+ }
+
+ /* SMS encoding to bmessage strings */
+ /** get the encoding type as a bMessage string */
+ public String getEncodingString(){
+ if(type == SMS_TYPE_GSM)
+ {
+ switch(encoding){
+ case SmsMessage.ENCODING_7BIT:
+ if(languageTable == 0)
+ return "G-7BIT";
+ else
+ return "G-7BITEXT";
+ case SmsMessage.ENCODING_8BIT:
+ return "G-8BIT";
+ case SmsMessage.ENCODING_16BIT:
+ return "G-16BIT";
+ case SmsMessage.ENCODING_UNKNOWN:
+ default:
+ return "";
+ }
+ } else /* SMS_TYPE_CDMA */ {
+ switch(encoding){
+ case SmsMessage.ENCODING_7BIT:
+ return "C-7ASCII";
+ case SmsMessage.ENCODING_8BIT:
+ return "C-8BIT";
+ case SmsMessage.ENCODING_16BIT:
+ return "C-UNICODE";
+ case SmsMessage.ENCODING_KSC5601:
+ return "C-KOREAN";
+ case SmsMessage.ENCODING_UNKNOWN:
+ default:
+ return "";
+ }
+ }
+ }
+ }
+
+ private static int sConcatenatedRef = new Random().nextInt(256);
+
+ protected static int getNextConcatenatedRef() {
+ sConcatenatedRef += 1;
+ return sConcatenatedRef;
+ }
+ public static ArrayList<SmsPdu> getSubmitPdus(String messageText, String address){
+ /* Use the generic GSM/CDMA SMS Message functionality within Android to generate the
+ * SMS PDU's as once generated to send the SMS message.
+ */
+
+ int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); // TODO: Change to use: ((TelephonyManager)myContext.getSystemService(Context.TELEPHONY_SERVICE))
+ int phoneType;
+ GsmAlphabet.TextEncodingDetails ted = (PHONE_TYPE_CDMA == activePhone) ?
+ com.android.internal.telephony.cdma.SmsMessage.calculateLength((CharSequence)messageText, false) :
+ com.android.internal.telephony.gsm.SmsMessage.calculateLength((CharSequence)messageText, false);
+
+ SmsPdu newPdu;
+ String destinationAddress;
+ int msgCount = ted.msgCount;
+ int encoding;
+ int languageTable;
+ int languageShiftTable;
+ int refNumber = getNextConcatenatedRef() & 0x00FF;
+ ArrayList<String> smsFragments = SmsMessage.fragmentText(messageText);
+ ArrayList<SmsPdu> pdus = new ArrayList<SmsPdu>(msgCount);
+ byte[] data;
+
+ // Default to GSM, as this code should not be used, if we neither have CDMA not GSM.
+ phoneType = (activePhone == PHONE_TYPE_CDMA) ? SMS_TYPE_CDMA : SMS_TYPE_GSM;
+ encoding = ted.codeUnitSize;
+ languageTable = ted.languageTable;
+ languageShiftTable = ted.languageShiftTable;
+ destinationAddress = PhoneNumberUtils.stripSeparators(address);
+ if(destinationAddress == null || destinationAddress.length() < 2) {
+ destinationAddress = "12"; // Ensure we add a number at least 2 digits as specified in the GSM spec.
+ }
+
+ if(msgCount == 1){
+ data = SmsMessage.getSubmitPdu(null, destinationAddress, smsFragments.get(0), false).encodedMessage;
+ newPdu = new SmsPdu(data, encoding, phoneType, languageTable);
+ pdus.add(newPdu);
+ }
+
+ /* This code is a reduced copy of the actual code used in the Android SMS sub system,
+ * hence the comments have been left untouched. */
+ for(int i = 0; i < msgCount; i++){
+ SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
+ concatRef.refNumber = refNumber;
+ concatRef.seqNumber = i + 1; // 1-based sequence
+ concatRef.msgCount = msgCount;
+ // TODO: We currently set this to true since our messaging app will never
+ // send more than 255 parts (it converts the message to MMS well before that).
+ // However, we should support 3rd party messaging apps that might need 16-bit
+ // references
+ // Note: It's not sufficient to just flip this bit to true; it will have
+ // ripple effects (several calculations assume 8-bit ref).
+ concatRef.isEightBits = true;
+ SmsHeader smsHeader = new SmsHeader();
+ smsHeader.concatRef = concatRef;
+
+ /* Depending on the type, call either GSM or CDMA getSubmitPdu(). The encoding
+ * will be determined(again) by getSubmitPdu().
+ * All packets need to be encoded using the same encoding, as the bMessage
+ * only have one filed to describe the encoding for all messages in a concatenated
+ * SMS... */
+ if (encoding == SmsConstants.ENCODING_7BIT) {
+ smsHeader.languageTable = languageTable;
+ smsHeader.languageShiftTable = languageShiftTable;
+ }
+
+ if(phoneType == SMS_TYPE_GSM){
+ data = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(null, destinationAddress,
+ smsFragments.get(i), false, SmsHeader.toByteArray(smsHeader),
+ encoding, languageTable, languageShiftTable).encodedMessage;
+ } else { // SMS_TYPE_CDMA
+ UserData uData = new UserData();
+ uData.payloadStr = smsFragments.get(i);
+ uData.userDataHeader = smsHeader;
+ if (encoding == SmsConstants.ENCODING_7BIT) {
+ uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
+ } else { // assume UTF-16
+ uData.msgEncoding = UserData.ENCODING_UNICODE_16;
+ }
+ uData.msgEncodingSet = true;
+ data = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(destinationAddress,
+ uData, false).encodedMessage;
+ }
+ newPdu = new SmsPdu(data, encoding, phoneType, languageTable);
+ pdus.add(newPdu);
+ }
+
+ return pdus;
+ }
+
+ /**
+ * Generate a list of deliver PDUs. The messageText and address parameters must be different from null,
+ * for CDMA the date can be omitted (and will be ignored if supplied)
+ * @param messageText The text to include.
+ * @param address The originator address.
+ * @param date The delivery time stamp.
+ * @return
+ */
+ public static ArrayList<SmsPdu> getDeliverPdus(String messageText, String address, long date){
+ ArrayList<SmsPdu> deliverPdus = getSubmitPdus(messageText, address);
+
+ /*
+ * For CDMA the only difference between deliver and submit pdus are the messageType,
+ * which is set in encodeMessageId, (the higher 4 bits of the 1st byte
+ * of the Message identification sub parameter data.) and the address type.
+ *
+ * For GSM, a larger part of the header needs to be generated.
+ */
+ for(SmsPdu currentPdu : deliverPdus){
+ if(currentPdu.getType() == SMS_TYPE_CDMA){
+ currentPdu.cdmaChangeToDeliverPdu(date);
+ } else { /* SMS_TYPE_GSM */
+ currentPdu.gsmChangeToDeliverPdu(date, address);
+ }
+ }
+
+ return deliverPdus;
+ }
+
+ public static void testSendRawPdu(SmsPdu pdu){
+ if(pdu.getType() == SMS_TYPE_CDMA){
+ /* TODO: Try to send the message using SmsManager.sendData()?*/
+ }else {
+
+ }
+ }
+
+ /**
+ * The decoding only supports decoding the actual textual content of the PDU received
+ * from the MAP client. (As the Android system has no interface to send pre encoded PDUs)
+ * The destination address must be extracted from the bmessage vCard(s).
+ */
+ public static String decodePdu(byte[] data, int type) {
+ String ret;
+ if(type == SMS_TYPE_CDMA) {
+ /* This is able to handle both submit and deliver PDUs */
+ ret = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord(0, data).getMessageBody();
+ } else {
+ /* For GSM, there is no submit pdu decoder, and most parser utils are private, and only minded for submit pdus */
+ ret = gsmParseSubmitPdu(data);
+ }
+ return ret;
+ }
+
+ /* At the moment we do not support using a SC-address. Use this function to strip off
+ * the SC-address before parsing it to the SmsPdu. (this was added in errata 4335)
+ */
+ private static byte[] gsmStripOffScAddress(byte[] data) {
+ /* The format of a native GSM SMS is: <sc-address><pdu> where sc-address is:
+ * <length-byte><type-byte><number-bytes> */
+ int addressLength = data[0] & 0xff; // Treat the byte value as an unsigned value
+ if(addressLength >= data.length) // TODO: We could verify that the address-length is no longer than 11 bytes
+ throw new IllegalArgumentException("Length of address exeeds the length of the PDU data.");
+ int pduLength = data.length-(1+addressLength);
+ byte[] newData = new byte[pduLength];
+ System.arraycopy(data, 1+addressLength, newData, 0, pduLength);
+ return newData;
+ }
+
+ private static String gsmParseSubmitPdu(byte[] data) {
+ /* Things to do:
+ * - extract hasUsrData bit
+ * - extract TP-DCS -> Character set, compressed etc.
+ * - extract user data header to get the language properties
+ * - extract user data
+ * - decode the string */
+ //Strip off the SC-address before parsing
+ SmsPdu pdu = new SmsPdu(gsmStripOffScAddress(data), SMS_TYPE_GSM);
+ boolean userDataCompressed = false;
+ int dataCodingScheme = pdu.gsmSubmitGetTpDcs();
+ int encodingType = SmsConstants.ENCODING_UNKNOWN;
+ String messageBody = null;
+
+ // Look up the data encoding scheme
+ if ((dataCodingScheme & 0x80) == 0) {
+ // Bits 7..4 == 0xxx
+ userDataCompressed = (0 != (dataCodingScheme & 0x20));
+
+ if (userDataCompressed) {
+ Log.w(TAG, "4 - Unsupported SMS data coding scheme "
+ + "(compression) " + (dataCodingScheme & 0xff));
+ } else {
+ switch ((dataCodingScheme >> 2) & 0x3) {
+ case 0: // GSM 7 bit default alphabet
+ encodingType = SmsConstants.ENCODING_7BIT;
+ break;
+
+ case 2: // UCS 2 (16bit)
+ encodingType = SmsConstants.ENCODING_16BIT;
+ break;
+
+ case 1: // 8 bit data
+ case 3: // reserved
+ Log.w(TAG, "1 - Unsupported SMS data coding scheme "
+ + (dataCodingScheme & 0xff));
+ encodingType = SmsConstants.ENCODING_8BIT;
+ break;
+ }
+ }
+ } else if ((dataCodingScheme & 0xf0) == 0xf0) {
+ userDataCompressed = false;
+
+ if (0 == (dataCodingScheme & 0x04)) {
+ // GSM 7 bit default alphabet
+ encodingType = SmsConstants.ENCODING_7BIT;
+ } else {
+ // 8 bit data
+ encodingType = SmsConstants.ENCODING_8BIT;
+ }
+ } else if ((dataCodingScheme & 0xF0) == 0xC0
+ || (dataCodingScheme & 0xF0) == 0xD0
+ || (dataCodingScheme & 0xF0) == 0xE0) {
+ // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
+
+ // 0xC0 == 7 bit, don't store
+ // 0xD0 == 7 bit, store
+ // 0xE0 == UCS-2, store
+
+ if ((dataCodingScheme & 0xF0) == 0xE0) {
+ encodingType = SmsConstants.ENCODING_16BIT;
+ } else {
+ encodingType = SmsConstants.ENCODING_7BIT;
+ }
+
+ userDataCompressed = false;
+
+ // bit 0x04 reserved
+ } else if ((dataCodingScheme & 0xC0) == 0x80) {
+ // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
+ // 0x80..0xBF == Reserved coding groups
+ if (dataCodingScheme == 0x84) {
+ // This value used for KSC5601 by carriers in Korea.
+ encodingType = SmsConstants.ENCODING_KSC5601;
+ } else {
+ Log.w(TAG, "5 - Unsupported SMS data coding scheme "
+ + (dataCodingScheme & 0xff));
+ }
+ } else {
+ Log.w(TAG, "3 - Unsupported SMS data coding scheme "
+ + (dataCodingScheme & 0xff));
+ }
+
+ /* TODO: This is NOT good design - to have the pdu class being depending on these two function calls.
+ * - move the encoding extraction into the pdu class */
+ pdu.setEncoding(encodingType);
+ if(pdu.gsmSubmitHasUserDataHeader()) {
+ pdu.gsmDecodeUserDataHeader();
+ }
+
+ try {
+ switch (encodingType) {
+ case SmsConstants.ENCODING_UNKNOWN:
+ case SmsConstants.ENCODING_8BIT:
+ messageBody = null;
+ break;
+
+ case SmsConstants.ENCODING_7BIT:
+ messageBody = GsmAlphabet.gsm7BitPackedToString(pdu.getData(), pdu.getUserDataMsgOffset(),
+ pdu.getMsgSeptetCount(), pdu.getUserDataSeptetPadding(), pdu.getLanguageTable(),
+ pdu.getLanguageShiftTable());
+
+ break;
+
+ case SmsConstants.ENCODING_16BIT:
+ messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "utf-16");
+ break;
+
+ case SmsConstants.ENCODING_KSC5601:
+ messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "KSC5601");
+
+ break;
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported encoding type???", e); // This should never happen.
+ return null;
+ }
+
+ return messageBody;
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapUtils.java b/src/com/android/bluetooth/map/BluetoothMapUtils.java
new file mode 100644
index 000000000..e57cf16b1
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapUtils.java
@@ -0,0 +1,116 @@
+/*
+* 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;
+
+
+/**
+ * Various utility methods and generic defines that can be used throughout MAPS
+ */
+public class BluetoothMapUtils {
+
+ private static final String TAG = "MapUtils";
+ private static final boolean V = BluetoothMapService.VERBOSE;
+ /* We use the upper 5 bits for the type mask - avoid using the top bit, since it
+ * indicates a negative value, hence corrupting the formatter when converting to
+ * type String. (I really miss the unsigned type in Java:))
+ */
+ private static final long HANDLE_TYPE_MASK = 0xf<<59;
+ private static final long HANDLE_TYPE_MMS_MASK = 0x1<<59;
+ private static final long HANDLE_TYPE_EMAIL_MASK = 0x2<<59;
+ private static final long HANDLE_TYPE_SMS_GSM_MASK = 0x4<<59;
+ private static final long HANDLE_TYPE_SMS_CDMA_MASK = 0x8<<59;
+
+ /**
+ * 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{
+ EMAIL,
+ SMS_GSM,
+ SMS_CDMA,
+ MMS
+ }
+
+ /**
+ * 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
+ */
+ static public String getMapHandle(long cpHandle, TYPE messageType){
+ String mapHandle = "-1";
+ switch(messageType)
+ {
+ case MMS:
+ mapHandle = String.format("%016X",(cpHandle | HANDLE_TYPE_MMS_MASK));
+ break;
+ case SMS_GSM:
+ mapHandle = String.format("%016X",cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
+ break;
+ case SMS_CDMA:
+ mapHandle = String.format("%016X",cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
+ break;
+ case EMAIL:
+ mapHandle = String.format("%016X",(cpHandle | HANDLE_TYPE_EMAIL_MASK)); //TODO correct when email support is implemented
+ 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
+ */
+ static public 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
+ */
+ static public long getCpHandle(String mapHandle)
+ {
+ long cpHandle = getMsgHandleAsLong(mapHandle);
+ /* remove masks as the call should already know what type of message this handle is for */
+ cpHandle &= ~HANDLE_TYPE_MASK;
+ return cpHandle;
+ }
+
+ /**
+ * Extract the message type from the handle.
+ * @param mapHandle
+ * @return
+ */
+ static public 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;
+
+ throw new IllegalArgumentException("Message type not found in handle string.");
+ }
+}
+
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessage.java b/src/com/android/bluetooth/map/BluetoothMapbMessage.java
new file mode 100644
index 000000000..084ebedfe
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessage.java
@@ -0,0 +1,778 @@
+/*
+* 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 java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
+public abstract class BluetoothMapbMessage {
+
+ protected static String TAG = "BluetoothMapbMessage";
+ protected static final boolean D = true;
+ protected static final boolean V = true;
+ private static final String VERSION = "VERSION:1.0";
+
+ public static int INVALID_VALUE = -1;
+
+ protected int appParamCharset = BluetoothMapAppParams.INVALID_VALUE_PARAMETER;
+
+ // TODO: Reevaluate if strings are the best types for the members.
+
+ /* BMSG attributes */
+ private String status = null; // READ/UNREAD
+ protected TYPE type = null; // SMS/MMS/EMAIL
+
+ private String folder = null;
+
+ /* BBODY attributes */
+ private long partId = INVALID_VALUE;
+ protected String encoding = null;
+ protected String charset = null;
+ private String language = null;
+
+ private int bMsgLength = INVALID_VALUE;
+
+ private ArrayList<vCard> originator = null;
+ private ArrayList<vCard> recipient = null;
+
+
+ public static class vCard {
+ /* VCARD attributes */
+ private String version;
+ private String name = null;
+ private String formattedName = null;
+ private String[] phoneNumbers = {};
+ private String[] emailAddresses = {};
+ private int envLevel = 0;
+
+ /**
+ * Construct a version 3.0 vCard
+ * @param name Structured
+ * @param formattedName Formatted name
+ * @param phoneNumbers a String[] of phone numbers
+ * @param emailAddresses a String[] of email addresses
+ * @param the bmessage envelope level (0 is the top/most outer level)
+ */
+ public vCard(String name, String formattedName, String[] phoneNumbers,
+ String[] emailAddresses, int envLevel) {
+ this.envLevel = envLevel;
+ this.version = "3.0";
+ this.name = name != null ? name : "";
+ this.formattedName = formattedName != null ? formattedName : "";
+ setPhoneNumbers(phoneNumbers);
+ if (emailAddresses != null)
+ this.emailAddresses = emailAddresses;
+ }
+
+ /**
+ * Construct a version 2.1 vCard
+ * @param name Structured name
+ * @param phoneNumbers a String[] of phone numbers
+ * @param emailAddresses a String[] of email addresses
+ * @param the bmessage envelope level (0 is the top/most outer level)
+ */
+ public vCard(String name, String[] phoneNumbers,
+ String[] emailAddresses, int envLevel) {
+ this.envLevel = envLevel;
+ this.version = "2.1";
+ this.name = name != null ? name : "";
+ setPhoneNumbers(phoneNumbers);
+ if (emailAddresses != null)
+ this.emailAddresses = emailAddresses;
+ }
+
+ /**
+ * Construct a version 3.0 vCard
+ * @param name Structured name
+ * @param formattedName Formatted name
+ * @param phoneNumbers a String[] of phone numbers
+ * @param emailAddresses a String[] of email addresses
+ */
+ public vCard(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
+ this.version = "3.0";
+ this.name = name != null ? name : "";
+ this.formattedName = formattedName != null ? formattedName : "";
+ setPhoneNumbers(phoneNumbers);
+ if (emailAddresses != null)
+ this.emailAddresses = emailAddresses;
+ }
+
+ /**
+ * Construct a version 2.1 vCard
+ * @param name Structured Name
+ * @param phoneNumbers a String[] of phone numbers
+ * @param emailAddresses a String[] of email addresses
+ */
+ public vCard(String name, String[] phoneNumbers, String[] emailAddresses) {
+ this.version = "2.1";
+ this.name = name != null ? name : "";
+ setPhoneNumbers(phoneNumbers);
+ if (emailAddresses != null)
+ this.emailAddresses = emailAddresses;
+ }
+
+ private void setPhoneNumbers(String[] numbers) {
+ if(numbers != null && numbers.length > 0)
+ {
+ phoneNumbers = new String[numbers.length];
+ for(int i = 0, n = numbers.length; i < n; i++){
+ phoneNumbers[i] = PhoneNumberUtils.extractNetworkPortion(numbers[i]);
+ }
+ }
+ }
+
+ public String getFirstPhoneNumber() {
+ if(phoneNumbers.length > 0) {
+ return phoneNumbers[0];
+ } else
+ throw new IllegalArgumentException("No Phone number");
+ }
+
+ public int getEnvLevel() {
+ return envLevel;
+ }
+
+ public void encode(StringBuilder sb)
+ {
+ sb.append("BEGIN:VCARD").append("\r\n");
+ sb.append("VERSION:").append(version).append("\r\n");
+ if (version.equals("3.0") && formattedName != null)
+ {
+ sb.append("FN:").append(formattedName).append("\r\n");
+ }
+ if (name != null)
+ sb.append("N:").append(name).append("\r\n");
+ for (String phoneNumber : phoneNumbers)
+ {
+ sb.append("TEL:").append(phoneNumber).append("\r\n");
+ }
+ for (String emailAddress : emailAddresses)
+ {
+ sb.append("EMAIL:").append(emailAddress).append("\r\n");
+ }
+ sb.append("END:VCARD").append("\r\n");
+ }
+
+ /**
+ * Parse a vCard from a BMgsReader, where a line containing "BEGIN:VCARD" have just been read.
+ * @param reader
+ * @param originator
+ * @return
+ */
+ public static vCard parseVcard(BMsgReader reader, int envLevel) {
+ String formattedName = null;
+ String name = null;
+ ArrayList<String> phoneNumbers = null;
+ ArrayList<String> emailAddresses = null;
+ String[] parts;
+ String line = reader.getLineEnforce();
+
+ while(!line.contains("END:VCARD")) {
+ line = line.trim();
+ if(line.startsWith("N:")){
+ parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':'
+ if(parts.length == 2) {
+ name = parts[1];
+ } else
+ name = "";
+ }
+ else if(line.startsWith("FN:")){
+ parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':'
+ if(parts.length == 2) {
+ formattedName = parts[1];
+ } else
+ formattedName = "";
+ }
+ else if(line.startsWith("TEL:")){
+ parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':'
+ if(parts.length == 2) {
+ String[] subParts = parts[1].split("[^\\\\];");
+ if(phoneNumbers == null)
+ phoneNumbers = new ArrayList<String>(1);
+ phoneNumbers.add(subParts[subParts.length-1]); // only keep actual phone number
+ } else {}
+ // Empty phone number - ignore
+ }
+ else if(line.startsWith("EMAIL:")){
+ parts = line.split("[^\\\\]:"); // Split on "un-escaped" :
+ if(parts.length == 2) {
+ String[] subParts = parts[1].split("[^\\\\];");
+ if(emailAddresses == null)
+ emailAddresses = new ArrayList<String>(1);
+ emailAddresses.add(subParts[subParts.length-1]); // only keep actual email address
+ } else {}
+ // Empty email address entry - ignore
+ }
+ line = reader.getLineEnforce();
+ }
+ return new vCard(name, formattedName,
+ phoneNumbers == null? null : phoneNumbers.toArray(new String[phoneNumbers.size()]),
+ emailAddresses == null ? null : emailAddresses.toArray(new String[emailAddresses.size()]),
+ envLevel);
+ }
+ };
+
+ private static class BMsgReader {
+ InputStream mInStream;
+ public BMsgReader(InputStream is)
+ {
+ this.mInStream = is;
+ }
+
+ private byte[] getLineAsBytes() {
+ int readByte;
+
+ /* TODO: Actually the vCard spec. allows to break lines by using a newLine
+ * followed by a white space character(space or tab). Not sure this is a good idea to implement
+ * as the Bluetooth MAP spec. illustrates vCards using tab alignment, hence actually
+ * showing an invalid vCard format...
+ * If we read such a folded line, the folded part will be skipped in the parser
+ */
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try {
+ while ((readByte = mInStream.read()) != -1) {
+ if (readByte == '\r') {
+ if ((readByte = mInStream.read()) != -1 && readByte == '\n') {
+ if(output.size() == 0)
+ continue; /* Skip empty lines */
+ else
+ break;
+ } else {
+ output.write('\r');
+ }
+ } else if (readByte == '\n' && output.size() == 0) {
+ /* Empty line - skip */
+ continue;
+ }
+
+ output.write(readByte);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ return output.toByteArray();
+ }
+
+ /**
+ * Read a line of text from the BMessage.
+ * @return the next line of text, or null at end of file, or if UTF-8 is not supported.
+ */
+ public String getLine() {
+ try {
+ byte[] line = getLineAsBytes();
+ if (line.length == 0)
+ return null;
+ else
+ return new String(line, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ }
+
+ /**
+ * same as getLine(), but throws an exception, if we run out of lines.
+ * Use this function when ever more lines are needed for the bMessage to be complete.
+ * @return the next line
+ */
+ public String getLineEnforce() {
+ String line = getLine();
+ if (line == null)
+ throw new IllegalArgumentException("Bmessage too short");
+ return line;
+ }
+
+
+ /**
+ * Reads a line from the InputStream, and examines if the subString
+ * matches the line read.
+ * @param subString
+ * The string to match against the line.
+ * @throws IllegalArgumentException
+ * If the expected substring is not found.
+ *
+ */
+ public void expect(String subString) throws IllegalArgumentException{
+ String line = getLine();
+ if (!line.contains(subString))
+ // TODO: Should this be case insensitive? (Either use toUpper() or matches())
+ throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\"");
+ }
+
+ /**
+ * Same as expect(String), but with two strings.
+ * @param subString
+ * @param subString2
+ * @throws IllegalArgumentException
+ * If one or all of the strings are not found.
+ */
+ public void expect(String subString, String subString2) throws IllegalArgumentException{
+ String line = getLine();
+ if(!line.contains(subString)) // TODO: Should this be case insensitive? (Either use toUpper() or matches())
+ throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\"");
+ if(!line.contains(subString2)) // TODO: Should this be case insensitive? (Either use toUpper() or matches())
+ throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\"");
+ }
+
+ /**
+ * Read a part of the bMessage as raw data.
+ * @param length the number of bytes to read
+ * @return the byte[] containing the number of bytes or null if an error occurs or EOF is reached
+ * before length bytes have been read.
+ */
+ public byte[] getDataBytes(int length) {
+ byte[] data = new byte[length];
+ try {
+ int bytesRead;
+ int offset=0;
+ while ((bytesRead = mInStream.read(data, offset, length-offset)) != length) {
+ if(bytesRead == -1)
+ return null;
+ offset += bytesRead;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ return data;
+ }
+ };
+
+ public BluetoothMapbMessage() {
+
+ }
+
+ public static BluetoothMapbMessage parse(InputStream bMsgStream, int appParamCharset) throws IllegalArgumentException{
+ BMsgReader reader = new BMsgReader(bMsgStream);
+ String line = "";
+ BluetoothMapbMessage newBMsg = null;
+ reader.expect("BEGIN:BMSG");
+ reader.expect("VERSION","1.0");
+ boolean status = false;
+ boolean statusFound = false;
+ TYPE type = null;
+ String folder = null;
+
+ line = reader.getLineEnforce();
+ // Parse the properties - which end with either a VCARD or a BENV
+ while(!line.contains("BEGIN:VCARD") && !line.contains("BEGIN:BENV")) {
+ if(line.contains("STATUS")){
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ if (arg[1].trim().equals("READ")) {
+ status = true;
+ } else if (arg[1].trim().equals("UNREAD")) {
+ status =false;
+ } else {
+ throw new IllegalArgumentException("Wrong value in 'STATUS': " + arg[1]);
+ }
+ } else {
+ throw new IllegalArgumentException("Missing value for 'STATUS': " + line);
+ }
+ }
+ if(line.contains("TYPE")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ String value = arg[1].trim();
+ type = TYPE.valueOf(value); // Will throw IllegalArgumentException if value is wrong
+ if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE
+ && type != TYPE.SMS_CDMA && type != TYPE.SMS_GSM) {
+ throw new IllegalArgumentException("Native appParamsCharset only supported for SMS");
+ }
+ switch(type) {
+ case SMS_CDMA:
+ case SMS_GSM:
+ newBMsg = new BluetoothMapbMessageSms();
+ break;
+ case MMS:
+ case EMAIL:
+ newBMsg = new BluetoothMapbMessageMmsEmail();
+ break;
+ default:
+ break;
+ }
+ } else {
+ throw new IllegalArgumentException("Missing value for 'TYPE':" + line);
+ }
+ }
+ if(line.contains("FOLDER")) {
+ String[] arg = line.split(":");
+ if (arg != null && arg.length == 2) {
+ folder = arg[1].trim();
+ } else {
+ throw new IllegalArgumentException("Missing value for 'FOLDER':" + line);
+ }
+ }
+ line = reader.getLineEnforce();
+ }
+ if(newBMsg == null)
+ throw new IllegalArgumentException("Missing bMessage TYPE: - unable to parse body-content");
+ newBMsg.setType(type);
+ newBMsg.appParamCharset = appParamCharset;
+ if(folder != null)
+ newBMsg.setFolder(folder);
+ if(statusFound)
+ newBMsg.setStatus(status);
+
+ // Now check for originator VCARDs
+ while(line.contains("BEGIN:VCARD")){
+ if(D) Log.d(TAG,"Decoding vCard");
+ newBMsg.addOriginator(vCard.parseVcard(reader,0));
+ line = reader.getLineEnforce();
+ }
+ if(line.contains("BEGIN:BENV")) {
+ newBMsg.parseEnvelope(reader, 0);
+ } else
+ throw new IllegalArgumentException("Bmessage has no BEGIN:BENV - line:" + line);
+
+ /* TODO: Do we need to validate the END:* tags? They are only needed if someone puts additional info
+ * below the END:MSG - in which case we don't handle it.
+ */
+ return newBMsg;
+ }
+
+ private void parseEnvelope(BMsgReader reader, int level) {
+ String line;
+ line = reader.getLineEnforce();
+ if(D) Log.d(TAG,"Decoding envelope level " + level);
+
+ while(line.contains("BEGIN:VCARD")){
+ if(D) Log.d(TAG,"Decoding recipient vCard level " + level);
+ if(recipient == null)
+ recipient = new ArrayList<vCard>(1);
+ recipient.add(vCard.parseVcard(reader, level));
+ line = reader.getLineEnforce();
+ }
+ if(line.contains("BEGIN:BENV")) {
+ if(D) Log.d(TAG,"Decoding nested envelope");
+ parseEnvelope(reader, ++level); // Nested BENV
+ }
+ if(line.contains("BEGIN:BBODY")){
+ if(D) Log.d(TAG,"Decoding bbody");
+ parseBody(reader);
+ }
+ }
+
+ private void parseBody(BMsgReader reader) {
+ String line;
+ line = reader.getLineEnforce();
+ while(!line.contains("END:")) {
+ if(line.contains("PARTID:")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ try {
+ partId = Long.parseLong(arg[1].trim());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]);
+ }
+ } else {
+ throw new IllegalArgumentException("Missing value for 'PARTID': " + line);
+ }
+ }
+ else if(line.contains("ENCODING:")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ encoding = arg[1].trim(); // TODO: Validate ?
+ } else {
+ throw new IllegalArgumentException("Missing value for 'ENCODING': " + line);
+ }
+ }
+ else if(line.contains("CHARSET:")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ charset = arg[1].trim(); // TODO: Validate ?
+ } else {
+ throw new IllegalArgumentException("Missing value for 'CHARSET': " + line);
+ }
+ }
+ else if(line.contains("LANGUAGE:")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ language = arg[1].trim(); // TODO: Validate ?
+ } else {
+ throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line);
+ }
+ }
+ else if(line.contains("LENGTH:")) {
+ String arg[] = line.split(":");
+ if (arg != null && arg.length == 2) {
+ try {
+ bMsgLength = Integer.parseInt(arg[1].trim());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]);
+ }
+ } else {
+ throw new IllegalArgumentException("Missing value for 'LENGTH': " + line);
+ }
+ }
+ else if(line.contains("BEGIN:MSG")) {
+ if(bMsgLength == INVALID_VALUE)
+ throw new IllegalArgumentException("Missing value for 'LENGTH'. Unable to read remaining part of the message");
+ // For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties, since PDUs are encodes as hex-strings
+ /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence
+ * using the length field to determine the amount of data to read, might not be the
+ * best solution.
+ * Since errata ???(bluetooth.org is down at the moment) introduced escaping of END:MSG
+ * in the actual message content, it is now safe to use the END:MSG tag as terminator,
+ * and simply ignore the length field.*/
+ byte[] rawData = reader.getDataBytes(bMsgLength - (line.getBytes().length + 2)); // 2 added to compensate for the removed \r\n
+ String data;
+ try {
+ data = new String(rawData, "UTF-8");
+ if(V) {
+ Log.v(TAG,"MsgLength: " + bMsgLength);
+ Log.v(TAG,"line.getBytes().length: " + line.getBytes().length);
+ String debug = line.replaceAll("\\n", "<LF>\n");
+ debug = debug.replaceAll("\\r", "<CR>");
+ Log.v(TAG,"The line: \"" + debug + "\"");
+ debug = data.replaceAll("\\n", "<LF>\n");
+ debug = debug.replaceAll("\\r", "<CR>");
+ Log.v(TAG,"The msgString: \"" + debug + "\"");
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG,e);
+ throw new IllegalArgumentException("Unable to convert to UTF-8");
+ }
+ /* Decoding of MSG:
+ * 1) split on "\r\nEND:MSG\r\n"
+ * 2) delete "BEGIN:MSG\r\n" for each msg
+ * 3) replace any occurrence of "\END:MSG" with "END:MSG"
+ * 4) based on charset from application properties either store as String[] or decode to raw PDUs
+ * */
+ String messages[] = data.split("\r\nEND:MSG\r\n");
+ parseMsgInit();
+ for(int i = 0; i < messages.length; i++) {
+ messages[i] = messages[i].replaceFirst("^BEGIN:MGS\r\n", "");
+ messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG");
+ messages[i] = messages[i].trim();
+ parseMsgPart(messages[i]);
+ }
+ }
+ line = reader.getLineEnforce();
+ }
+ }
+
+ /**
+ * Parse the 'message' part of <bmessage-body-content>"
+ * @param msgPart
+ */
+ public abstract void parseMsgPart(String msgPart);
+ /**
+ * Set initial values before parsing - will be called is a message body is found
+ * during parsing.
+ */
+ public abstract void parseMsgInit();
+
+ public abstract byte[] encode() throws UnsupportedEncodingException;
+
+ public void setStatus(boolean read) {
+ if(read)
+ this.status = "READ";
+ else
+ this.status = "UNREAD";
+ }
+
+ public void setType(TYPE type) {
+ this.type = type;
+ }
+
+ /**
+ * @return the type
+ */
+ public TYPE getType() {
+ return type;
+ }
+
+ public void setFolder(String folder) {
+ this.folder = "telecom/msg/" + folder;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public ArrayList<vCard> getOriginators() {
+ return originator;
+ }
+
+ public void addOriginator(vCard originator) {
+ if(this.originator == null)
+ this.originator = new ArrayList<vCard>();
+ this.originator.add(originator);
+ }
+
+ /**
+ * Add a version 3.0 vCard with a formatted name
+ * @param name e.g. Bonde;Casper
+ * @param formattedName e.g. "Casper Bonde"
+ * @param phoneNumbers
+ * @param emailAddresses
+ */
+ public void addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
+ if(originator == null)
+ originator = new ArrayList<vCard>();
+ originator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
+ }
+
+ /** Add a version 2.1 vCard with only a name.
+ *
+ * @param name e.g. Bonde;Casper
+ * @param phoneNumbers
+ * @param emailAddresses
+ */
+ public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) {
+ if(originator == null)
+ originator = new ArrayList<vCard>();
+ originator.add(new vCard(name, phoneNumbers, emailAddresses));
+ }
+
+ public ArrayList<vCard> getRecipients() {
+ return recipient;
+ }
+
+ public void setRecipient(vCard recipient) {
+ if(this.recipient == null)
+ this.recipient = new ArrayList<vCard>();
+ this.recipient.add(recipient);
+ }
+
+ public void addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
+ if(recipient == null)
+ recipient = new ArrayList<vCard>();
+ recipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
+ }
+
+ public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) {
+ if(recipient == null)
+ recipient = new ArrayList<vCard>();
+ recipient.add(new vCard(name, phoneNumbers, emailAddresses));
+ }
+
+ /**
+ * Convert a byte[] of data to a hex string representation, converting each nibble to the corresponding
+ * hex char.
+ * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented as a string
+ * as only the characters [0-9] and [a-f] is used.
+ * @param pduData the byte-array of data.
+ * @param scAddressData the byte-array of the encoded sc-Address.
+ * @return the resulting string.
+ */
+ protected String encodeBinary(byte[] pduData, byte[] scAddressData) {
+ StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2);
+ for(int i = 0; i < scAddressData.length; i++) {
+ out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first
+ out.append(Integer.toString( scAddressData[i] & 0x0f,16));
+ }
+ for(int i = 0; i < pduData.length; i++) {
+ out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first
+ out.append(Integer.toString( pduData[i] & 0x0f,16));
+ /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not include the needed 0's
+ e.g. it converts the value 3 to "3" and not "03" */
+ }
+ return out.toString();
+ }
+
+ /**
+ * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set.
+ * @param data The string representation of the data - must have an even number of characters.
+ * @return the byte[] represented in the data.
+ */
+ protected byte[] decodeBinary(String data) {
+ byte[] out = new byte[data.length()/2];
+ String value;
+ if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END");
+ for(int i = 0, j = 0, n = out.length; i < n; i++)
+ {
+ value = data.substring(j++, j++); // same as data.substring(2*i, 2*i+1)
+ out[i] = Byte.valueOf(value, 16);
+ }
+ return out;
+ }
+
+ public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException
+ {
+ StringBuilder sb = new StringBuilder(256);
+ byte[] msgStart, msgEnd;
+ sb.append("BEGIN:BMSG").append("\r\n");
+ sb.append(VERSION).append("\r\n");
+ sb.append("STATUS:").append(status).append("\r\n");
+ sb.append("TYPE:").append(type.name()).append("\r\n");
+ sb.append("FOLDER:").append(folder).append("\r\n");
+ if(originator != null){
+ for(vCard element : originator)
+ element.encode(sb);
+ }
+ /* TODO: Do we need the three levels of env? - e.g. for e-mail. - we do have a level in the
+ * vCards that could be used to determine the the levels of the envelope.
+ */
+
+ sb.append("BEGIN:BENV").append("\r\n");
+ if(recipient != null){
+ for(vCard element : recipient)
+ element.encode(sb);
+ }
+ sb.append("BEGIN:BBODY").append("\r\n");
+ if(encoding != null && encoding != "")
+ sb.append("ENCODING:").append(encoding).append("\r\n");
+ if(charset != null && charset != "")
+ sb.append("CHARSET:").append(charset).append("\r\n");
+
+
+ int length = 0;
+ /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */
+ for (byte[] fragment : bodyFragments) {
+ length += fragment.length + 22;
+ }
+ sb.append("LENGTH:").append(length).append("\r\n");
+
+ // Extract the initial part of the bMessage string
+ msgStart = sb.toString().getBytes("UTF-8");
+
+ sb = new StringBuilder(31);
+ sb.append("END:BBODY").append("\r\n");
+ sb.append("END:BENV").append("\r\n");
+ sb.append("END:BMSG").append("\r\n");
+
+ msgEnd = sb.toString().getBytes("UTF-8");
+
+ try {
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream(msgStart.length + msgEnd.length + length);
+ stream.write(msgStart);
+
+ for (byte[] fragment : bodyFragments) {
+ stream.write("BEGIN:MSG\r\n".getBytes("UTF-8"));
+ stream.write(fragment);
+ stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8"));
+ }
+ stream.write(msgEnd);
+
+ if(V) Log.v(TAG,stream.toString("UTF-8"));
+ return stream.toByteArray();
+ } catch (IOException e) {
+ Log.w(TAG,e);
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java
new file mode 100644
index 000000000..169c42dc4
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java
@@ -0,0 +1,538 @@
+/*
+* 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 java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.UUID;
+
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Base64;
+import android.util.Log;
+
+public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
+
+ private String mmsBody;
+ /**
+ * TODO: Determine the best way to store the MMS message content.
+ * @param mmsBody
+ */
+
+ public static class MimePart {
+ public long _id = INVALID_VALUE; /* The _id from the content provider, can be used to sort the parts if needed */
+ public String contentType = null; /* The mime type, e.g. text/plain */
+ public String contentId = null;
+ public String partName = null; /* e.g. text_1.txt*/
+ public String charsetName = null; /* This seems to be a number e.g. 106 for UTF-8 CharacterSets
+ holds a method for the mapping. */
+ public String fileName = null; /* Do not seem to be used */
+ public byte[] data = null; /* The raw un-encoded data e.g. the raw jpeg data or the text.getBytes("utf-8") */
+
+ public void encode(StringBuilder sb, String boundaryTag, boolean last) throws UnsupportedEncodingException {
+ sb.append("--").append(boundaryTag);
+ if(last)
+ sb.append("--");
+ sb.append("\r\n");
+ if(contentType != null)
+ sb.append("Content-Type: ").append(contentType);
+ if(charsetName != null)
+ sb.append("; ").append("charset=\"").append(charsetName).append("\"");
+ sb.append("\r\n");
+ if(partName != null)
+ sb.append("Content-Location: ").append(partName).append("\r\n");
+ if(data != null) {
+ // If errata 4176 is adopted in the current form, the below is not allowed, Base64 should be used for text
+ if(contentType.toUpperCase().contains("TEXT")) {
+ sb.append("Content-Transfer-Encoding: 8BIT\r\n\r\n"); // Add the header split empty line
+ sb.append(new String(data,"UTF-8")).append("\r\n");
+ }
+ else {
+ sb.append("Content-Transfer-Encoding: Base64\r\n\r\n"); // Add the header split empty line
+ sb.append(Base64.encodeToString(data, Base64.DEFAULT)).append("\r\n");
+ }
+ }
+ }
+ }
+ private long date = INVALID_VALUE;
+ private String subject = null;
+ private ArrayList<Rfc822Token> from = null; // Shall not be empty
+ private ArrayList<Rfc822Token> sender = null; // Shall not be empty
+ private ArrayList<Rfc822Token> to = null; // Shall not be empty
+ private ArrayList<Rfc822Token> cc = null; // Can be empty
+ private ArrayList<Rfc822Token> bcc = null; // Can be empty
+ private ArrayList<Rfc822Token> replyTo = null;// Can be empty
+ private String messageId = null;
+ private ArrayList<MimePart> parts = null;
+ private String contentType = null;
+ private String boundary = null;
+
+ /* TODO:
+ * - create an encoder for the parts e.g. embedded in the mimePart class
+ * */
+ private String getBoundary() {
+ if(boundary == null)
+ boundary = "----" + UUID.randomUUID();
+ return boundary;
+ }
+
+ /**
+ * @return the parts
+ */
+ public ArrayList<MimePart> getMimeParts() {
+ return parts;
+ }
+
+ public MimePart addMimePart() {
+ if(parts == null)
+ parts = new ArrayList<BluetoothMapbMessageMmsEmail.MimePart>();
+ MimePart newPart = new MimePart();
+ parts.add(newPart);
+ return newPart;
+ }
+ public String getDateString() {
+ SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+ Date dateObj = new Date(date);
+ return format.format(dateObj); // Format according to RFC 2822 page 14
+ }
+ public long getDate() {
+ return date;
+ }
+ public void setDate(long date) {
+ this.date = date;
+ }
+ public String getSubject() {
+ return subject;
+ }
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+ public ArrayList<Rfc822Token> getFrom() {
+ return from;
+ }
+ public void setFrom(ArrayList<Rfc822Token> from) {
+ this.from = from;
+ }
+ public void addFrom(String name, String address) {
+ if(this.from == null)
+ this.from = new ArrayList<Rfc822Token>(1);
+ //this.from.add(formatAddress(name, address));
+ this.from.add(new Rfc822Token(name, address, null));
+ }
+ public ArrayList<Rfc822Token> getSender() {
+ return from;
+ }
+ public void setSender(ArrayList<Rfc822Token> sender) {
+ this.sender = sender;
+ }
+ public void addSender(String name, String address) {
+ if(this.sender == null)
+ this.sender = new ArrayList<Rfc822Token>(1);
+ //this.sender.add(formatAddress(name, address));
+ this.sender.add(new Rfc822Token(name,address,null));
+ }
+ public ArrayList<Rfc822Token> getTo() {
+ return to;
+ }
+ public void setTo(ArrayList<Rfc822Token> to) {
+ this.to = to;
+ }
+ public void addTo(String name, String address) {
+ if(this.to == null)
+ this.to = new ArrayList<Rfc822Token>(1);
+ //this.to.add(formatAddress(name, address));
+ this.to.add(new Rfc822Token(name, address, null));
+ }
+ public ArrayList<Rfc822Token> getCc() {
+ return cc;
+ }
+ public void setCc(ArrayList<Rfc822Token> cc) {
+ this.cc = cc;
+ }
+ public void addCc(String name, String address) {
+ if(this.cc == null)
+ this.cc = new ArrayList<Rfc822Token>(1);
+ //this.cc.add(formatAddress(name, address));
+ this.cc.add(new Rfc822Token(name, address, null));
+ }
+ public ArrayList<Rfc822Token> getBcc() {
+ return bcc;
+ }
+ public void setBcc(ArrayList<Rfc822Token> bcc) {
+ this.bcc = bcc;
+ }
+ public void addBcc(String name, String address) {
+ if(this.bcc == null)
+ this.bcc = new ArrayList<Rfc822Token>(1);
+ //this.bcc.add(formatAddress(name, address));
+ this.bcc.add(new Rfc822Token(name, address, null));
+ }
+ public ArrayList<Rfc822Token> getReplyTo() {
+ return replyTo;
+ }
+ public void setReplyTo(ArrayList<Rfc822Token> replyTo) {
+ this.replyTo = replyTo;
+ }
+ public void addReplyTo(String name, String address) {
+ if(this.replyTo == null)
+ this.replyTo = new ArrayList<Rfc822Token>(1);
+ //this.replyTo.add(formatAddress(name, address));
+ this.replyTo.add(new Rfc822Token(name, address, null));
+ }
+ public void setMessageId(String messageId) {
+ this.messageId = messageId;
+ }
+ public String getMessageId() {
+ return messageId;
+ }
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+ public String getContentType() {
+ return contentType;
+ }
+ public void updateCharset() {
+ charset = null;
+ for(MimePart part : parts) {
+ if(part.contentType != null &&
+ part.contentType.toUpperCase().contains("TEXT")) {
+ charset = "UTF-8";
+ }
+ }
+ }
+ /**
+ * Use this to format an address according to RFC 2822.
+ * @param name
+ * @param address
+ * @return
+ */
+ public static String formatAddress(String name, String address) {
+ StringBuilder sb = new StringBuilder();
+ Boolean nameSet = false;
+ if(name != null && !(name = name.trim()).equals("")) {
+ sb.append(name.trim());
+ nameSet = true;
+ }
+ if(address != null && !(address = address.trim()).equals(""))
+ {
+ if(nameSet == true)
+ sb.append(":");
+ sb.append("<").append(address).append(">");
+ }
+ // TODO: Throw exception of the string is larger than 996
+ return sb.toString();
+ }
+
+
+ /**
+ * Encode an address header, and perform folding if needed.
+ * @param sb The stringBuilder to write to
+ * @param headerName The RFC 2822 header name
+ * @param addresses the reformatted address substrings to encode. Create
+ * these using {@link formatAddress}
+ */
+ public void encodeHeaderAddresses(StringBuilder sb, String headerName,
+ ArrayList<Rfc822Token> addresses) {
+ /* TODO: Do we need to encode the addresses if they contain illegal characters */
+ int partLength, lineLength = 0;
+ lineLength += headerName.getBytes().length;
+ sb.append(headerName);
+ for(Rfc822Token address : addresses) {
+ partLength = address.toString().getBytes().length+1;
+ // Add folding if needed
+ if(lineLength + partLength >= 998) // max line length in RFC2822
+ {
+ sb.append("\r\n "); // Append a FWS (folding whitespace)
+ lineLength = 0;
+ }
+ sb.append(address.toString()).append(";");
+ lineLength += partLength;
+ }
+ sb.append("\r\n");
+ }
+
+ public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException
+ {
+ /* TODO: From RFC-4356 - about the RFC-(2)822 headers:
+ * "Current Internet Message format requires that only 7-bit US-ASCII
+ * characters be present in headers. Non-7-bit characters in an address
+ * domain must be encoded with [IDN]. If there are any non-7-bit
+ * characters in the local part of an address, the message MUST be
+ * rejected. Non-7-bit characters elsewhere in a header MUST be encoded
+ * according to [Hdr-Enc]."
+ * We need to add the address encoding in encodeHeaderAddresses, but it is not
+ * straight forward, as it is unclear how to do this. */
+ if (date != INVALID_VALUE)
+ sb.append("Date: ").append(getDateString()).append("\r\n");
+ /* According to RFC-2822 headers must use US-ASCII, where the MAP specification states
+ * UTF-8 should be used for the entire <bmessage-body-content>. We let the MAP specification
+ * take precedence above the RFC-2822. The code to
+ */
+/* If we are to use US-ASCII anyway, here are the code for it.
+ if (subject != null){
+ // Use base64 encoding for the subject, as it may contain non US-ASCII characters or other
+ // illegal (RFC822 header), and android do not seem to have encoders/decoders for quoted-printables
+ sb.append("Subject:").append("=?utf-8?B?");
+ sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT));
+ sb.append("?=\r\n");
+ }*/
+ if (subject != null)
+ sb.append("Subject: ").append(subject).append("\r\n");
+ if(from != null)
+ encodeHeaderAddresses(sb, "From: ", from); // This includes folding if needed.
+ if(sender != null)
+ encodeHeaderAddresses(sb, "Sender: ", sender); // This includes folding if needed.
+ /* For MMS one recipient(to, cc or bcc) must exists, if none: 'To: undisclosed-
+ * recipients:;' could be used.
+ * TODO: Is this a valid solution for E-Mail?
+ */
+ if(to == null && cc == null && bcc == null)
+ sb.append("To: undisclosed-recipients:;\r\n");
+ if(to != null)
+ encodeHeaderAddresses(sb, "To: ", to); // This includes folding if needed.
+ if(cc != null)
+ encodeHeaderAddresses(sb, "Cc: ", cc); // This includes folding if needed.
+ if(bcc != null)
+ encodeHeaderAddresses(sb, "Bcc: ", bcc); // This includes folding if needed.
+ if(replyTo != null)
+ encodeHeaderAddresses(sb, "Reply-To: ", replyTo); // This includes folding if needed.
+ if(messageId != null)
+ sb.append("Message-Id: ").append(messageId).append("\r\n");
+ if(contentType != null)
+ sb.append("Content-Type: ").append(contentType).append("; boundary=").append(getBoundary());
+ sb.append("\r\n\r\n"); // If no headers exists, we still need two CRLF, hence keep it out of the if above.
+ }
+
+ /* Notes on MMS
+ * ------------
+ * According to rfc4356 all headers of a MMS converted to an E-mail must use
+ * 7-bit encoding. According the the MAP specification only 8-bit encoding is
+ * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes
+ * sense, since the info is already present in the bMessage properties.)
+ * The result is that no information from RFC4356 is needed, since it does not
+ * describe any mapping between MMS content and E-mail content.
+ * Suggestion:
+ * Clearly state in the MAP specification that
+ * only the actual message content should be included in the <bmessage-body-content>.
+ * Correct the Example to not include the E-mail headers, and in stead show how to
+ * include a picture or another binary attachment.
+ *
+ * If the headers should be included, clearly state which, as the example clearly shows
+ * that some of the headers should be excluded.
+ * Additionally it is not clear how to handle attachments. There is a parameter in the
+ * get message to include attachments, but since only 8-bit encoding is allowed,
+ * (hence neither base64 nor binary) there is not mechanism to embed the attachment in
+ * the <bmessage-body-content>.
+ *
+ * UPDATE: Errata xxx allows the needed encoding typed inside the <bmessage-body-content>
+ * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii
+ * messages - e.g. pictures and utf-8 strings with non-us-ascii content.
+ * */
+
+ /**
+ * Encode the bMessage as a MMS
+ * @return
+ * @throws UnsupportedEncodingException
+ */
+ public byte[] encodeMms() throws UnsupportedEncodingException
+ {
+ ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
+ StringBuilder sb = new StringBuilder();
+ int count = 0;
+ String mmsBody;
+
+ encoding = "8BIT"; // The encoding used
+
+ encodeHeaders(sb);
+ for(MimePart part : parts) {
+ count++;
+ part.encode(sb, getBoundary(), (count == parts.size()));
+ }
+
+ mmsBody = sb.toString();
+
+ if(mmsBody != null) {
+ String tmpBody = mmsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
+ bodyFragments.add(tmpBody.getBytes("UTF-8"));
+ } else {
+ bodyFragments.add(new byte[0]); // TODO: Is this allowed? (An empty message)
+ }
+
+ return encodeGeneric(bodyFragments);
+ }
+
+ private void parseMmsHeaders(String hdrPart) {
+ String[] headers = hdrPart.split("\r\n");
+
+ for(String header : headers) {
+ if(header.trim() == "")
+ continue;
+ String[] headerParts = header.split(":",2);
+ if(headerParts.length != 2)
+ throw new IllegalArgumentException("Header not formatted correctly: " + header);
+ String headerType = headerParts[0].toUpperCase();
+ String headerValue = headerParts[1].trim();
+
+ // Address headers
+ // TODO: If this is empty, the MSE needs to fill it in
+ if(headerType.contains("FROM")) {
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
+ from = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
+ }
+ else if(headerType.contains("TO")) {
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
+ to = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
+ }
+ else if(headerType.contains("CC")) {
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
+ cc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
+ }
+ else if(headerType.contains("BCC")) {
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
+ bcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
+ }
+ else if(headerType.contains("REPLY-TO")) {
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
+ replyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
+ }// Other headers
+ else if(headerType.contains("SUBJECT")) {
+ subject = headerValue;
+ }
+ else if(headerType.contains("MESSAGE-ID")) {
+ messageId = headerValue;
+ }
+ else if(headerType.contains("DATE")) {
+ /* XXX: Set date */
+ }
+ else if(headerType.contains("CONTENT-TYPE")) {
+ String[] contentTypeParts = headerValue.split(";");
+ contentType = contentTypeParts[0];
+ // Extract the boundary if it exists
+ for(int i=1, n=contentTypeParts.length; i<n; i++)
+ {
+ if(contentTypeParts[i].contains("boundary")) {
+ boundary = contentTypeParts[i].split("boundary[\\s]*=", 2)[1].trim();
+ }
+ }
+ }
+ else {
+ if(D) Log.w(TAG,"Skipping unknown header: " + headerType + " (" + header + ")");
+ }
+ }
+ }
+
+ private void parseMmsMimePart(String partStr) {
+ /**/
+ String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body
+ if(parts.length != 2) {
+ throw new IllegalArgumentException("Wrongly formatted email part - unable to locate header section");
+ }
+ String[] headers = parts[0].split("\r\n");
+ MimePart newPart = addMimePart();
+ String encoding = "";
+
+ for(String header : headers) {
+ if(header.length() == 0)
+ continue;
+
+ if(header.trim() == "" || header.trim().equals("--")) // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
+ continue;
+ String[] headerParts = header.split(":",2);
+ if(headerParts.length != 2)
+ throw new IllegalArgumentException("part-Header not formatted correctly: " + header);
+ String headerType = headerParts[0].toUpperCase();
+ String headerValue = headerParts[1].trim();
+ if(headerType.contains("CONTENT-TYPE")) {
+ // TODO: extract charset - as for
+ newPart.contentType = headerValue;
+ Log.d(TAG, "*** CONTENT-TYPE: " + newPart.contentType);
+ }
+ else if(headerType.contains("CONTENT-LOCATION")) {
+ // This is used if the smil refers to a file name in its src=
+ newPart.partName = headerValue;
+ }
+ else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
+ encoding = headerValue;
+ }
+ else if(headerType.contains("CONTENT-ID")) {
+ // This is used if the smil refers to a cid:<xxx> in it's src=
+ newPart.contentId = headerValue;
+ }
+ else {
+ if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType + " (" + header + ")");
+ }
+ }
+ // Now for the body
+ if(encoding.toUpperCase().contains("BASE64")) {
+ newPart.data = Base64.decode(parts[1], Base64.DEFAULT);
+ } else {
+ // TODO: handle other encoding types? - here we simply store the string data as bytes
+ try {
+ newPart.data = parts[1].getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // This will never happen, as UTF-8 is mandatory on Android platforms
+ }
+ }
+ }
+ private void parseMms(String message) {
+ /* Overall strategy for decoding:
+ * 1) split on first empty line to extract the header
+ * 2) unfold and parse headers
+ * 3) split on boundary to split into parts (or use the remaining as a part,
+ * if part is not found)
+ * 4) parse each part
+ * */
+ String[] messageParts;
+ String[] mimeParts;
+ message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold
+ messageParts = message.split("\r\n\r\n", 2); // Split the header from the body
+ if(messageParts.length != 2) {
+ throw new IllegalArgumentException("Wrongly formatted email message - unable to locate header section");
+ }
+ parseMmsHeaders(messageParts[0]);
+ mimeParts = messageParts[1].split("--" + boundary);
+ for(String part : mimeParts) {
+ if (part != null && (part.length() > 0))
+ parseMmsMimePart(part);
+ }
+ }
+
+ /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
+ * src="filename.jpg" refers to a part with Content-Location: filename.jpg
+ * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/
+ @Override
+ public void parseMsgPart(String msgPart) {
+ // TODO Auto-generated method stub
+ parseMms(msgPart);
+
+ }
+
+ @Override
+ public void parseMsgInit() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public byte[] encode() throws UnsupportedEncodingException {
+ return encodeMms();
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java b/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java
new file mode 100644
index 000000000..07a2a6731
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java
@@ -0,0 +1,82 @@
+/*
+* 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 java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
+public class BluetoothMapbMessageSms extends BluetoothMapbMessage {
+
+ private ArrayList<SmsPdu> smsBodyPdus = null;
+ private String smsBody = null;
+
+ public void setSmsBodyPdus(ArrayList<SmsPdu> smsBodyPdus) {
+ this.smsBodyPdus = smsBodyPdus;
+ this.charset = null;
+ if(smsBodyPdus.size() > 0)
+ this.encoding = smsBodyPdus.get(0).getEncodingString();
+ }
+
+ public String getSmsBody() {
+ return smsBody;
+ }
+
+ public void setSmsBody(String smsBody) {
+ this.smsBody = smsBody;
+ this.charset = "UTF-8";
+ this.encoding = null;
+ }
+
+ @Override
+ public void parseMsgPart(String msgPart) {
+ if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE) {
+ smsBody += BluetoothMapSmsPdu.decodePdu(decodeBinary(msgPart),
+ type == TYPE.SMS_CDMA ? BluetoothMapSmsPdu.SMS_TYPE_CDMA
+ : BluetoothMapSmsPdu.SMS_TYPE_GSM);
+ } else {
+ smsBody += msgPart;
+ }
+ }
+ @Override
+ public void parseMsgInit() {
+ smsBody = "";
+ }
+
+ public byte[] encode() throws UnsupportedEncodingException
+ {
+ ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
+
+ /* Store the messages in an ArrayList to be able to handle the different message types in a generic way.
+ * We use byte[] since we need to extract the length in bytes.
+ */
+ if(smsBody != null) {
+ String tmpBody = smsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
+ bodyFragments.add(tmpBody.getBytes("UTF-8"));
+ }else if (smsBodyPdus.size() > 0) {
+ for (SmsPdu pdu : smsBodyPdus) {
+ // This cannot(must not) contain END:MSG
+ bodyFragments.add(encodeBinary(pdu.getData(),pdu.getScAddress()).getBytes("UTF-8"));
+ }
+ } else {
+ bodyFragments.add(new byte[0]); // TODO: Is this allowed? (An empty message)
+ }
+
+ return encodeGeneric(bodyFragments);
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
new file mode 100644
index 000000000..c6283f1b5
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
@@ -0,0 +1,346 @@
+/*
+* 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.obex.ApplicationParameter;
+import javax.obex.ClientOperation;
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ObexTransport;
+import javax.obex.ResponseCodes;
+
+/**
+ *
+ * @author Casper Bonde, Henrik Ejersbo and Kim Schulz
+ *
+ * The Message Notification Service class runs its own message handler thread,
+ * to avoid executing long operations on the MAP service Thread.
+ * This handler context is passed to the content observers,
+ * hence all call-backs (and thereby transmission of data) is executed
+ * from this thread.
+ */
+public class BluetoothMnsObexClient extends Thread{
+
+ private static final String TAG = "BluetoothMnsObexClient";
+ private static final boolean D = true;
+ private static final boolean V = true;
+
+ public final static int MSG_SESSION_ERROR = 1;
+ public final static int MSG_CONNECT_TIMEOUT = 2;
+
+ private ObexTransport mTransport;
+ private Context mContext;
+ public static Handler mHandler = null;
+ private volatile boolean mWaitingForRemote;
+ private static final String TYPE_EVENT = "x-bt/MAP-event-report";
+ private ClientSession mClientSession;
+ private boolean mConnected = false;
+ BluetoothDevice mRemoteDevice;
+ private static BluetoothMapContentObserver mObserver;
+ private boolean mObserverRegistered = false;
+
+ private Looper mLooper = null;
+ // Used by the MAS to forward notification registrations
+ public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
+ public static final int MSG_MNS_SHUTDOWN = 2;
+
+ public static final ParcelUuid BluetoothUuid_ObexMns =
+ ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
+
+
+ public BluetoothMnsObexClient(Context context, BluetoothDevice remoteDevice) {
+ if (remoteDevice == null) {
+ throw new NullPointerException("Obex transport is null");
+ }
+ mContext = context;
+ mRemoteDevice = remoteDevice;
+ }
+
+ public static Handler getMessageHandler() {
+ // TODO: if mHandle is null, we should wait for it to be created.
+ return mHandler;
+ }
+
+ public static BluetoothMapContentObserver getContentObserver() {
+ return mObserver;
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ mLooper = Looper.myLooper();
+
+
+ /* Create the context observer from within the thread to ensure the "content changed"
+ * events are handled in this thread. */
+ mObserver = new BluetoothMapContentObserver(mContext);
+ mObserver.init();
+
+ mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_MNS_NOTIFICATION_REGISTRATION:
+ handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
+ break;
+ case MSG_MNS_SHUTDOWN:
+ if(mObserverRegistered)
+ mObserver.unregisterObserver();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ Looper.loop();
+ }
+
+ public boolean isConnected() {
+ return mConnected;
+ }
+
+ public void disconnect() {
+ try {
+ if (mClientSession != null) {
+ mClientSession.disconnect(null);
+ if (D) Log.d(TAG, "OBEX session disconnected");
+ }
+ mClientSession = null;
+ } catch (IOException e) {
+ Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
+ }
+ try {
+ if (mClientSession != null) {
+ if (D) Log.d(TAG, "OBEX session close mClientSession");
+ mClientSession.close();
+ if (D) Log.d(TAG, "OBEX session closed");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "OBEX session close error:" + e.getMessage());
+ }
+ if (mTransport != null) {
+ try {
+ if (D) Log.d(TAG, "Close Obex Transport");
+ mTransport.close();
+ if (D) Log.d(TAG, "Obex Transport Closed");
+ } catch (IOException e) {
+ Log.e(TAG, "mTransport.close error: " + e.getMessage());
+ }
+ }
+ if(mObserverRegistered) {
+ mObserver.unregisterObserver();
+ mObserverRegistered = false;
+ }
+
+ mObserver.deinit();
+ // Shut down the thread
+ if(mLooper != null)
+ mLooper.quit();
+ interrupt();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ if(V) Log.w(TAG, e);
+ }
+ }
+
+ private HeaderSet hsConnect = null;
+
+ public void handleRegistration(int masId, int notificationStatus){
+ Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
+
+ if(isConnected() == false) {
+ Log.d(TAG, "handleRegistration: connect");
+ connect();
+ }
+
+ if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
+ // Unregister - should we disconnect, or keep the connection? - the spec. says nothing about this.
+ if(mObserverRegistered == true) {
+ mObserver.unregisterObserver();
+ mObserverRegistered = false;
+ disconnect();
+ }
+ } else if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
+ /* Connect if we do not have a connection, and start the content observers providing
+ * this thread as Handler.
+ */
+ if(mObserverRegistered == false) {
+ mObserver.registerObserver(this, masId);
+ mObserverRegistered = true;
+ }
+ }
+ }
+
+ public void connect() {
+ Log.d(TAG, "handleRegistration: connect 2");
+
+ BluetoothSocket btSocket = null;
+ try {
+ btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
+ BluetoothUuid_ObexMns.getUuid());
+ btSocket.connect();
+ } catch (IOException e) {
+ Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
+ // TODO: do we need to report error somewhere?
+ return;
+ }
+
+ mTransport = new BluetoothMnsRfcommTransport(btSocket);
+
+ try {
+ mClientSession = new ClientSession(mTransport);
+ mConnected = true;
+ } catch (IOException e1) {
+ Log.e(TAG, "OBEX session create error " + e1.getMessage());
+ }
+ if (mConnected && mClientSession != null) {
+ mConnected = false;
+ HeaderSet hs = new HeaderSet();
+ // bb582b41-420c-11db-b0de-0800200c9a66
+ byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
+ (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb,
+ (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00,
+ (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 };
+ hs.setHeader(HeaderSet.TARGET, mnsTarget);
+
+ synchronized (this) {
+ mWaitingForRemote = true;
+ }
+ try {
+ hsConnect = mClientSession.connect(hs);
+ if (D) Log.d(TAG, "OBEX session created");
+ mConnected = true;
+ } catch (IOException e) {
+ Log.e(TAG, "OBEX session connect error " + e.getMessage());
+ }
+ }
+ synchronized (this) {
+ mWaitingForRemote = false;
+ }
+ }
+
+ public int sendEvent(byte[] eventBytes, int masInstanceId) {
+
+ boolean error = false;
+ int responseCode = -1;
+ HeaderSet request;
+ int maxChunkSize, bytesToWrite, bytesWritten = 0;
+ request = new HeaderSet();
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setMasInstanceId(masInstanceId);
+
+ ClientOperation putOperation = null;
+ OutputStream outputStream = null;
+
+ try {
+ request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
+ request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
+
+ request.mConnectionID = new byte[4];
+ System.arraycopy(hsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
+
+ synchronized (this) {
+ mWaitingForRemote = true;
+ }
+ // Send the header first and then the body
+ try {
+ if (V) Log.v(TAG, "Send headerset Event ");
+ putOperation = (ClientOperation)mClientSession.put(request);
+ // TODO - Should this be kept or Removed
+
+ } catch (IOException e) {
+ Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
+ error = true;
+ }
+ synchronized (this) {
+ mWaitingForRemote = false;
+ }
+ if (!error) {
+ try {
+ if (V) Log.v(TAG, "Send headerset Event ");
+ outputStream = putOperation.openOutputStream();
+ } catch (IOException e) {
+ Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
+ error = true;
+ }
+ }
+
+ if (!error) {
+
+ maxChunkSize = putOperation.getMaxPacketSize();
+
+ while (bytesWritten < eventBytes.length) {
+ bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
+ outputStream.write(eventBytes, bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ }
+
+ if (bytesWritten == eventBytes.length) {
+ Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
+ outputStream.close();
+ } else {
+ error = true;
+ // TBD - Is Output stream close needed here
+ putOperation.abort();
+ Log.i(TAG, "SendEvent interrupted");
+ }
+ }
+ } catch (IOException e) {
+ handleSendException(e.toString());
+ error = true;
+ } catch (IndexOutOfBoundsException e) {
+ handleSendException(e.toString());
+ error = true;
+ } finally {
+ try {
+ if (!error) {
+ responseCode = putOperation.getResponseCode();
+ if (responseCode != -1) {
+ if (V) Log.v(TAG, "Put response code " + responseCode);
+ if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
+ Log.i(TAG, "Response error code is " + responseCode);
+ }
+ }
+ }
+ if (putOperation != null) {
+ putOperation.close();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error when closing stream after send " + e.getMessage());
+ }
+ }
+
+ return responseCode;
+ }
+
+ private void handleSendException(String exception) {
+ Log.e(TAG, "Error when sending event: " + exception);
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java b/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java
new file mode 100644
index 000000000..fc5d54a5d
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java
@@ -0,0 +1,79 @@
+/*
+* 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.bluetooth.BluetoothSocket;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.obex.ObexTransport;
+
+public class BluetoothMnsRfcommTransport implements ObexTransport {
+
+ private final BluetoothSocket mSocket;
+
+ public BluetoothMnsRfcommTransport(BluetoothSocket socket) {
+ super();
+ this.mSocket = socket;
+ }
+
+ public void close() throws IOException {
+ mSocket.close();
+ }
+
+ public DataInputStream openDataInputStream() throws IOException {
+ return new DataInputStream(openInputStream());
+ }
+
+ public DataOutputStream openDataOutputStream() throws IOException {
+ return new DataOutputStream(openOutputStream());
+ }
+
+ public InputStream openInputStream() throws IOException {
+ return mSocket.getInputStream();
+ }
+
+ public OutputStream openOutputStream() throws IOException {
+ return mSocket.getOutputStream();
+ }
+
+ public void connect() throws IOException {
+ }
+
+ public void create() throws IOException {
+ }
+
+ public void disconnect() throws IOException {
+ }
+
+ public void listen() throws IOException {
+ }
+
+ public boolean isConnected() throws IOException {
+ // TODO: add implementation
+ return true;
+ }
+
+ public String getRemoteAddress() {
+ if (mSocket == null)
+ return null;
+ return mSocket.getRemoteDevice().getAddress();
+ }
+
+}