OSDN Git Service

Obex over L2CAP + SDP search API
authorCasper Bonde <c.bonde@samsung.com>
Thu, 9 Apr 2015 07:20:50 +0000 (09:20 +0200)
committerCasper Bonde <c.bonde@samsung.com>
Thu, 9 Apr 2015 07:20:50 +0000 (09:20 +0200)
Each profile had its own implementation of ObexTransport.
These implementations were implemented very similar, and
could easily be merged into a common implementation.

Additionally it will make it easier to adopt the transport
to support L2CAP.

The SDP functionality is implemented in a way that is scalable,
hence adding new record types is easy.
Intents are currently used to distribute the SDP search results,
as we have observed that the new client side profiles have been
located outside the Bluetooth package.
We strongly recommend to keep all bluetooth profiles within the
Bluetooth package, to acoid the need for exposing all bluetooth
interfaces through the Android framework. For instance this new
SDP create API is internal to Bluetooth, hence cannot be used by
the external profiles - hence they cannot use OBEX over L2CAP.

The SDP search currently supports:
 - MAP both sides
 - OPP server (only the server needs an SDP record)
 - PBAP server (only the server needs SDP record)

The SDP create record currently supports:
 - MAP both sides
 - OPP server
 - PBAP server
The new l2cap sockets introduces a new wrapper class for a rfcomm
and an l2cap socket.
The wrapper design:
 - Creates two accept threads running while bluetooth is
   turned on.
 - When a connection is accepted the owner is asked wether or not
   to accept the connection if rejected, the connection will be
   rejected at obex level, else it will be accepted and the state
   is changed to busy.
 - Any further connections will be rejected at Obex level, until
   the state is changed back to idle.

Notes tor executing the test cases:
Test OBEX using local pipes(no BT) or two bluetooth enabled
devices.

Test SDP using two Bluetooth enabled devices

Start server testcase first on one device, then start
the matching client test on the other device.

You cannot run all tests in one go, as they need to run
on multiple devices.

Edit the test sequesce to add/remove/modify the sequence
of OBEX operations to perform.

Use the new SDP record create interface.

Change-Id: I3941793f9843abf4afa5ffbee40d1d01c118b29b
Signed-off-by: Casper Bonde <c.bonde@samsung.com>
35 files changed:
jni/Android.mk
jni/com_android_bluetooth.h
jni/com_android_bluetooth_avrcp.cpp
jni/com_android_bluetooth_btservice_AdapterService.cpp
jni/com_android_bluetooth_sdp.cpp [new file with mode: 0644]
src/com/android/bluetooth/BluetoothObexTransport.java [moved from src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java with 66% similarity]
src/com/android/bluetooth/IObexConnectionHandler.java [new file with mode: 0644]
src/com/android/bluetooth/ObexRejectServer.java [new file with mode: 0644]
src/com/android/bluetooth/ObexServerSockets.java [new file with mode: 0644]
src/com/android/bluetooth/btservice/AdapterService.java
src/com/android/bluetooth/btservice/JniCallbacks.java
src/com/android/bluetooth/btservice/RemoteDevices.java
src/com/android/bluetooth/map/BluetoothMapMasInstance.java
src/com/android/bluetooth/map/BluetoothMapObexServer.java
src/com/android/bluetooth/map/BluetoothMapService.java
src/com/android/bluetooth/map/BluetoothMapUtils.java
src/com/android/bluetooth/map/BluetoothMnsObexClient.java
src/com/android/bluetooth/opp/BluetoothOppObexServerSession.java
src/com/android/bluetooth/opp/BluetoothOppRfcommListener.java
src/com/android/bluetooth/opp/BluetoothOppRfcommTransport.java [deleted file]
src/com/android/bluetooth/opp/BluetoothOppTransfer.java
src/com/android/bluetooth/opp/TestActivity.java
src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java [deleted file]
src/com/android/bluetooth/pbap/BluetoothPbapService.java
src/com/android/bluetooth/sdp/SdpManager.java [new file with mode: 0644]
tests/Android.mk
tests/src/com/android/bluetooth/tests/IResultLogger.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/ObexPipeTransport.java [moved from src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java with 64% similarity]
tests/src/com/android/bluetooth/tests/ObexTest.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/ObexTestDataHandler.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/ObexTestParams.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/ObexTestServer.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/SdpManagerTest.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/SdpManagerTestServer.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/TestResultLogger.java [new file with mode: 0644]

index e1c63b2..fc4d871 100644 (file)
@@ -13,7 +13,8 @@ LOCAL_SRC_FILES:= \
     com_android_bluetooth_hid.cpp \
     com_android_bluetooth_hdp.cpp \
     com_android_bluetooth_pan.cpp \
-    com_android_bluetooth_gatt.cpp
+    com_android_bluetooth_gatt.cpp \
+    com_android_bluetooth_sdp.cpp
 
 LOCAL_C_INCLUDES += \
     $(JNI_H_INCLUDE) \
index 74dcddb..4f55c06 100644 (file)
@@ -52,6 +52,8 @@ int register_com_android_bluetooth_pan(JNIEnv* env);
 
 int register_com_android_bluetooth_gatt (JNIEnv* env);
 
+int register_com_android_bluetooth_sdp (JNIEnv* env);
+
 }
 
 #endif /* COM_ANDROID_BLUETOOTH_H */
index fb6f2fd..e9a7bc9 100644 (file)
@@ -67,7 +67,8 @@ static void btavrcp_remote_features_callback(bt_bdaddr_t* bd_addr, btrc_remote_f
     sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr);
     sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getRcFeatures, addr, (jint)features);
     checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__);
-    sCallbackEnv->DeleteLocalRef(addr);
+    /* TODO: I think we leak the addr object, we should add a
+     * sCallbackEnv->DeleteLocalRef(addr) */
 }
 
 static void btavrcp_get_play_status_callback() {
index f780364..02c293e 100644 (file)
 #define LOG_TAG "BluetoothServiceJni"
 #include "com_android_bluetooth.h"
 #include "hardware/bt_sock.h"
-#include "hardware/bt_mce.h"
 #include "utils/Log.h"
 #include "utils/misc.h"
 #include "cutils/properties.h"
 #include "android_runtime/AndroidRuntime.h"
 #include "android_runtime/Log.h"
-
 #include <string.h>
 #include <pthread.h>
-
+#include <binder/Parcel.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 
@@ -45,12 +43,10 @@ static jmethodID method_discoveryStateChangeCallback;
 static jmethodID method_setWakeAlarm;
 static jmethodID method_acquireWakeLock;
 static jmethodID method_releaseWakeLock;
-static jmethodID method_deviceMasInstancesFoundCallback;
 static jmethodID method_energyInfo;
 
 static const bt_interface_t *sBluetoothInterface = NULL;
 static const btsock_interface_t *sBluetoothSocketInterface = NULL;
-static const btmce_interface_t *sBluetoothMceInterface = NULL;
 static JNIEnv *callbackEnv = NULL;
 
 static jobject sJniAdapterServiceObj;
@@ -528,12 +524,10 @@ static int acquire_wake_lock_callout(const char *lock_name) {
     JNIEnv *env;
     JavaVM *vm = AndroidRuntime::getJavaVM();
     jint status = vm->GetEnv((void **)&env, JNI_VERSION_1_6);
-
     if (status != JNI_OK && status != JNI_EDETACHED) {
         ALOGE("%s unable to get environment for JNI call", __func__);
         return BT_STATUS_FAIL;
     }
-
     if (status == JNI_EDETACHED && vm->AttachCurrentThread(&env, &sAttachArgs) != 0) {
         ALOGE("%s unable to attach thread to VM", __func__);
         return BT_STATUS_FAIL;
@@ -564,12 +558,10 @@ static int release_wake_lock_callout(const char *lock_name) {
         ALOGE("%s unable to get environment for JNI call", __func__);
         return BT_STATUS_FAIL;
     }
-
     if (status == JNI_EDETACHED && vm->AttachCurrentThread(&env, &sAttachArgs) != 0) {
         ALOGE("%s unable to attach thread to VM", __func__);
         return BT_STATUS_FAIL;
     }
-
     jboolean ret = JNI_FALSE;
     jstring lock_name_jni = env->NewStringUTF(lock_name);
     if (lock_name_jni) {
@@ -578,11 +570,9 @@ static int release_wake_lock_callout(const char *lock_name) {
     } else {
         ALOGE("%s unable to allocate string: %s", __func__, lock_name);
     }
-
     if (status == JNI_EDETACHED) {
         vm->DetachCurrentThread();
     }
-
     return ret ? BT_STATUS_SUCCESS : BT_STATUS_FAIL;
 }
 
@@ -596,73 +586,6 @@ static void alarmFiredNative(JNIEnv *env, jobject obj) {
     }
 }
 
-static void remote_mas_instances_callback(bt_status_t status, bt_bdaddr_t *bd_addr,
-                                          int num_instances, btmce_mas_instance_t *instances)
-{
-    if (!checkCallbackThread()) {
-       ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__);
-       return;
-    }
-
-    ALOGV("%s: Status is: %d, Instances: %d", __FUNCTION__, status, num_instances);
-
-    if (status != BT_STATUS_SUCCESS) {
-        ALOGE("%s: Status %d is incorrect", __FUNCTION__, status);
-        return;
-    }
-
-    callbackEnv->PushLocalFrame(ADDITIONAL_NREFS);
-
-    jbyteArray addr = NULL;
-    jobjectArray a_name = NULL;
-    jintArray a_scn = NULL;
-    jintArray a_masid = NULL;
-    jintArray a_msgtype = NULL;
-    jclass mclass;
-
-    mclass = callbackEnv->FindClass("java/lang/String");
-
-    addr = callbackEnv->NewByteArray(sizeof(bt_bdaddr_t));
-    if (addr == NULL) goto clean;
-
-    callbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*)bd_addr);
-
-    a_name = callbackEnv->NewObjectArray(num_instances, mclass, NULL);
-    if (a_name == NULL) goto clean;
-
-    a_scn = callbackEnv->NewIntArray(num_instances);
-    if (a_scn == NULL) goto clean;
-
-    a_masid = callbackEnv->NewIntArray(num_instances);
-    if (a_masid == NULL) goto clean;
-
-    a_msgtype = callbackEnv->NewIntArray(num_instances);
-    if (a_msgtype == NULL) goto clean;
-
-    for (int i = 0; i < num_instances; i++) {
-        jstring name = callbackEnv->NewStringUTF(instances[i].p_name);
-
-        callbackEnv->SetObjectArrayElement(a_name, i, name);
-        callbackEnv->SetIntArrayRegion(a_scn, i, 1, &instances[i].scn);
-        callbackEnv->SetIntArrayRegion(a_masid, i, 1, &instances[i].id);
-        callbackEnv->SetIntArrayRegion(a_msgtype, i, 1, &instances[i].msg_types);
-
-        callbackEnv->DeleteLocalRef(name);
-    }
-
-    callbackEnv->CallVoidMethod(sJniCallbacksObj, method_deviceMasInstancesFoundCallback,
-            (jint) status, addr, a_name, a_scn, a_masid, a_msgtype);
-    checkAndClearExceptionFromCallback(callbackEnv, __FUNCTION__);
-
-clean:
-    if (addr != NULL) callbackEnv->DeleteLocalRef(addr);
-    if (a_name != NULL) callbackEnv->DeleteLocalRef(a_name);
-    if (a_scn != NULL) callbackEnv->DeleteLocalRef(a_scn);
-    if (a_masid != NULL) callbackEnv->DeleteLocalRef(a_masid);
-    if (a_msgtype != NULL) callbackEnv->DeleteLocalRef(a_msgtype);
-    callbackEnv->PopLocalFrame(NULL);
-}
-
 static bt_os_callouts_t sBluetoothOsCallouts = {
     sizeof(sBluetoothOsCallouts),
     set_wake_alarm_callout,
@@ -670,10 +593,7 @@ static bt_os_callouts_t sBluetoothOsCallouts = {
     release_wake_lock_callout,
 };
 
-static btmce_callbacks_t sBluetoothMceCallbacks = {
-    sizeof(sBluetoothMceCallbacks),
-    remote_mas_instances_callback,
-};
+
 
 static void classInitNative(JNIEnv* env, jclass clazz) {
     int err;
@@ -710,9 +630,6 @@ static void classInitNative(JNIEnv* env, jclass clazz) {
     method_setWakeAlarm = env->GetMethodID(clazz, "setWakeAlarm", "(JZ)Z");
     method_acquireWakeLock = env->GetMethodID(clazz, "acquireWakeLock", "(Ljava/lang/String;)Z");
     method_releaseWakeLock = env->GetMethodID(clazz, "releaseWakeLock", "(Ljava/lang/String;)Z");
-    method_deviceMasInstancesFoundCallback = env->GetMethodID(jniCallbackClass,
-                                                    "deviceMasInstancesFoundCallback",
-                                                    "(I[B[Ljava/lang/String;[I[I[I)V");
     method_energyInfo = env->GetMethodID(clazz, "energyInfoCallback", "(IIJJJJ)V");
 
     char value[PROPERTY_VALUE_MAX];
@@ -762,16 +679,6 @@ static bool initNative(JNIEnv* env, jobject obj) {
                 ALOGE("Error getting socket interface");
         }
 
-        if ( (sBluetoothMceInterface = (btmce_interface_t *)
-                  sBluetoothInterface->get_profile_interface(BT_PROFILE_MAP_CLIENT_ID)) == NULL) {
-                ALOGE("Error getting mapclient interface");
-        } else {
-            if ( (sBluetoothMceInterface->init(&sBluetoothMceCallbacks)) != BT_STATUS_SUCCESS) {
-                ALOGE("Failed to initialize Bluetooth MCE");
-                sBluetoothMceInterface = NULL;
-            }
-        }
-
         return JNI_TRUE;
     }
     return JNI_FALSE;
@@ -1090,25 +997,6 @@ static jboolean getRemoteServicesNative(JNIEnv *env, jobject obj, jbyteArray add
     return result;
 }
 
-static jboolean getRemoteMasInstancesNative(JNIEnv *env, jobject obj, jbyteArray address) {
-    ALOGV("%s:",__FUNCTION__);
-
-    jbyte *addr = NULL;
-    jboolean result = JNI_FALSE;
-    if (!sBluetoothMceInterface) return result;
-
-    addr = env->GetByteArrayElements(address, NULL);
-    if (addr == NULL) {
-        jniThrowIOException(env, EINVAL);
-        return result;
-    }
-
-    int ret = sBluetoothMceInterface->get_remote_mas_instances((bt_bdaddr_t *)addr);
-    env->ReleaseByteArrayElements(address, addr, NULL);
-    result = (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-    return result;
-}
-
 static int connectSocketNative(JNIEnv *env, jobject object, jbyteArray address, jint type,
                                    jbyteArray uuidObj, jint channel, jint flag) {
     jbyte *addr = NULL, *uuid = NULL;
@@ -1123,10 +1011,12 @@ static int connectSocketNative(JNIEnv *env, jobject object, jbyteArray address,
         goto Fail;
     }
 
-    uuid = env->GetByteArrayElements(uuidObj, NULL);
-    if (!uuid) {
-        ALOGE("failed to get uuid");
-        goto Fail;
+    if(uuidObj != NULL) {
+        uuid = env->GetByteArrayElements(uuidObj, NULL);
+        if (!uuid) {
+            ALOGE("failed to get uuid");
+            goto Fail;
+        }
     }
 
     if ( (status = sBluetoothSocketInterface->connect((bt_bdaddr_t *) addr, (btsock_type_t) type,
@@ -1137,7 +1027,7 @@ static int connectSocketNative(JNIEnv *env, jobject object, jbyteArray address,
 
 
     if (socket_fd < 0) {
-        ALOGE("Fail to creat file descriptor on socket fd");
+        ALOGE("Fail to create file descriptor on socket fd");
         goto Fail;
     }
     env->ReleaseByteArrayElements(address, addr, 0);
@@ -1153,7 +1043,7 @@ Fail:
 
 static int createSocketChannelNative(JNIEnv *env, jobject object, jint type,
                                      jstring name_str, jbyteArray uuidObj, jint channel, jint flag) {
-    const char *service_name;
+    const char *service_name = NULL;
     jbyte *uuid = NULL;
     int socket_fd;
     bt_status_t status;
@@ -1162,12 +1052,16 @@ static int createSocketChannelNative(JNIEnv *env, jobject object, jint type,
 
     ALOGV("%s: SOCK FLAG = %x", __FUNCTION__, flag);
 
-    service_name = env->GetStringUTFChars(name_str, NULL);
+    if(name_str != NULL) {
+        service_name = env->GetStringUTFChars(name_str, NULL);
+    }
 
-    uuid = env->GetByteArrayElements(uuidObj, NULL);
-    if (!uuid) {
-        ALOGE("failed to get uuid");
-        goto Fail;
+    if(uuidObj != NULL) {
+        uuid = env->GetByteArrayElements(uuidObj, NULL);
+        if (!uuid) {
+            ALOGE("failed to get uuid");
+            goto Fail;
+        }
     }
     if ( (status = sBluetoothSocketInterface->listen((btsock_type_t) type, service_name,
                        (const uint8_t*) uuid, channel, &socket_fd, flag)) != BT_STATUS_SUCCESS) {
@@ -1186,7 +1080,6 @@ static int createSocketChannelNative(JNIEnv *env, jobject object, jint type,
 Fail:
     if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
     if (uuid) env->ReleaseByteArrayElements(uuidObj, uuid, 0);
-
     return -1;
 }
 
@@ -1235,7 +1128,6 @@ static JNINativeMethod sMethods[] = {
     {"pinReplyNative", "([BZI[B)Z", (void*) pinReplyNative},
     {"sspReplyNative", "([BIZI)Z", (void*) sspReplyNative},
     {"getRemoteServicesNative", "([B)Z", (void*) getRemoteServicesNative},
-    {"getRemoteMasInstancesNative", "([B)Z", (void*) getRemoteMasInstancesNative},
     {"connectSocketNative", "([BI[BII)I", (void*) connectSocketNative},
     {"createSocketChannelNative", "(ILjava/lang/String;[BII)I",
      (void*) createSocketChannelNative},
@@ -1323,5 +1215,11 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved)
         ALOGE("jni gatt registration failure: %d", status);
         return JNI_ERR;
     }
+
+    if ((status = android::register_com_android_bluetooth_sdp(e)) < 0) {
+        ALOGE("jni sdp registration failure: %d", status);
+        return JNI_ERR;
+    }
+
     return JNI_VERSION_1_6;
 }
diff --git a/jni/com_android_bluetooth_sdp.cpp b/jni/com_android_bluetooth_sdp.cpp
new file mode 100644 (file)
index 0000000..19c38a6
--- /dev/null
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#define LOG_TAG "BluetoothSdpJni"
+
+#define LOG_NDEBUG 0
+
+#include "com_android_bluetooth.h"
+#include "hardware/bt_sdp.h"
+#include "utils/Log.h"
+#include "android_runtime/AndroidRuntime.h"
+
+#include <string.h>
+
+
+namespace android {
+static jmethodID method_sdpRecordFoundCallback;
+static jmethodID method_sdpMasRecordFoundCallback;
+static jmethodID method_sdpMnsRecordFoundCallback;
+static jmethodID method_sdpPseRecordFoundCallback;
+static jmethodID method_sdpOppOpsRecordFoundCallback;
+
+static const btsdp_interface_t *sBluetoothSdpInterface = NULL;
+
+static void sdp_search_callback(bt_status_t status, bt_bdaddr_t *bd_addr, uint8_t* uuid_in,
+        int record_size, bluetooth_sdp_record* record);
+
+btsdp_callbacks_t sBluetoothSdpCallbacks = {
+        sizeof(sBluetoothSdpCallbacks),
+        sdp_search_callback
+};
+
+static jobject sCallbacksObj = NULL;
+static JNIEnv *sCallbackEnv = NULL;
+
+static bool checkCallbackThread() {
+    sCallbackEnv = getCallbackEnv();
+
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    if (sCallbackEnv != env || sCallbackEnv == NULL) {
+        ALOGE("Callback env check fail: env: %p, callback: %p", env, sCallbackEnv);
+        return false;
+    }
+    return true;
+}
+
+static void initializeNative(JNIEnv *env, jobject object) {
+    const bt_interface_t* btInf;
+    bt_status_t status;
+
+    if ( (btInf = getBluetoothInterface()) == NULL) {
+        ALOGE("Bluetooth module is not loaded");
+        return;
+    }
+    if (sBluetoothSdpInterface !=NULL) {
+         ALOGW("Cleaning up Bluetooth SDP Interface before initializing...");
+         sBluetoothSdpInterface->deinit();
+         sBluetoothSdpInterface = NULL;
+    }
+    if ( (sBluetoothSdpInterface = (btsdp_interface_t *)
+            btInf->get_profile_interface(BT_PROFILE_SDP_CLIENT_ID)) == NULL) {
+            ALOGE("Error getting SDP client interface");
+    }else{
+        sBluetoothSdpInterface->init(&sBluetoothSdpCallbacks);
+    }
+
+    sCallbacksObj = env->NewGlobalRef(object);
+}
+
+static void classInitNative(JNIEnv* env, jclass clazz) {
+
+    /* generic SDP record (raw data)*/
+    method_sdpRecordFoundCallback = env->GetMethodID(clazz,
+                                                    "sdpRecordFoundCallback",
+                                                    "(I[B[BI[B)V");
+
+    /* MAS SDP record*/
+    method_sdpMasRecordFoundCallback = env->GetMethodID(clazz,
+                                                    "sdpMasRecordFoundCallback",
+                                                    "(I[B[BIIIIIILjava/lang/String;Z)V");
+    /* MNS SDP record*/
+    method_sdpMnsRecordFoundCallback = env->GetMethodID(clazz,
+                                                    "sdpMnsRecordFoundCallback",
+                                                    "(I[B[BIIIILjava/lang/String;Z)V");
+    /* PBAP PSE record */
+    method_sdpPseRecordFoundCallback = env->GetMethodID(clazz,
+                                                    "sdpPseRecordFoundCallback",
+                                                    "(I[B[BIIIIILjava/lang/String;Z)V");
+    /* OPP Server record */
+    method_sdpOppOpsRecordFoundCallback = env->GetMethodID(clazz,
+                                                    "sdpOppOpsRecordFoundCallback",
+                                                    "(I[B[BIIILjava/lang/String;[BZ)V");
+
+}
+
+static jboolean sdpSearchNative(JNIEnv *env, jobject obj, jbyteArray address, jbyteArray uuidObj) {
+    ALOGD("%s:",__FUNCTION__);
+
+    jbyte *addr = NULL, *uuid = NULL;
+    jboolean result = JNI_FALSE;
+    int ret;
+    if (!sBluetoothSdpInterface)
+        goto Fail;
+
+    addr = env->GetByteArrayElements(address, NULL);
+    if (addr == NULL) {
+        jniThrowIOException(env, EINVAL);
+        goto Fail;
+    }
+    uuid = env->GetByteArrayElements(uuidObj, NULL);
+    if (!uuid) {
+        ALOGE("failed to get uuid");
+        goto Fail;
+    }
+    ALOGD("%s UUID %.*X",__FUNCTION__,16, (uint8_t*)uuid);
+
+
+    if ( (ret = sBluetoothSdpInterface->sdp_search((bt_bdaddr_t *)addr,
+                    (const uint8_t*)uuid)) != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Search initialization failed: %d", ret);
+        goto Fail;
+    }
+
+    result = (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+
+    Fail:
+    if (addr) env->ReleaseByteArrayElements(address, addr, 0);
+    if (uuid) env->ReleaseByteArrayElements(uuidObj, uuid, 0);
+    return result;
+}
+
+static void sdp_search_callback(bt_status_t status, bt_bdaddr_t *bd_addr, uint8_t* uuid_in,
+        int count, bluetooth_sdp_record* records)
+{
+
+    jbyteArray addr = NULL;
+    jbyteArray uuid = NULL;
+    jstring service_name = NULL;
+    int i = 0;
+    bluetooth_sdp_record* record;
+
+    if (!checkCallbackThread()) {
+        ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__);
+        goto clean;
+    }
+
+    addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t));
+    if (addr == NULL) goto clean;
+
+    uuid = sCallbackEnv->NewByteArray(sizeof(bt_uuid_t));
+    if (uuid == NULL) goto clean;
+
+    sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*)bd_addr);
+    sCallbackEnv->SetByteArrayRegion(uuid, 0, sizeof(bt_uuid_t), (jbyte*)uuid_in);
+
+    ALOGD("%s: Status is: %d, Record count: %d", __FUNCTION__, status, count);
+
+    // Ensure we run the loop at least once, to also signal errors if they occure
+    for(i = 0; i < count || i==0; i++) {
+        bool more_results = (i<(count-1))?true:false;
+        record = &records[i];
+        service_name = NULL;
+        if (record->hdr.service_name_length > 0) {
+            ALOGD("%s, ServiceName:  %s", __FUNCTION__, record->mas.hdr.service_name);
+            service_name = (jstring)sCallbackEnv->NewStringUTF(record->mas.hdr.service_name);
+        }
+
+        /* call the right callback according to the uuid*/
+        if (IS_UUID(UUID_MAP_MAS,uuid_in)){
+
+            sCallbackEnv->CallVoidMethod(sCallbacksObj, method_sdpMasRecordFoundCallback,
+                    (jint) status,
+                    addr,
+                    uuid,
+                    (jint)record->mas.mas_instance_id,
+                    (jint)record->mas.hdr.l2cap_psm,
+                    (jint)record->mas.hdr.rfcomm_channel_number,
+                    (jint)record->mas.hdr.profile_version,
+                    (jint)record->mas.supported_features,
+                    (jint)record->mas.supported_message_types,
+                    service_name,
+                    more_results);
+
+        }else if (IS_UUID(UUID_MAP_MNS,uuid_in)){
+
+            sCallbackEnv->CallVoidMethod(sCallbacksObj, method_sdpMnsRecordFoundCallback,
+                    (jint) status,
+                    addr,
+                    uuid,
+                    (jint)record->mns.hdr.l2cap_psm,
+                    (jint)record->mns.hdr.rfcomm_channel_number,
+                    (jint)record->mns.hdr.profile_version,
+                    (jint)record->mns.supported_features,
+                    service_name,
+                    more_results);
+
+        } else if (IS_UUID(UUID_PBAP_PSE, uuid_in)) {
+
+            sCallbackEnv->CallVoidMethod(sCallbacksObj, method_sdpPseRecordFoundCallback,
+                    (jint) status,
+                    addr,
+                    uuid,
+                    (jint)record->pse.hdr.l2cap_psm,
+                    (jint)record->pse.hdr.rfcomm_channel_number,
+                    (jint)record->pse.hdr.profile_version,
+                    (jint)record->pse.supported_features,
+                    (jint)record->pse.supported_repositories,
+                    service_name,
+                    more_results);
+
+        } else if (IS_UUID(UUID_OBEX_OBJECT_PUSH, uuid_in)) {
+
+            jint formats_list_size = record->ops.supported_formats_list_len;
+            jbyteArray formats_list = sCallbackEnv->NewByteArray(formats_list_size);
+            if (formats_list == NULL) goto clean;
+            sCallbackEnv->SetByteArrayRegion(formats_list, 0, formats_list_size,
+                    (jbyte*)record->ops.supported_formats_list);
+
+            sCallbackEnv->CallVoidMethod(sCallbacksObj, method_sdpOppOpsRecordFoundCallback,
+                    (jint) status,
+                    addr,
+                    uuid,
+                    (jint)record->ops.hdr.l2cap_psm,
+                    (jint)record->ops.hdr.rfcomm_channel_number,
+                    (jint)record->ops.hdr.profile_version,
+                    service_name,
+                    formats_list,
+                    more_results);
+            sCallbackEnv->DeleteLocalRef(formats_list);
+
+        } else {
+            // we don't have a wrapper for this uuid, send as raw data
+            jint record_data_size = record->hdr.user1_ptr_len;
+            jbyteArray record_data = NULL;
+
+            record_data = sCallbackEnv->NewByteArray(record_data_size);
+            if (record_data == NULL) goto clean;
+
+            sCallbackEnv->SetByteArrayRegion(record_data, 0, record_data_size,
+                    (jbyte*)record->hdr.user1_ptr);
+            sCallbackEnv->CallVoidMethod(sCallbacksObj, method_sdpRecordFoundCallback,
+                    (jint) status, addr, uuid, record_data_size, record_data);
+
+            sCallbackEnv->DeleteLocalRef(record_data);
+
+        }
+        // Cleanup for each iteration
+        if (service_name != NULL) {
+            sCallbackEnv->DeleteLocalRef(service_name);
+            service_name = NULL;
+        }
+    } // End of for-loop
+
+    clean:
+    if (service_name != NULL)
+        sCallbackEnv->DeleteLocalRef(service_name);
+    if (addr != NULL) sCallbackEnv->DeleteLocalRef(addr);
+    if (uuid != NULL) sCallbackEnv->DeleteLocalRef(uuid);
+    checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__);
+}
+
+static jint sdpCreateMapMasRecordNative(JNIEnv *env, jobject obj, jstring name_str, jint mas_id,
+                                         jint scn, jint l2cap_psm, jint version,
+                                         jint msg_types, jint features) {
+    ALOGD("%s:",__FUNCTION__);
+
+    const char* service_name = NULL;
+    bluetooth_sdp_record record = {}; // Must be zero initialized
+    int handle=-1;
+    int ret = 0;
+    if (!sBluetoothSdpInterface) return handle;
+
+    record.mas.hdr.type = SDP_TYPE_MAP_MAS;
+
+    if (name_str != NULL) {
+        service_name = env->GetStringUTFChars(name_str, NULL);
+        record.mas.hdr.service_name = (char *) service_name;
+        record.mas.hdr.service_name_length = strlen(service_name);
+    } else {
+        record.mas.hdr.service_name = NULL;
+        record.mas.hdr.service_name_length = 0;
+    }
+    record.mas.hdr.rfcomm_channel_number = scn;
+    record.mas.hdr.l2cap_psm = l2cap_psm;
+    record.mas.hdr.profile_version = version;
+
+    record.mas.mas_instance_id = mas_id;
+    record.mas.supported_features = features;
+    record.mas.supported_message_types = msg_types;
+
+    if ( (ret = sBluetoothSdpInterface->create_sdp_record(&record, &handle))
+            != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Create record failed: %d", ret);
+        goto Fail;
+    }
+
+    ALOGD("SDP Create record success - handle: %d", handle);
+
+    Fail:
+    if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
+    return handle;
+}
+
+static jint sdpCreateMapMnsRecordNative(JNIEnv *env, jobject obj, jstring name_str,
+                                         jint scn, jint l2cap_psm, jint version,
+                                         jint features) {
+    ALOGD("%s:",__FUNCTION__);
+
+    const char* service_name = NULL;
+    bluetooth_sdp_record record = {}; // Must be zero initialized
+    int handle=-1;
+    int ret = 0;
+    if (!sBluetoothSdpInterface) return handle;
+
+    record.mns.hdr.type = SDP_TYPE_MAP_MNS;
+
+    if (name_str != NULL) {
+        service_name = env->GetStringUTFChars(name_str, NULL);
+        record.mns.hdr.service_name = (char *) service_name;
+        record.mns.hdr.service_name_length = strlen(service_name);
+    } else {
+        record.mns.hdr.service_name = NULL;
+        record.mns.hdr.service_name_length = 0;
+    }
+    record.mns.hdr.rfcomm_channel_number = scn;
+    record.mns.hdr.l2cap_psm = l2cap_psm;
+    record.mns.hdr.profile_version = version;
+
+    record.mns.supported_features = features;
+
+    if ( (ret = sBluetoothSdpInterface->create_sdp_record(&record, &handle))
+            != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Create record failed: %d", ret);
+        goto Fail;
+    }
+
+    ALOGD("SDP Create record success - handle: %d", handle);
+
+    Fail:
+    if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
+    return handle;
+}
+
+static jint sdpCreatePbapPseRecordNative(JNIEnv *env, jobject obj, jstring name_str,
+                                         jint scn, jint l2cap_psm, jint version,
+                                         jint supported_repositories, jint features) {
+    ALOGD("%s:",__FUNCTION__);
+
+    const char* service_name = NULL;
+    bluetooth_sdp_record record = {}; // Must be zero initialized
+    int handle=-1;
+    int ret = 0;
+    if (!sBluetoothSdpInterface) return handle;
+
+    record.pse.hdr.type = SDP_TYPE_PBAP_PSE;
+
+    if (name_str != NULL) {
+        service_name = env->GetStringUTFChars(name_str, NULL);
+        record.pse.hdr.service_name = (char *) service_name;
+        record.pse.hdr.service_name_length = strlen(service_name);
+    } else {
+        record.pse.hdr.service_name = NULL;
+        record.pse.hdr.service_name_length = 0;
+    }
+    record.pse.hdr.rfcomm_channel_number = scn;
+    record.pse.hdr.l2cap_psm = l2cap_psm;
+    record.pse.hdr.profile_version = version;
+
+    record.pse.supported_features = features;
+    record.pse.supported_repositories = supported_repositories;
+
+    if ( (ret = sBluetoothSdpInterface->create_sdp_record(&record, &handle))
+            != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Create record failed: %d", ret);
+        goto Fail;
+    }
+
+    ALOGD("SDP Create record success - handle: %d", handle);
+
+    Fail:
+    if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
+    return handle;
+}
+
+static jint sdpCreateOppOpsRecordNative(JNIEnv *env, jobject obj, jstring name_str,
+                                         jint scn, jint l2cap_psm, jint version,
+                                         jbyteArray supported_formats_list) {
+    ALOGD("%s:",__FUNCTION__);
+
+    const char* service_name = NULL;
+    bluetooth_sdp_record record = {}; // Must be zero initialized
+    jbyte* formats_list;
+    int formats_list_len = 0;
+    int handle=-1;
+    int ret = 0;
+    if (!sBluetoothSdpInterface) return handle;
+
+    record.ops.hdr.type = SDP_TYPE_OPP_SERVER;
+
+    if (name_str != NULL) {
+        service_name = env->GetStringUTFChars(name_str, NULL);
+        record.ops.hdr.service_name = (char *) service_name;
+        record.ops.hdr.service_name_length = strlen(service_name);
+    } else {
+        record.ops.hdr.service_name = NULL;
+        record.ops.hdr.service_name_length = 0;
+    }
+    record.ops.hdr.rfcomm_channel_number = scn;
+    record.ops.hdr.l2cap_psm = l2cap_psm;
+    record.ops.hdr.profile_version = version;
+
+    formats_list = env->GetByteArrayElements(supported_formats_list, NULL);
+    if (formats_list != NULL) {
+        formats_list_len = env->GetArrayLength(supported_formats_list);
+        if (formats_list_len > SDP_OPP_SUPPORTED_FORMATS_MAX_LENGTH) {
+            formats_list_len = SDP_OPP_SUPPORTED_FORMATS_MAX_LENGTH;
+        }
+        memcpy(record.ops.supported_formats_list, formats_list, formats_list_len);
+    }
+
+    record.ops.supported_formats_list_len = formats_list_len;
+
+    if ( (ret = sBluetoothSdpInterface->create_sdp_record(&record, &handle))
+            != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Create record failed: %d", ret);
+        goto Fail;
+    }
+
+    ALOGD("SDP Create record success - handle: %d", handle);
+
+    Fail:
+    if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
+    if (formats_list) env->ReleaseByteArrayElements(supported_formats_list, formats_list, 0);
+    return handle;
+}
+
+static jboolean sdpRemoveSdpRecordNative(JNIEnv *env, jobject obj, jint record_id) {
+    ALOGD("%s:",__FUNCTION__);
+
+    int ret = 0;
+    if (!sBluetoothSdpInterface) return false;
+
+    if ( (ret = sBluetoothSdpInterface->remove_sdp_record(record_id))
+            != BT_STATUS_SUCCESS) {
+        ALOGE("SDP Remove record failed: %d", ret);
+        return false;
+    }
+
+    ALOGD("SDP Remove record success - handle: %d", record_id);
+    return true;
+}
+
+
+static void cleanupNative(JNIEnv *env, jobject object) {
+    const bt_interface_t* btInf;
+    bt_status_t status;
+
+    if ( (btInf = getBluetoothInterface()) == NULL) {
+        ALOGE("Bluetooth module is not loaded");
+        return;
+    }
+
+    if (sBluetoothSdpInterface !=NULL) {
+        ALOGW("Cleaning up Bluetooth SDP Interface...");
+        sBluetoothSdpInterface->deinit();
+        sBluetoothSdpInterface = NULL;
+    }
+
+    if (sCallbacksObj != NULL) {
+        ALOGW("Cleaning up Bluetooth Health object");
+        env->DeleteGlobalRef(sCallbacksObj);
+        sCallbacksObj = NULL;
+    }
+}
+
+static JNINativeMethod sMethods[] = {
+    /* name, signature, funcPtr */
+    {"classInitNative", "()V", (void *) classInitNative},
+    {"initializeNative", "()V", (void *) initializeNative},
+    {"cleanupNative", "()V", (void*) cleanupNative},
+    {"sdpSearchNative", "([B[B)Z", (void*) sdpSearchNative},
+    {"sdpCreateMapMasRecordNative", "(Ljava/lang/String;IIIIII)I",
+        (void*) sdpCreateMapMasRecordNative},
+    {"sdpCreateMapMnsRecordNative", "(Ljava/lang/String;IIII)I",
+        (void*) sdpCreateMapMnsRecordNative},
+    {"sdpCreatePbapPseRecordNative", "(Ljava/lang/String;IIIII)I",
+        (void*) sdpCreatePbapPseRecordNative},
+    {"sdpCreateOppOpsRecordNative", "(Ljava/lang/String;III[B)I",
+        (void*) sdpCreateOppOpsRecordNative},
+    {"sdpRemoveSdpRecordNative", "(I)Z", (void*) sdpRemoveSdpRecordNative}
+};
+
+int register_com_android_bluetooth_sdp(JNIEnv* env)
+{
+    return jniRegisterNativeMethods(env, "com/android/bluetooth/sdp/SdpManager",
+                                    sMethods, NELEM(sMethods));
+}
+
+
+}
@@ -1,5 +1,5 @@
 /*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -12,7 +12,8 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-package com.android.bluetooth.map;
+
+package com.android.bluetooth;
 
 import android.bluetooth.BluetoothSocket;
 
@@ -24,12 +25,14 @@ import java.io.OutputStream;
 
 import javax.obex.ObexTransport;
 
-public class BluetoothMnsRfcommTransport implements ObexTransport {
-
-    private final BluetoothSocket mSocket;
+/**
+ * Generic Obex Transport class, to be used in OBEX based Bluetooth
+ * Profiles.
+ */
+public class BluetoothObexTransport implements ObexTransport {
+    private BluetoothSocket mSocket = null;
 
-    public BluetoothMnsRfcommTransport(BluetoothSocket socket) {
-        super();
+    public BluetoothObexTransport(BluetoothSocket socket) {
         this.mSocket = socket;
     }
 
@@ -66,14 +69,34 @@ public class BluetoothMnsRfcommTransport implements ObexTransport {
     }
 
     public boolean isConnected() throws IOException {
-        // TODO: add implementation
         return true;
     }
 
+    public int getMaxTransmitPacketSize() {
+        if(mSocket.getConnectionType() != BluetoothSocket.TYPE_L2CAP) {
+            return -1;
+        }
+        return mSocket.getMaxTransmitPacketSize();
+    }
+
+    public int getMaxReceivePacketSize() {
+        if(mSocket.getConnectionType() != BluetoothSocket.TYPE_L2CAP) {
+            return -1;
+        }
+        return mSocket.getMaxReceivePacketSize();
+    }
+
     public String getRemoteAddress() {
         if (mSocket == null)
             return null;
         return mSocket.getRemoteDevice().getAddress();
     }
 
+    @Override
+    public boolean isSrmSupported() {
+        if(mSocket.getConnectionType() == BluetoothSocket.TYPE_L2CAP) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/bluetooth/IObexConnectionHandler.java b/src/com/android/bluetooth/IObexConnectionHandler.java
new file mode 100644 (file)
index 0000000..87c60b5
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+* Copyright (C) 2014 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;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+
+public interface IObexConnectionHandler {
+
+    /**
+     * Called to validate if a connection to the Bluetooth device should be accepted.
+     *
+     * @param device the connecting BluetoothDevice. Use .getType() to determine the type of
+     *         connection.
+     * @return Shall return TRUE if the connection should be accepted.
+     * FALSE otherwise
+     */
+    public boolean onConnect(BluetoothDevice device, BluetoothSocket socket);
+
+    /**
+     * Will be called in case the accept call fails.
+     * When called, at lease one of the accept threads are about to terminate.
+     * The behavior needed is to shutdown the ObexServerSockets object, and create a
+     * new one.
+     */
+    public void onAcceptFailed();
+}
diff --git a/src/com/android/bluetooth/ObexRejectServer.java b/src/com/android/bluetooth/ObexRejectServer.java
new file mode 100644 (file)
index 0000000..a68f4b8
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+* Copyright (C) 2015 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;
+
+import java.io.IOException;
+
+import javax.obex.HeaderSet;
+import javax.obex.ServerRequestHandler;
+
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * A simple ObexServer used to handle connection rejection in two cases:
+ *  - A profile cannot handle a new connection, as it is already connected to another device.
+ *  - The user rejected access to the resources needed by the profile.
+ *
+ * Will reject the OBEX connection, start a timer, and at timeout close the socket.
+ */
+public class ObexRejectServer extends ServerRequestHandler implements Callback {
+
+    private static final String TAG = "ObexRejectServer";
+    private static final boolean V = true;
+    private final int mResult;
+    private final HandlerThread mHandlerThread;
+    private final Handler mMessageHandler;
+    private final static int MSG_ID_TIMEOUT = 0x01;
+    private final static int TIMEOUT_VALUE = 5*1000; // ms
+    private final BluetoothSocket mSocket;
+
+    /**
+     * @param result the ResponseCodes.OBEX_HTTP_ code to respond to an incoming connect request.
+     */
+    public ObexRejectServer(int result, BluetoothSocket socket) {
+        super();
+        mResult = result;
+        mSocket = socket;
+        mHandlerThread = new HandlerThread("TestTimeoutHandler",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        mHandlerThread.start();
+        Looper timeoutLooper = mHandlerThread.getLooper();
+        mMessageHandler = new Handler(timeoutLooper, this);
+        // Initiate self destruction.
+        mMessageHandler.sendEmptyMessageDelayed(MSG_ID_TIMEOUT, TIMEOUT_VALUE);
+    }
+
+    // OBEX operation handlers
+    @Override
+    public int onConnect(HeaderSet request, HeaderSet reply) {
+        if(V) Log.i(TAG,"onConnect() returning error");
+        return mResult;
+    }
+
+    public void shutdown() {
+      mMessageHandler.removeCallbacksAndMessages(null);
+      mHandlerThread.quit();
+      try {
+          // This will cause an exception in the ServerSession, causing it to shut down
+          mSocket.close();
+      } catch (IOException e) {
+          Log.w(TAG, "Unable to close socket - ignoring", e);
+      }
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        if(V) Log.i(TAG,"Handling message ID: " + msg.what);
+        switch(msg.what) {
+        case MSG_ID_TIMEOUT:
+            shutdown();
+            break;
+        default:
+            // Message not handled
+            return false;
+        }
+        return true; // Message handled
+    }
+}
diff --git a/src/com/android/bluetooth/ObexServerSockets.java b/src/com/android/bluetooth/ObexServerSockets.java
new file mode 100644 (file)
index 0000000..f5b7363
--- /dev/null
@@ -0,0 +1,386 @@
+/*
+* Copyright (C) 2015 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;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+import javax.obex.ObexSession;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerSession;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.util.Log;
+
+/**
+ * Wraps multiple BluetoothServerSocket objects to make it possible to accept connections on
+ * both a RFCOMM and L2CAP channel in parallel.<br>
+ * Create an instance using {@link #create()}, which will block until the sockets have been created
+ * and channel numbers have been assigned.<br>
+ * Use {@link #getRfcommChannel()} and {@link #getL2capPsm()} to get the channel numbers to
+ * put into the SDP record.<br>
+ * Call {@link #shutdown(boolean)} to terminate the accept threads created by the call to
+ * {@link #create(IObexConnectionHandler)}.<br>
+ * A reference to an object of this type cannot be reused, and the {@link BluetoothServerSocket}
+ * object references passed to this object will be closed by this object, hence cannot be reused
+ * either (This is needed, as the only way to interrupt an accept call is to close the socket...)
+ * <br>
+ * When a connection is accepted,
+ * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
+ * If the an error occur while waiting for an incoming connection
+ * {@link IObexConnectionHandler#onConnect(BluetoothDevice, BluetoothSocket)} will be called.<br>
+ * In both cases the {@link ObexServerSockets} object have terminated, and a new must be created.
+ */
+public class ObexServerSockets {
+    private final String TAG;
+    private static final String STAG = "ObexServerSockets";
+    private static final boolean D = true; // TODO: set to false!
+    private static final int NUMBER_OF_SOCKET_TYPES = 2; // increment if LE will be supported
+
+    private final IObexConnectionHandler mConHandler;
+    /* The wrapped sockets */
+    private final BluetoothServerSocket mRfcommSocket;
+    private final BluetoothServerSocket mL2capSocket;
+    /* Handles to the accept threads. Needed for shutdown. */
+    private SocketAcceptThread mRfcommThread = null;
+    private SocketAcceptThread mL2capThread = null;
+
+    private volatile boolean mConAccepted = false;
+
+    private static volatile int sInstanceCounter = 0;
+
+    private ObexServerSockets(IObexConnectionHandler conHandler,
+            BluetoothServerSocket rfcommSocket,
+            BluetoothServerSocket l2capSocket) {
+        mConHandler = conHandler;
+        mRfcommSocket = rfcommSocket;
+        mL2capSocket = l2capSocket;
+        TAG = "ObexServerSockets" + sInstanceCounter++;
+    }
+
+    /**
+     * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket}
+     * @param validator a reference to the {@link IObexConnectionHandler} object to call
+     *                  to validate an incoming connection.
+     * @return a reference to a {@link ObexServerSockets} object instance.
+     * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s.
+     */
+    public static ObexServerSockets create(IObexConnectionHandler validator) {
+        return create(validator, BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP,
+                BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP);
+    }
+
+    /**
+     * Creates an RFCOMM {@link BluetoothServerSocket} and a L2CAP {@link BluetoothServerSocket}
+     * with specific l2cap and RFCOMM channel numbers. It is the responsibility of the caller to
+     * ensure the numbers are free and can be used, e.g. by calling {@link #getL2capPsm()} and
+     * {@link #getRfcommChannel()} in {@link ObexServerSockets}.
+     * @param validator a reference to the {@link IObexConnectionHandler} object to call
+     *                  to validate an incoming connection.
+     * @return a reference to a {@link ObexServerSockets} object instance.
+     * @throws IOException if it occurs while creating the {@link BluetoothServerSocket}s.
+     *
+     * TODO: Make public when it becomes possible to determine that the listen-call
+     *       failed due to channel-in-use.
+     */
+    private static ObexServerSockets create(IObexConnectionHandler validator,
+            int rfcommChannel, int l2capPsm) {
+        if(D) Log.d(STAG,"create(rfcomm = " +rfcommChannel + ", l2capPsm = " + l2capPsm +")");
+        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+        if(bt == null) {
+            throw new RuntimeException("No bluetooth adapter...");
+        }
+        BluetoothServerSocket rfcommSocket = null;
+        BluetoothServerSocket l2capSocket = null;
+        boolean initSocketOK = false;
+        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; i++) {
+            initSocketOK = true;
+            try {
+                if(rfcommSocket == null) {
+                    rfcommSocket = bt.listenUsingRfcommOn(rfcommChannel);
+                }
+                if(l2capSocket == null) {
+                    l2capSocket = bt.listenUsingL2capOn(l2capPsm);
+                }
+            } catch (IOException e) {
+                Log.e(STAG, "Error create ServerSockets ",e);
+                initSocketOK = false;
+            }
+            if (!initSocketOK) {
+                // Need to break out of this loop if BT is being turned off.
+                int state = bt.getState();
+                if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
+                    (state != BluetoothAdapter.STATE_ON)) {
+                    Log.w(STAG, "initServerSockets failed as BT is (being) turned off");
+                    break;
+                }
+                try {
+                    if (D) Log.v(STAG, "waiting 300 ms...");
+                    Thread.sleep(300);
+                } catch (InterruptedException e) {
+                    Log.e(STAG, "create() was interrupted");
+                }
+            } else {
+                break;
+            }
+        }
+
+        if (initSocketOK) {
+            if (D) Log.d(STAG, "Succeed to create listening sockets ");
+            ObexServerSockets sockets = new ObexServerSockets(validator, rfcommSocket, l2capSocket);
+            sockets.startAccept();
+            return sockets;
+        } else {
+            Log.e(STAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
+            return null;
+        }
+    }
+
+    /**
+     * Returns the channel number assigned to the RFCOMM socket. This will be a static value, that
+     * should be reused for multiple connections.
+     * @return the RFCOMM channel number
+     */
+    public int getRfcommChannel() {
+        return mRfcommSocket.getChannel();
+    }
+
+    /**
+     * Returns the channel number assigned to the L2CAP socket. This will be a static value, that
+     * should be reused for multiple connections.
+     * @return the L2CAP channel number
+     */
+    public int getL2capPsm() {
+        return mL2capSocket.getChannel();
+    }
+
+    /**
+     * Initiate the accept threads.
+     * Will create a thread for each socket type. an incoming connection will be signaled to
+     * the {@link IObexConnectionValidator#onConnect()}, at which point both threads will exit.
+     */
+    private void startAccept() {
+        if(D) Log.d(TAG,"startAccept()");
+        prepareForNewConnect();
+
+        mRfcommThread = new SocketAcceptThread(mRfcommSocket);
+        mRfcommThread.start();
+
+        mL2capThread = new SocketAcceptThread(mL2capSocket);
+        mL2capThread.start();
+    }
+
+    /**
+     * Set state to accept new incoming connection. Will cause the next incoming connection to be
+     * Signaled through {@link IObexConnectionValidator#onConnect()};
+     */
+    public void prepareForNewConnect() {
+        if(D) Log.d(TAG, "prepareForNewConnect()");
+        mConAccepted = false;
+    }
+
+    /**
+     * Called from the AcceptThreads to signal an incoming connection.
+     * This is the entry point that needs to synchronize between the accept
+     * threads, and ensure only a single connection is accepted.
+     * {@link mAcceptedSocket} is used a state variable.
+     * @param device the connecting device.
+     * @param conSocket the socket associated with the connection.
+     * @return true if the connection is accepted, false otherwise.
+     */
+    synchronized private boolean onConnect(BluetoothDevice device, BluetoothSocket conSocket) {
+        if(D) Log.d(TAG, "onConnect() socket: " + conSocket + " mConAccepted = " + mConAccepted);
+        if(mConAccepted  == false && mConHandler.onConnect(device, conSocket) == true) {
+            mConAccepted = true; // TODO: Reset this when ready to accept new connection
+            /* Signal the remaining threads to stop.
+            shutdown(false); */ // UPDATE: TODO: remove - redesigned to keep running...
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Signal to the {@link IObexConnectionHandler} that an error have occurred.
+     */
+    synchronized private void onAcceptFailed() {
+        Log.w(TAG,"onAcceptFailed() calling shutdown...");
+        mConHandler.onAcceptFailed();
+        shutdown(false);
+    }
+
+    /**
+     * Terminate any running accept threads
+     * @param block Set true to block the calling thread until the AcceptThreads
+     * has ended execution
+     */
+    synchronized public void shutdown(boolean block) {
+        if(D) Log.d(TAG, "shutdown(block = " + block + ")");
+        if(mRfcommThread != null) {
+            mRfcommThread.shutdown();
+        }
+        if(mL2capThread != null){
+            mL2capThread.shutdown();
+        }
+        if(block == true) {
+            while(mRfcommThread != null || mL2capThread != null) {
+                try {
+                    if(mRfcommThread != null) {
+                        mRfcommThread.join();
+                        mRfcommThread = null;
+                    }
+                    if(mL2capThread != null) {
+                        mL2capThread.join();
+                        mL2capThread = null;
+                    }
+                } catch (InterruptedException e) {
+                    Log.i(TAG, "shutdown() interrupted, continue waiting...", e);
+                }
+            }
+        } else {
+            mRfcommThread = null;
+            mL2capThread = null;
+        }
+    }
+
+    /**
+     * A thread that runs in the background waiting for remote an incoming
+     * connect. Once a remote socket connects, this thread will be
+     * shutdown. When the remote disconnect, this thread shall be restarted to
+     * accept a new connection.
+     */
+    private class SocketAcceptThread extends Thread {
+
+        private boolean mStopped = false;
+        private final BluetoothServerSocket mServerSocket;
+
+        /**
+         * Create a SocketAcceptThread
+         * @param serverSocket shall never be null.
+         * @param latch shall never be null.
+         * @throws IllegalArgumentException
+         */
+        public SocketAcceptThread(BluetoothServerSocket serverSocket) {
+            if(serverSocket == null) {
+                throw new IllegalArgumentException("serverSocket cannot be null");
+            }
+            mServerSocket = serverSocket;
+        }
+
+        /**
+         * Run until shutdown of BT.
+         * Accept incoming connections and reject if needed. Keep accepting incoming connections.
+         */
+        @Override
+        public void run() {
+            try {
+                while (!mStopped) {
+                    BluetoothSocket connSocket;
+                    BluetoothDevice device;
+
+                    try {
+                        if (D) Log.d(TAG, "Accepting socket connection...");
+
+                        connSocket = mServerSocket.accept();
+                        if (D) Log.d(TAG, "Accepted socket connection from: " + mServerSocket);
+
+                       if (connSocket == null) {
+                           // TODO: Do we need a max error count, to avoid spinning?
+                            Log.w(TAG, "connSocket is null - reattempt accept");
+                            continue;
+                        }
+                        device = connSocket.getRemoteDevice();
+
+                        if (device == null) {
+                            Log.i(TAG, "getRemoteDevice() = null - reattempt accept");
+                            try{
+                                connSocket.close();
+                            } catch (IOException e) {
+                                Log.w(TAG, "Error closing the socket. ignoring...",e );
+                            }
+                            continue;
+                        }
+
+                        /* Signal to the service that we have received an incoming connection.
+                         */
+                        boolean isValid = ObexServerSockets.this.onConnect(device, connSocket);
+
+                        if(isValid == false) {
+                            /* Close connection if we already have a connection with another device
+                             * by responding to the OBEX connect request.
+                             */
+                            Log.i(TAG, "RemoteDevice is invalid - creating ObexRejectServer.");
+                            BluetoothObexTransport obexTrans =
+                                    new BluetoothObexTransport(connSocket);
+                            // Create and detach a selfdestructing ServerSession to respond to any
+                            // incoming OBEX signals.
+                            new ServerSession(obexTrans,
+                                    new ObexRejectServer(
+                                            ResponseCodes.OBEX_HTTP_UNAVAILABLE,
+                                            connSocket),
+                                    null);
+                            // now wait for a new connect
+                        } else {
+                            // now wait for a new connect
+                        }
+                    } catch (IOException ex) {
+                        if(mStopped == true) {
+                            // Expected exception because of shutdown.
+                        } else {
+                            Log.w(TAG, "Accept exception for " +
+                                    mServerSocket, ex);
+                            ObexServerSockets.this.onAcceptFailed();
+                        }
+                        mStopped=true;
+                    }
+                } // End while()
+            } finally {
+                if (D) Log.d(TAG, "AcceptThread ended for: " + mServerSocket);
+            }
+        }
+
+        /**
+         * Shuts down the accept threads, and closes the ServerSockets, causing all related
+         * BluetoothSockets to disconnect, hence do not call until all all accepted connections
+         * are ready to be disconnected.
+         */
+        public void shutdown() {
+            if(mStopped == false) {
+                mStopped = true;
+                // TODO: According to the documentation, this should not close the accepted
+                //       sockets - and that is true, but it closes the l2cap connections, and
+                //       therefore it implicitly also closes the accepted sockets...
+                try {
+                     mServerSocket.close();
+                } catch (IOException e) {
+                    if(D) Log.d(TAG, "Exception while thread shutdown:", e);
+                }
+            }
+            // If called from another thread, interrupt the thread
+            if(!Thread.currentThread().equals(this)){
+                // TODO: Will this interrupt the thread if it is blocked in synchronized?
+                // Else: change to use InterruptableLock
+                if(D) Log.d(TAG, "shutdown called from another thread - interrupt().");
+                interrupt();
+            }
+        }
+    }
+
+}
index 2a5dfc7..3363855 100644 (file)
@@ -61,9 +61,10 @@ import com.android.bluetooth.hid.HidService;
 import com.android.bluetooth.hfp.HeadsetService;
 import com.android.bluetooth.hdp.HealthService;
 import com.android.bluetooth.pan.PanService;
+import com.android.bluetooth.sdp.SdpManager;
+import com.android.internal.R;
 import com.android.bluetooth.Utils;
 import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
-import com.android.internal.R;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -107,7 +108,7 @@ public class AdapterService extends Service {
     private static final String ACTION_ALARM_WAKEUP =
         "com.android.bluetooth.btservice.action.ALARM_WAKEUP";
 
-    static final String BLUETOOTH_ADMIN_PERM =
+    public static final String BLUETOOTH_ADMIN_PERM =
         android.Manifest.permission.BLUETOOTH_ADMIN;
     public static final String BLUETOOTH_PRIVILEGED =
                 android.Manifest.permission.BLUETOOTH_PRIVILEGED;
@@ -165,6 +166,10 @@ public class AdapterService extends Service {
     private BondStateMachine mBondStateMachine;
     private JniCallbacks mJniCallbacks;
     private RemoteDevices mRemoteDevices;
+
+    /* TODO: Consider to remove the search API from this class, if changed to use call-back */
+    private SdpManager mSdpManager = null;
+
     private boolean mProfilesStarted;
     private boolean mNativeAvailable;
     private boolean mCleaningUp;
@@ -359,7 +364,11 @@ public class AdapterService extends Service {
         mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
         mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
 
+        mSdpManager = SdpManager.init(this);
         registerReceiver(mAlarmBroadcastReceiver, new IntentFilter(ACTION_ALARM_WAKEUP));
+
+
+
     }
 
     @Override
@@ -473,6 +482,11 @@ public class AdapterService extends Service {
             mRemoteDevices.cleanup();
         }
 
+        if(mSdpManager != null) {
+            mSdpManager.cleanup();
+            mSdpManager = null;
+        }
+
         if (mNativeAvailable) {
             debugLog("cleanup() - Cleaning up adapter native");
             cleanupNative();
@@ -654,7 +668,6 @@ public class AdapterService extends Service {
                 Log.w(TAG, "enable() - Not allowed for non-active user and non system user");
                 return false;
             }
-
             AdapterService service = getService();
             if (service == null) return false;
             return service.enable();
@@ -954,16 +967,7 @@ public class AdapterService extends Service {
             return service.fetchRemoteUuids(device);
         }
 
-        public boolean fetchRemoteMasInstances(BluetoothDevice device) {
-            if (!Utils.checkCaller()) {
-                Log.w(TAG,"fetchMasInstances(): not allowed for non-active user");
-                return false;
-            }
 
-            AdapterService service = getService();
-            if (service == null) return false;
-            return service.fetchRemoteMasInstances(device);
-        }
 
         public boolean setPin(BluetoothDevice device, boolean accept, int len, byte[] pinCode) {
             if (!Utils.checkCaller()) {
@@ -1072,6 +1076,16 @@ public class AdapterService extends Service {
             if (service == null) return null;
             return service.createSocketChannel(type, serviceName, uuid, port, flag);
         }
+        public boolean sdpSearch(BluetoothDevice device, ParcelUuid uuid) {
+            if (!Utils.checkCaller()) {
+                Log.w(TAG,"sdpSea(): not allowed for non-active user");
+                return false;
+            }
+
+            AdapterService service = getService();
+            if (service == null) return false;
+            return service.sdpSearch(device,uuid);
+        }
 
         public boolean configHciSnoopLog(boolean enable) {
             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
@@ -1303,6 +1317,16 @@ public class AdapterService extends Service {
 
         return mAdapterProperties.getProfileConnectionState(profile);
     }
+     boolean sdpSearch(BluetoothDevice device,ParcelUuid uuid) {
+         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+         if(mSdpManager != null) {
+             mSdpManager.sdpSearch(device,uuid);
+             return true;
+         } else {
+             return false;
+         }
+     }
+
 
      boolean createBond(BluetoothDevice device, int transport) {
         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
@@ -1537,11 +1561,6 @@ public class AdapterService extends Service {
         return true;
     }
 
-      boolean fetchRemoteMasInstances(BluetoothDevice device) {
-         enforceCallingOrSelfPermission(RECEIVE_MAP_PERM, "Need RECEIVE BLUETOOTH MAP permission");
-         mRemoteDevices.fetchMasInstances(device);
-         return true;
-     }
 
      boolean setPin(BluetoothDevice device, boolean accept, int len, byte[] pinCode) {
         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
@@ -1917,6 +1936,7 @@ public class AdapterService extends Service {
     /*package*/ native boolean createBondNative(byte[] address, int transport);
     /*package*/ native boolean removeBondNative(byte[] address);
     /*package*/ native boolean cancelBondNative(byte[] address);
+    /*package*/ native boolean sdpSearchNative(byte[] address, byte[] uuid);
 
     /*package*/ native int getConnectionStateNative(byte[] address);
 
index d6eab6c..4781776 100644 (file)
@@ -67,7 +67,7 @@ final class JniCallbacks {
     }
 
     void aclStateChangeCallback(int status, byte[] address, int newState) {
-               mRemoteDevices.aclStateChangeCallback(status, address, newState);
+        mRemoteDevices.aclStateChangeCallback(status, address, newState);
     }
 
     void stateChangeCallback(int status) {
@@ -82,8 +82,4 @@ final class JniCallbacks {
         mAdapterProperties.adapterPropertyChangedCallback(types, val);
     }
 
-    void deviceMasInstancesFoundCallback(int status, byte[] address, String[] name, int[] scn,
-            int[] id, int[] msgtype) {
-        mRemoteDevices.deviceMasInstancesFoundCallback(status, address, name, scn, id, msgtype);
-    }
 }
index 790cf6d..dd752df 100644 (file)
@@ -19,8 +19,6 @@ package com.android.bluetooth.btservice;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothMasInstance;
-import android.content.Context;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.Message;
@@ -28,13 +26,10 @@ import android.os.ParcelUuid;
 import android.util.Log;
 
 import com.android.bluetooth.Utils;
-import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
 
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.LinkedList;
 
 
 final class RemoteDevices {
@@ -45,23 +40,17 @@ final class RemoteDevices {
     private static BluetoothAdapter mAdapter;
     private static AdapterService mAdapterService;
     private static ArrayList<BluetoothDevice> mSdpTracker;
-    private static ArrayList<BluetoothDevice> mSdpMasTracker;
-
     private Object mObject = new Object();
 
     private static final int UUID_INTENT_DELAY = 6000;
     private static final int MESSAGE_UUID_INTENT = 1;
 
-    private static final int MAS_INSTANCE_INTENT_DELAY = 6000;
-    private static final int MESSAGE_MAS_INSTANCE_INTENT = 2;
-
     private HashMap<BluetoothDevice, DeviceProperties> mDevices;
 
     RemoteDevices(AdapterService service) {
         mAdapter = BluetoothAdapter.getDefaultAdapter();
         mAdapterService = service;
         mSdpTracker = new ArrayList<BluetoothDevice>();
-        mSdpMasTracker = new ArrayList<BluetoothDevice>();
         mDevices = new HashMap<BluetoothDevice, DeviceProperties>();
     }
 
@@ -70,9 +59,6 @@ final class RemoteDevices {
         if (mSdpTracker !=null)
             mSdpTracker.clear();
 
-        if (mSdpMasTracker != null)
-            mSdpMasTracker.clear();
-
         if (mDevices != null)
             mDevices.clear();
     }
@@ -236,17 +222,6 @@ final class RemoteDevices {
     }
 
 
-    private void sendMasInstanceIntent(BluetoothDevice device,
-            ArrayList<BluetoothMasInstance> instances) {
-        Intent intent = new Intent(BluetoothDevice.ACTION_MAS_INSTANCE);
-
-        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-        if (instances != null)  intent.putExtra(BluetoothDevice.EXTRA_MAS_INSTANCE, instances);
-        mAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM);
-
-        //Remove the outstanding UUID request
-        mSdpMasTracker.remove(device);
-    }
     void devicePropertyChangedCallback(byte[] address, int[] types, byte[][] values) {
         Intent intent;
         byte[] val;
@@ -264,7 +239,8 @@ final class RemoteDevices {
             type = types[j];
             val = values[j];
             if(val.length <= 0)
-                errorLog("devicePropertyChangedCallback: bdDevice: " + bdDevice + ", value is empty for type: " + type);
+                errorLog("devicePropertyChangedCallback: bdDevice: " + bdDevice 
+                        + ", value is empty for type: " + type);
             else {
                 synchronized(mObject) {
                     switch (type) {
@@ -350,7 +326,7 @@ final class RemoteDevices {
 
         DeviceProperties prop = getDeviceProperties(device);
         if (prop == null) {
           errorLog("aclStateChangeCallback reported unknown device " + Arrays.toString(address));
//         errorLog("aclStateChangeCallback reported unknown device " + Arrays.toString(address));
         }
         Intent intent = null;
         if (newState == AbstractionLayer.BT_ACL_STATE_CONNECTED) {
@@ -365,30 +341,7 @@ final class RemoteDevices {
         mAdapterService.sendBroadcast(intent, mAdapterService.BLUETOOTH_PERM);
     }
 
-    void deviceMasInstancesFoundCallback(int status, byte[] address, String[] name, int[] scn,
-            int[] id, int[] msgtype) {
-        BluetoothDevice device = getDevice(address);
-
-        if (device == null) {
-            errorLog("deviceMasInstancesFoundCallback: Device is NULL");
-            return;
-        }
-
-        debugLog("deviceMasInstancesFoundCallback: found " + name.length + " instances");
-
-        ArrayList<BluetoothMasInstance> instances = new ArrayList<BluetoothMasInstance>();
-
-        for (int i = 0; i < name.length; i++) {
-            BluetoothMasInstance inst = new BluetoothMasInstance(id[i], name[i],
-                    scn[i], msgtype[i]);
-
-            debugLog(inst.toString());
 
-            instances.add(inst);
-        }
-
-        sendMasInstanceIntent(device, instances);
-    }
 
     void fetchUuids(BluetoothDevice device) {
         if (mSdpTracker.contains(device)) return;
@@ -398,21 +351,9 @@ final class RemoteDevices {
         message.obj = device;
         mHandler.sendMessageDelayed(message, UUID_INTENT_DELAY);
 
-        //mAdapterService.getDevicePropertyNative(Utils.getBytesFromAddress(device.getAddress()), AbstractionLayer.BT_PROPERTY_UUIDS);
         mAdapterService.getRemoteServicesNative(Utils.getBytesFromAddress(device.getAddress()));
     }
 
-    void fetchMasInstances(BluetoothDevice device) {
-        if (mSdpMasTracker.contains(device)) return;
-        mSdpMasTracker.add(device);
-
-        Message message = mHandler.obtainMessage(MESSAGE_MAS_INSTANCE_INTENT);
-        message.obj = device;
-        mHandler.sendMessageDelayed(message, MAS_INSTANCE_INTENT_DELAY);
-
-        mAdapterService.getRemoteMasInstancesNative(Utils.getBytesFromAddress(device.getAddress()));
-    }
-
     private final Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -423,12 +364,6 @@ final class RemoteDevices {
                     sendUuidIntent(device);
                 }
                 break;
-            case MESSAGE_MAS_INSTANCE_INTENT:
-                BluetoothDevice dev = (BluetoothDevice)msg.obj;
-                if (dev != null) {
-                    sendMasInstanceIntent(dev, null);
-                }
-                break;
             }
         }
     };
index 64caba4..449b934 100644 (file)
@@ -18,6 +18,11 @@ import java.io.IOException;
 
 import javax.obex.ServerSession;
 
+import com.android.bluetooth.BluetoothObexTransport;
+import com.android.bluetooth.IObexConnectionHandler;
+import com.android.bluetooth.ObexServerSockets;
+import com.android.bluetooth.sdp.SdpManager;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothServerSocket;
@@ -29,7 +34,7 @@ import android.os.Handler;
 import android.os.RemoteException;
 import android.util.Log;
 
-public class BluetoothMapMasInstance {
+public class BluetoothMapMasInstance implements IObexConnectionHandler {
     private static final String TAG = "BluetoothMapMasInstance";
 
     private static final boolean D = BluetoothMapService.DEBUG;
@@ -40,12 +45,16 @@ public class BluetoothMapMasInstance {
     private static final int SDP_MAP_MSG_TYPE_SMS_CDMA = 0x04;
     private static final int SDP_MAP_MSG_TYPE_MMS      = 0x08;
 
-    private SocketAcceptThread mAcceptThread = null;
+    private static final int SDP_MAP_MAS_VERSION       = 0x0102;
+
+    /* TODO: Should these be adaptive for each MAS? - e.g. read from app? */
+    private static final int SDP_MAP_MAS_FEATURES      = 0x0000007F;
 
     private ServerSession mServerSession = null;
 
     // The handle to the socket registration with SDP
-    private BluetoothServerSocket mServerSocket = null;
+    private ObexServerSockets mServerSockets = null;
+    private int mSdpHandle = -1;
 
     // The actual incoming connection handle
     private BluetoothSocket mConnSocket = null;
@@ -66,6 +75,12 @@ public class BluetoothMapMasInstance {
     private boolean mEnableSmsMms = false;
     BluetoothMapContentObserver mObserver;
 
+    private int mRemoteFeatureMask = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK;
+
+    public static final String TYPE_SMS_MMS_STR = "SMS/MMS";
+    public static final String TYPE_EMAIL_STR = "EMAIL";
+    public static final String TYPE_IM_STR = "IM";
+
     /**
      * Create a e-mail MAS instance
      * @param callback
@@ -103,175 +118,79 @@ public class BluetoothMapMasInstance {
         return mMasInstanceId;
     }
 
-    /**
-     * 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 {
+    public void startRfcommSocketListener() {
+        if (D) Log.d(TAG, "Map Service startRfcommSocketListener");
+
+        if (mServerSession != null) {
+            if (D) Log.d(TAG, "mServerSession exists - shutting it down...");
+            mServerSession.close();
+            mServerSession = null;
+        }
+        if (mObserver != null) {
+            if (D) Log.d(TAG, "mObserver exists - shutting it down...");
+            mObserver.deinit();
+            mObserver = null;
+        }
 
-        private boolean stopped = false;
+        closeConnectionSocket();
 
-        @Override
-        public void run() {
-            BluetoothServerSocket serverSocket;
-            if (mServerSocket == null) {
-                if (!initSocket()) {
-                    return;
-                }
-            }
+        if(mServerSockets != null) {
+            mServerSockets.prepareForNewConnect();
+        } else {
 
-            while (!stopped) {
-                try {
-                    if (D) Log.d(TAG, "Accepting socket connection...");
-                    serverSocket = mServerSocket;
-                    if(serverSocket == null) {
-                        Log.w(TAG, "mServerSocket is null");
-                        break;
-                    }
-                    mConnSocket = serverSocket.accept();
-                    if (D) Log.d(TAG, "Accepted socket connection...");
-
-                    synchronized (BluetoothMapMasInstance.this) {
-                        if (mConnSocket == null) {
-                            Log.w(TAG, "mConnSocket is null");
-                            break;
-                        }
-                        mRemoteDevice = mConnSocket.getRemoteDevice();
-                    }
-
-                    if (mRemoteDevice == null) {
-                        Log.i(TAG, "getRemoteDevice() = null");
-                        break;
-                    }
-
-                    /* Signal to the service that we have received an incoming connection.
-                     */
-                    boolean isValid = mMapService.onConnect(mRemoteDevice, BluetoothMapMasInstance.this);
-
-                    if(isValid == false) {
-                        // Close connection if we already have a connection with another device
-                        Log.i(TAG, "RemoteDevice is invalid - closing.");
-                        mConnSocket.close();
-                        mConnSocket = null;
-                        // now wait for a new connect
-                    } else {
-                        stopped = true; // job done ,close this thread;
-                    }
-                } catch (IOException ex) {
-                    stopped=true;
-                    if (D) Log.v(TAG, "Accept exception: (expected at shutdown)", ex);
-                }
-            }
-        }
+            mServerSockets = ObexServerSockets.create(this);
 
-        void shutdown() {
-            stopped = true;
-            if(mServerSocket != null) {
-                try {
-                    mServerSocket.close();
-                } catch (IOException e) {
-                    if(D) Log.d(TAG, "Exception while thread shurdown:", e);
-                } finally {
-                    mServerSocket = null;
-                }
+            if(mServerSockets == null) {
+                // TODO: Handle - was not handled before
+                Log.e(TAG, "Failed to start the listeners");
+                return;
             }
-            interrupt();
+            if(mSdpHandle > 0) {
+                SdpManager.getDefaultManager().removeSdpRecord(mSdpHandle);
+            }
+            mSdpHandle = createMasSdpRecord(mServerSockets.getRfcommChannel(),
+                    mServerSockets.getL2capPsm());
         }
     }
 
-    public void startRfcommSocketListener() {
-        if (D) Log.d(TAG, "Map Service startRfcommSocketListener");
-        mInterrupted = false; /* For this to work all calls to this function
-                                 and shutdown() must be from same thread. */
-        if (mAcceptThread == null) {
-            mAcceptThread = new SocketAcceptThread();
-            mAcceptThread.setName("BluetoothMapAcceptThread masId=" + mMasInstanceId);
-            mAcceptThread.start();
+    /**
+     * Create the MAS SDP record with the information stored in the instance.
+     * @param rfcommChannel the rfcomm channel ID
+     * @param l2capPsm the l2capPsm - set to -1 to exclude
+     */
+    private int createMasSdpRecord(int rfcommChannel, int l2capPsm) {
+        String masName = "";
+        int messageTypeFlags = 0;
+        if(mEnableSmsMms) {
+            masName = TYPE_SMS_MMS_STR;
+            messageTypeFlags |= SDP_MAP_MSG_TYPE_SMS_GSM |
+//                           SDP_MAP_MSG_TYPE_SMS_CDMA|
+                           SDP_MAP_MSG_TYPE_MMS;
         }
-    }
-
-    private final boolean initSocket() {
-        if (D) Log.d(TAG, "MAS initSocket()");
-
-        boolean initSocketOK = false;
-        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++) {
-            initSocketOK = true;
-            try {
-                // It is mandatory for MSE to support initiation of bonding and
-                // encryption.
-                String masId = String.format("%02x", mMasInstanceId & 0xff);
-                String masName = "";
-                int messageTypeFlags = 0;
-                if(mEnableSmsMms) {
-                    masName = "SMS/MMS";
-                    messageTypeFlags |= SDP_MAP_MSG_TYPE_SMS_GSM |
-                                   SDP_MAP_MSG_TYPE_SMS_CDMA|
-                                   SDP_MAP_MSG_TYPE_MMS;
-                }
-                if(mBaseEmailUri != null) {
-                    if(mEnableSmsMms) {
-                        masName += "/EMAIL";
-                    } else {
-                        masName = mAccount.getName();
-                    }
-                    messageTypeFlags |= SDP_MAP_MSG_TYPE_EMAIL;
-                }
-                String msgTypes = String.format("%02x", messageTypeFlags & 0xff);
-                String sdpString = masId + msgTypes + masName;
-                if(V) Log.d(TAG, "  masId = " + masId +
-                                 "\n  msgTypes = " + msgTypes +
-                                 "\n  masName = " + masName +
-                                 "\n  SDP string = " + sdpString);
-                mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord
-                    (sdpString, BluetoothUuid.MAS.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;
-                }
-                try {
-                    if (V) Log.v(TAG, "waiting 300 ms...");
-                    Thread.sleep(300);
-                } catch (InterruptedException e) {
-                    Log.e(TAG, "socketAcceptThread thread was interrupted (3)");
-                }
+        if(mBaseEmailUri != null) {
+            if(mEnableSmsMms) {
+                masName += "/" + TYPE_EMAIL_STR;
             } else {
-                break;
+                masName = mAccount.getName();
             }
+            messageTypeFlags |= SDP_MAP_MSG_TYPE_EMAIL;
         }
-        if (mInterrupted) {
-            initSocketOK = false;
-            // close server socket to avoid resource leakage
-            closeServerSocket();
-        }
-
-        if (initSocketOK) {
-            if (V) Log.v(TAG, "Succeed to create listening socket ");
 
-        } else {
-            Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
-        }
-        return initSocketOK;
+        return SdpManager.getDefaultManager().createMapMasRecord(masName,
+                mMasInstanceId,
+                rfcommChannel,
+                l2capPsm,
+                SDP_MAP_MAS_VERSION,
+                messageTypeFlags,
+                SDP_MAP_MAS_FEATURES);
     }
 
     /* Called for all MAS instances for each instance when auth. is completed, hence
      * must check if it has a valid connection before creating a session.
      * Returns true at success. */
-    public boolean startObexServerSession(BluetoothMnsObexClient mnsClient) throws IOException, RemoteException {
+    public boolean startObexServerSession(BluetoothMnsObexClient mnsClient)
+            throws IOException, RemoteException {
         if (D) Log.d(TAG, "Map Service startObexServerSession masid = " + mMasInstanceId);
 
         if (mConnSocket != null) {
@@ -279,6 +198,7 @@ public class BluetoothMapMasInstance {
                 // Already connected, just return true
                 return true;
             }
+
             mMnsClient = mnsClient;
             BluetoothMapObexServer mapServer;
             mObserver = new  BluetoothMapContentObserver(mContext,
@@ -293,8 +213,9 @@ public class BluetoothMapMasInstance {
                                                     mMasInstanceId,
                                                     mAccount,
                                                     mEnableSmsMms);
-            // setup RFCOMM transport
-            BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket);
+
+            // setup transport
+            BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket);
             mServerSession = new ServerSession(transport, mapServer, null);
             if (D) Log.d(TAG, "    ServerSession started.");
 
@@ -330,44 +251,27 @@ public class BluetoothMapMasInstance {
             mObserver.deinit();
             mObserver = null;
         }
-        mInterrupted = true;
-        if(mAcceptThread != null) {
-            mAcceptThread.shutdown();
-            try {
-                mAcceptThread.join();
-            } catch (InterruptedException e) {/* Not much we can do about this*/}
-            mAcceptThread = null;
-        }
 
         closeConnectionSocket();
+
+        closeServerSockets(true);
     }
 
     /**
-     * Stop a running server session or cleanup, and start a new
-     * RFComm socket listener thread.
+     * Signal to the ServerSockets handler that a new connection may be accepted.
      */
     public void restartObexServerSession() {
-        if (D) Log.d(TAG, "MAP Service stopObexServerSession");
-
-        shutdown();
-
-        // Last obex transaction is finished, we start to listen for incoming
-        // connection again -
+        if (D) Log.d(TAG, "MAP Service restartObexServerSession()");
         startRfcommSocketListener();
     }
 
 
-    private final synchronized void closeServerSocket() {
+    private final synchronized void closeServerSockets(boolean block) {
         // exit SocketAcceptThread early
-        if (mServerSocket != null) {
-            try {
-                // this will cause mServerSocket.accept() return early with IOException
-                mServerSocket.close();
-            } catch (IOException ex) {
-                Log.e(TAG, "Close Server Socket error: " + ex);
-            } finally {
-                mServerSocket = null;
-            }
+        ObexServerSockets sockets = mServerSockets;
+        if (sockets != null) {
+            sockets.shutdown(block);
+            mServerSockets = null;
         }
     }
 
@@ -376,11 +280,46 @@ public class BluetoothMapMasInstance {
             try {
                 mConnSocket.close();
             } catch (IOException e) {
-                Log.e(TAG, "Close Connection Socket error: " + e.toString());
+                Log.e(TAG, "Close Connection Socket error: ", e);
             } finally {
                 mConnSocket = null;
             }
         }
     }
 
+    public void setRemoteFeatureMask(int supported_features) {
+        mRemoteFeatureMask  = supported_features;
+    }
+
+    public int getRemoteFeatureMask(){
+        return this.mRemoteFeatureMask;
+    }
+
+    @Override
+    public synchronized boolean onConnect(BluetoothDevice device, BluetoothSocket socket) {
+        /* Signal to the service that we have received an incoming connection.
+         */
+        boolean isValid = mMapService.onConnect(device, BluetoothMapMasInstance.this);
+
+        if(isValid == true) {
+            mRemoteDevice = device;
+            mConnSocket = socket;
+        }
+
+        return isValid;
+    }
+
+    /**
+     * Called when an unrecoverable error occurred in an accept thread.
+     * Close down the server socket, and restart.
+     * TODO: Change to message, to call start in correct context.
+     */
+    @Override
+    public synchronized void onAcceptFailed() {
+        mServerSockets = null; // Will cause a new to be created when calling start.
+        Log.e(TAG,"Failed to accept incomming connection - restarting");
+        startRfcommSocketListener();
+
+    }
+
 }
index 7b321d1..f0cdb2d 100644 (file)
@@ -92,6 +92,8 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
     private Uri mEmailFolderUri = null;
 
     private int mMasId = 0;
+    // updated during connect if remote has alternative value
+    private int mRemoteFeatureMask = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK;
 
     private boolean mEnableSmsMms = false;
     private boolean mThreadIdSupport = false; // true if peer supports threadId in msg listing
@@ -225,6 +227,12 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
     }
 
     @Override
+    public boolean isSrmSupported() {
+        // TODO: Update based on the transport used
+        return true;
+    }
+
+    @Override
     public int onConnect(final HeaderSet request, HeaderSet reply) {
         if (D) Log.d(TAG, "onConnect():");
         if (V) logHeader(request);
index 12c294b..7e58b5b 100755 (executable)
 
 package com.android.bluetooth.map;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Set;
-
-import javax.obex.ServerSession;
-
 import android.app.AlarmManager;
-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.BluetoothMap;
 import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.IBluetoothMap;
 import android.bluetooth.BluetoothUuid;
-import android.bluetooth.BluetoothMap;
-import android.bluetooth.BluetoothSocket;
+import android.bluetooth.IBluetoothMap;
+import android.bluetooth.SdpMnsRecord;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.IntentFilter.MalformedMimeTypeException;
 import android.os.Handler;
 import android.os.Message;
-import android.os.PowerManager;
 import android.os.ParcelUuid;
+import android.os.PowerManager;
 import android.os.RemoteException;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
-import android.provider.Settings;
-import android.provider.Telephony.Sms;
-import android.content.IntentFilter;
-import android.content.BroadcastReceiver;
-import android.database.ContentObserver;
 
-import com.android.bluetooth.R;
 import com.android.bluetooth.Utils;
 import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder;
-import com.android.bluetooth.opp.BluetoothOppTransferHistory;
-import com.android.bluetooth.opp.BluetoothShare;
-import com.android.bluetooth.opp.Constants;
+import com.android.bluetooth.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
 
 public class BluetoothMapService extends ProfileService {
     private static final String TAG = "BluetoothMapService";
@@ -87,7 +74,8 @@ public class BluetoothMapService extends ProfileService {
     private static final int USER_CONFIRM_TIMEOUT_VALUE = 25000;
 
     /** Intent indicating that the email settings activity should be opened*/
-    public static final String ACTION_SHOW_MAPS_EMAIL_SETTINGS = "android.btmap.intent.action.SHOW_MAPS_EMAIL_SETTINGS";
+    public static final String ACTION_SHOW_MAPS_EMAIL_SETTINGS =
+            "android.btmap.intent.action.SHOW_MAPS_EMAIL_SETTINGS";
 
     public static final int MSG_SERVERSESSION_CLOSE = 5000;
 
@@ -151,6 +139,8 @@ public class BluetoothMapService extends ProfileService {
     private boolean mRemoveTimeoutMsg = false;
     private int mPermission = BluetoothDevice.ACCESS_UNKNOWN;
     private boolean mAccountChanged = false;
+    private boolean mSdpSearchInitiated = false;
+    SdpMnsRecord mMnsRecord = null;
 
     // 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";
@@ -199,12 +189,21 @@ public class BluetoothMapService extends ProfileService {
     }
 
     /**
-     * Starts the RFComm listerner threads for each MAS
+     * Starts the RFComm listener threads for each MAS
      * @throws IOException
      */
-    private final void startRfcommSocketListeners() {
-        for(int i=0, c=mMasInstances.size(); i < c; i++) {
-            mMasInstances.valueAt(i).startRfcommSocketListener();
+    private final void startRfcommSocketListeners(int masId) {
+        if(masId == -1) {
+            for(int i=0, c=mMasInstances.size(); i < c; i++) {
+                mMasInstances.valueAt(i).startRfcommSocketListener();
+            }
+        } else {
+            BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1
+            if(masInst != null) {
+                masInst.startRfcommSocketListener();
+            } else {
+                Log.w(TAG, "startRfcommSocketListeners(): Invalid MasId: " + masId);
+            }
         }
     }
 
@@ -225,7 +224,8 @@ public class BluetoothMapService extends ProfileService {
         }
 
         if(mBluetoothMnsObexClient == null) {
-            mBluetoothMnsObexClient = new BluetoothMnsObexClient(mRemoteDevice, mSessionStatusHandler);
+            mBluetoothMnsObexClient =
+                    new BluetoothMnsObexClient(mRemoteDevice, mMnsRecord, mSessionStatusHandler);
         }
 
         boolean connected = false;
@@ -236,10 +236,12 @@ public class BluetoothMapService extends ProfileService {
                     connected = true;
                 }
             } catch (IOException e) {
-                Log.w(TAG,"IOException occured while starting an obexServerSession restarting the listener",e);
+                Log.w(TAG,"IOException occured while starting an obexServerSession restarting" +
+                        " the listener",e);
                 mMasInstances.valueAt(i).restartObexServerSession();
             } catch (RemoteException e) {
-                Log.w(TAG,"RemoteException occured while starting an obexServerSession restarting the listener",e);
+                Log.w(TAG,"RemoteException occured while starting an obexServerSession restarting" +
+                        " the listener",e);
                 mMasInstances.valueAt(i).restartObexServerSession();
             }
         }
@@ -322,13 +324,17 @@ public class BluetoothMapService extends ProfileService {
                     break;
                 case START_LISTENER:
                     if (mAdapter.isEnabled()) {
-                        startRfcommSocketListeners();
+                        startRfcommSocketListeners(msg.arg1);
                     }
                     break;
                 case MSG_MAS_CONNECT:
                     onConnectHandler(msg.arg1);
                     break;
                 case MSG_MAS_CONNECT_CANCEL:
+                    /* TODO: We need to handle this by accepting the connection and reject at
+                     * OBEX level, by using ObexRejectServer - add timeout to handle clients not
+                     * closing the transport channel.
+                     */
                     stopObexServerSessions(-1);
                     break;
                 case USER_TIMEOUT:
@@ -449,7 +455,8 @@ public class BluetoothMapService extends ProfileService {
     }
 
     public boolean disconnect(BluetoothDevice device) {
-        mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(DISCONNECT_MAP, 0, 0, device));
+        mSessionStatusHandler.sendMessage(mSessionStatusHandler
+                .obtainMessage(DISCONNECT_MAP, 0, 0, device));
         return true;
     }
 
@@ -459,7 +466,6 @@ public class BluetoothMapService extends ProfileService {
         if (getRemoteDevice().equals(device)) {
             switch (mState) {
                 case BluetoothMap.STATE_CONNECTED:
-                    sendShutdownMessage();
                     /* Disconnect all connections and restart all MAS instances */
                     stopObexServerSessions(-1);
                     result = true;
@@ -539,6 +545,7 @@ public class BluetoothMapService extends ProfileService {
         filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
         filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+        filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
         filter.addAction(ACTION_SHOW_MAPS_EMAIL_SETTINGS);
         filter.addAction(USER_CONFIRM_TIMEOUT_ACTION);
 
@@ -565,9 +572,7 @@ public class BluetoothMapService extends ProfileService {
         createMasInstances();
 
         // start RFCOMM listener
-        mSessionStatusHandler.sendMessage(mSessionStatusHandler
-                .obtainMessage(START_LISTENER));
-
+        sendStartListenerMessage(-1);
         return true;
     }
 
@@ -605,11 +610,13 @@ public class BluetoothMapService extends ProfileService {
         boolean changed = false;
 
         if(getState() == BluetoothMap.STATE_DISCONNECTED) {
-            ArrayList<BluetoothMapEmailSettingsItem> newAccountList = mAppObserver.getEnabledAccountItems();
+            ArrayList<BluetoothMapEmailSettingsItem> newAccountList =
+                    mAppObserver.getEnabledAccountItems();
             ArrayList<BluetoothMapEmailSettingsItem> newAccounts = null;
             ArrayList<BluetoothMapEmailSettingsItem> removedAccounts = null;
             newAccounts = new ArrayList<BluetoothMapEmailSettingsItem>();
-            removedAccounts = mEnabledAccounts; // reuse the current enabled list, to track removed accounts
+            removedAccounts = mEnabledAccounts; // reuse the current enabled list, to track removed
+                                                // accounts
             for(BluetoothMapEmailSettingsItem account: newAccountList) {
                 if(!removedAccounts.remove(account)) {
                     newAccounts.add(account);
@@ -815,7 +822,8 @@ public class BluetoothMapService extends ProfileService {
         Intent timeoutIntent =
                 new Intent(USER_CONFIRM_TIMEOUT_ACTION);
         PendingIntent pIntent = PendingIntent.getBroadcast(this, 0, timeoutIntent, 0);
-        mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+USER_CONFIRM_TIMEOUT_VALUE,pIntent);
+        mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() +
+                USER_CONFIRM_TIMEOUT_VALUE,pIntent);
     }
 
     private void cancelUserTimeoutAlarm(){
@@ -827,10 +835,27 @@ public class BluetoothMapService extends ProfileService {
         mRemoveTimeoutMsg = false;
     }
 
+    /**
+     * Start the incoming connection listeners for a MAS ID
+     * @param masId the MasID to start. Use -1 to start all listeners.
+     */
+    public void sendStartListenerMessage(int masId) {
+        if(mSessionStatusHandler != null) {
+            Message msg = mSessionStatusHandler.obtainMessage(START_LISTENER, masId, 0);
+            /* We add a small delay here to ensure the call returns true before this message is
+             * handled. It seems wrong to add a delay, but the alternative is to build a lock
+             * system to handle synchronization, which isn't nice either... */
+            mSessionStatusHandler.sendMessageDelayed(msg, 20);
+        } // Can only be null during shutdown
+    }
+
     private void sendConnectMessage(int masId) {
         if(mSessionStatusHandler != null) {
             Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT, masId, 0);
-            msg.sendToTarget();
+            /* We add a small delay here to ensure onConnect returns true before this message is
+             * handled. It seems wrong, but the alternative is to store a reference to the
+             * connection in this message, which isn't nice either... */
+            mSessionStatusHandler.sendMessageDelayed(msg, 20);
         } // Can only be null during shutdown
     }
     private void sendConnectTimeoutMessage() {
@@ -877,17 +902,20 @@ public class BluetoothMapService extends ProfileService {
                     sendShutdownMessage();
                 } else if (state == BluetoothAdapter.STATE_ON) {
                     if (DEBUG) Log.d(TAG, "STATE_ON");
-                    // start RFCOMM listener
-                    mSessionStatusHandler.sendMessage(mSessionStatusHandler
-                                  .obtainMessage(START_LISTENER));
+                    // start ServerSocket listener threads
+                    sendStartListenerMessage(-1);
                 }
+
             }else if (action.equals(USER_CONFIRM_TIMEOUT_ACTION)){
                 if (DEBUG) Log.d(TAG, "USER_CONFIRM_TIMEOUT ACTION Received.");
                 // send us self a message about the timeout.
                 sendConnectTimeoutMessage();
+
             } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
+
                 int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
                                                BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
+
                 if (DEBUG) Log.d(TAG, "Received ACTION_CONNECTION_ACCESS_REPLY:" +
                            requestType + "isWaitingAuthorization:" + mIsWaitingAuthorization);
                 if ((!mIsWaitingAuthorization)
@@ -916,7 +944,8 @@ public class BluetoothMapService extends ProfileService {
                                     + result);
                         }
                     }
-                    sendConnectMessage(-1); // -1 indicates all MAS instances
+                    mRemoteDevice.sdpSearch(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS);
+                    mSdpSearchInitiated = true;
                 } else {
                     // Auth. declined by user, serverSession should not be running, but
                     // call stop anyway to restart listener.
@@ -931,6 +960,28 @@ public class BluetoothMapService extends ProfileService {
                     }
                     sendConnectCancelMessage();
                 }
+            } else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)){
+                Log.v(TAG, "Received ACTION_SDP_RECORD.");
+                ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
+                Log.v(TAG, "Received UUID: " + uuid.toString());
+                Log.v(TAG, "expected UUID: " +
+                        BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS.toString());
+                if(uuid.equals(BluetoothMnsObexClient.BLUETOOTH_UUID_OBEX_MNS)
+                        && mSdpSearchInitiated)
+                {
+                    mMnsRecord = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+                    Log.v(TAG, " -> MNS Record:" + mMnsRecord);
+                    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
+                    Log.v(TAG, " -> status: " + status);
+                    mSdpSearchInitiated = false; // done searching
+                    if(status != -1 && mMnsRecord != null){
+                        for(int i=0, c=mMasInstances.size(); i < c; i++) {
+                                mMasInstances.valueAt(i).setRemoteFeatureMask(
+                                        mMnsRecord.getSupportedFeatures());
+                        }
+                    }
+                    sendConnectMessage(-1); // -1 indicates all MAS instances
+                }
             } else if (action.equals(ACTION_SHOW_MAPS_EMAIL_SETTINGS)) {
                 Log.v(TAG, "Received ACTION_SHOW_MAPS_EMAIL_SETTINGS.");
 
@@ -952,7 +1003,8 @@ public class BluetoothMapService extends ProfileService {
                 {
                     /* We do not have a connection to a device, hence we need to move
                        the SMS to the correct folder. */
-                    BluetoothMapContentObserver.actionMessageSentDisconnected(context, intent, result);
+                    BluetoothMapContentObserver
+                            .actionMessageSentDisconnected(context, intent, result);
                 }
             } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) &&
                     mIsWaitingAuthorization) {
@@ -999,7 +1051,7 @@ public class BluetoothMapService extends ProfileService {
             }
 
             if (mService != null && mService.isAvailable()) {
-                mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+                mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM,"Need BLUETOOTH permission");
                 return mService;
             }
             return null;
@@ -1034,7 +1086,8 @@ public class BluetoothMapService extends ProfileService {
             if (VERBOSE) Log.v(TAG, "isConnected()");
             BluetoothMapService service = getService();
             if (service == null) return false;
-            return service.getState() == BluetoothMap.STATE_CONNECTED && service.getRemoteDevice().equals(device);
+            return service.getState() == BluetoothMap.STATE_CONNECTED
+                    && service.getRemoteDevice().equals(device);
         }
 
         public boolean connect(BluetoothDevice device) {
index 8105739..fb1e7e8 100644 (file)
@@ -36,6 +36,9 @@ public class BluetoothMapUtils {
     private static final long HANDLE_TYPE_SMS_GSM_MASK    = (((long)0x4)<<56);
     private static final long HANDLE_TYPE_SMS_CDMA_MASK   = (((long)0x8)<<56);
 
+
+    static final int MAP_FEATURE_DEFAULT_BITMASK                = 0x0000001F;
+
     /**
      * 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.
index 1e2c183..7df31da 100644 (file)
@@ -16,6 +16,7 @@ package com.android.bluetooth.map;
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothSocket;
+import android.bluetooth.SdpMnsRecord;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -24,6 +25,8 @@ import android.os.ParcelUuid;
 import android.util.Log;
 import android.util.SparseBooleanArray;
 
+import com.android.bluetooth.BluetoothObexTransport;
+
 import java.io.IOException;
 import java.io.OutputStream;
 
@@ -57,7 +60,7 @@ public class BluetoothMnsObexClient {
 
     private HeaderSet mHsConnect = null;
     private Handler mCallback = null;
-
+    private final SdpMnsRecord mMnsRecord;
     // Used by the MAS to forward notification registrations
     public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
     public static final int MSG_MNS_SEND_EVENT = 2;
@@ -67,7 +70,8 @@ public class BluetoothMnsObexClient {
             ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
 
 
-    public BluetoothMnsObexClient(BluetoothDevice remoteDevice, Handler callback) {
+    public BluetoothMnsObexClient(BluetoothDevice remoteDevice,
+            SdpMnsRecord mnsRecord, Handler callback) {
         if (remoteDevice == null) {
             throw new NullPointerException("Obex transport is null");
         }
@@ -79,6 +83,7 @@ public class BluetoothMnsObexClient {
         Looper looper = thread.getLooper();
         mHandler = new MnsObexClientHandler(looper);
         mCallback = callback;
+        mMnsRecord = mnsRecord;
     }
 
     public Handler getMessageHandler() {
@@ -201,9 +206,21 @@ public class BluetoothMnsObexClient {
 
         BluetoothSocket btSocket = null;
         try {
-            // TODO: Why insecure? - is it because the link is already encrypted?
-            btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
-                    BLUETOOTH_UUID_OBEX_MNS.getUuid());
+            // TODO: Do SDP record search again?
+            if(mMnsRecord != null && mMnsRecord.getL2capPsm() > 0) {
+                // Do L2CAP connect
+                btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm());
+
+            } else if (mMnsRecord != null && mMnsRecord.getRfcommChannelNumber() > 0) {
+                // Do Rfcomm connect
+                btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber());
+            } else {
+                // This should not happen...
+                Log.e(TAG, "Invalid SDP content - attempt a connect to UUID...");
+                // TODO: Why insecure? - is it because the link is already encrypted?
+              btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
+                      BLUETOOTH_UUID_OBEX_MNS.getUuid());
+            }
             btSocket.connect();
         } catch (IOException e) {
             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
@@ -212,7 +229,7 @@ public class BluetoothMnsObexClient {
             return;
         }
 
-        mTransport = new BluetoothMnsRfcommTransport(btSocket);
+        mTransport = new BluetoothObexTransport(btSocket);
 
         try {
             mClientSession = new ClientSession(mTransport);
index f0d4cda..ba592b1 100644 (file)
@@ -56,6 +56,8 @@ import javax.obex.ResponseCodes;
 import javax.obex.ServerRequestHandler;
 import javax.obex.ServerSession;
 
+import com.android.bluetooth.BluetoothObexTransport;
+
 /**
  * This class runs as an OBEX server
  */
@@ -181,8 +183,8 @@ public class BluetoothOppObexServerSession extends ServerRequestHandler implemen
         }
 
         String destination;
-        if (mTransport instanceof BluetoothOppRfcommTransport) {
-            destination = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress();
+        if (mTransport instanceof BluetoothObexTransport) {
+            destination = ((BluetoothObexTransport)mTransport).getRemoteAddress();
         } else {
             destination = "FF:FF:FF:00:00:00";
         }
@@ -555,8 +557,8 @@ public class BluetoothOppObexServerSession extends ServerRequestHandler implemen
             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
         }
         String destination;
-        if (mTransport instanceof BluetoothOppRfcommTransport) {
-            destination = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress();
+        if (mTransport instanceof BluetoothObexTransport) {
+            destination = ((BluetoothObexTransport)mTransport).getRemoteAddress();
         } else {
             destination = "FF:FF:FF:00:00:00";
         }
index 6109b5a..ed6388d 100755 (executable)
@@ -36,6 +36,8 @@ import java.io.IOException;
 import java.net.ServerSocket;
 import java.net.Socket;
 
+import com.android.bluetooth.BluetoothObexTransport;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
@@ -148,17 +150,17 @@ public class BluetoothOppRfcommListener {
                             try {
                                 if (V) Log.v(TAG, "Accepting connection...");
                                 if (mBtServerSocket == null) {
-                                    
+
                                 }
                                 BluetoothServerSocket sSocket = mBtServerSocket;
                                 if (sSocket ==null) {
                                     mInterrupted = true;
-                                    
+
                                 } else {
                                     clientSocket = sSocket.accept();
                                     if (V) Log.v(TAG, "Accepted connection from "
                                         + clientSocket.getRemoteDevice());
-                                    BluetoothOppRfcommTransport transport = new BluetoothOppRfcommTransport(
+                                    BluetoothObexTransport transport = new BluetoothObexTransport(
                                         clientSocket);
                                     Message msg = Message.obtain();
                                     msg.setTarget(mCallback);
diff --git a/src/com/android/bluetooth/opp/BluetoothOppRfcommTransport.java b/src/com/android/bluetooth/opp/BluetoothOppRfcommTransport.java
deleted file mode 100644 (file)
index a4ec0fe..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2008-2009, Motorola, Inc.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * - Neither the name of the Motorola, Inc. nor the names of its contributors
- * may be used to endorse or promote products derived from this software
- * without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.android.bluetooth.opp;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import android.bluetooth.BluetoothSocket;
-
-import javax.obex.ObexTransport;
-
-public class BluetoothOppRfcommTransport implements ObexTransport {
-
-    private final BluetoothSocket mSocket;
-
-    public BluetoothOppRfcommTransport(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 {
-        //return mSocket.isConnected();
-        // TODO: add implementation
-        return true;
-    }
-
-    public String getRemoteAddress() {
-        if (mSocket == null)
-            return null;
-        return mSocket.getRemoteDevice().getAddress();
-    }
-
-}
index 6c883d2..e96c536 100755 (executable)
@@ -34,6 +34,8 @@ package com.android.bluetooth.opp;
 
 import javax.obex.ObexTransport;
 
+import com.android.bluetooth.BluetoothObexTransport;
+
 import android.app.NotificationManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -628,8 +630,8 @@ public class BluetoothOppTransfer implements BluetoothOppBatch.BluetoothOppBatch
 
                     if (V) Log.v(TAG, "Rfcomm socket connection attempt took " +
                             (System.currentTimeMillis() - timestamp) + " ms");
-                    BluetoothOppRfcommTransport transport;
-                    transport = new BluetoothOppRfcommTransport(btSocket);
+                    BluetoothObexTransport transport;
+                    transport = new BluetoothObexTransport(btSocket);
 
                     BluetoothOppPreference.getInstance(mContext).setName(device, device.getName());
 
index ed8226b..821e176 100644 (file)
@@ -644,4 +644,19 @@ class TestTcpTransport implements ObexTransport {
         return s.isConnected();
     }
 
+    @Override
+    public int getMaxTransmitPacketSize() {
+        return -1;
+    }
+
+    @Override
+    public int getMaxReceivePacketSize() {
+        return -1;
+    }
+
+    @Override
+    public boolean isSrmSupported() {
+        // TODO: It should be possible to use SRM in TCP connections
+        return false;
+    }
 }
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java b/src/com/android/bluetooth/pbap/BluetoothPbapRfcommTransport.java
deleted file mode 100644 (file)
index cb9ec5d..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (c) 2008-2009, Motorola, Inc.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * - Neither the name of the Motorola, Inc. nor the names of its contributors
- * may be used to endorse or promote products derived from this software
- * without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.android.bluetooth.pbap;
-
-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 BluetoothPbapRfcommTransport implements ObexTransport {
-    private BluetoothSocket mSocket = null;
-
-    public BluetoothPbapRfcommTransport(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;
-    }
-
-}
index 86e1c06..ebf594d 100644 (file)
@@ -56,6 +56,8 @@ import android.os.ServiceManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
+
+import com.android.bluetooth.BluetoothObexTransport;
 import com.android.bluetooth.Utils;
 
 
@@ -502,7 +504,7 @@ public class BluetoothPbapService extends Service {
             mAuth.setChallenged(false);
             mAuth.setCancelled(false);
         }
-        BluetoothPbapRfcommTransport transport = new BluetoothPbapRfcommTransport(mConnSocket);
+        BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket);
         mServerSession = new ServerSession(transport, mPbapServer, mAuth);
         setState(BluetoothPbap.STATE_CONNECTED);
 
diff --git a/src/com/android/bluetooth/sdp/SdpManager.java b/src/com/android/bluetooth/sdp/SdpManager.java
new file mode 100644 (file)
index 0000000..16f5b00
--- /dev/null
@@ -0,0 +1,608 @@
+/*
+* Copyright (C) 2015 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.sdp;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.SdpMasRecord;
+import android.bluetooth.SdpMnsRecord;
+import android.bluetooth.SdpOppOpsRecord;
+import android.bluetooth.SdpPseRecord;
+import android.bluetooth.SdpRecord;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AbstractionLayer;
+import com.android.bluetooth.btservice.AdapterService;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class SdpManager {
+
+    private static final boolean D = true;
+    private static final boolean V = false;
+    private static final String TAG="SdpManager";
+
+    // TODO: When changing PBAP to use this new API.
+    //       Move the defines to the profile (PBAP already have the feature bits)
+    /* PBAP repositories */
+    public static final byte PBAP_REPO_LOCAL        = 0x01<<0;
+    public static final byte PBAP_REPO_SIM          = 0x01<<1;
+    public static final byte PBAP_REPO_SPEED_DAIL   = 0x01<<2;
+    public static final byte PBAP_REPO_FAVORITES    = 0x01<<3;
+
+    // TODO: When changing OPP to use this new API.
+    //       Move the defines to the profile
+    /* Object Push formats */
+    public static final byte OPP_FORMAT_VCARD21     = 0x01;
+    public static final byte OPP_FORMAT_VCARD30     = 0x02;
+    public static final byte OPP_FORMAT_VCAL10      = 0x03;
+    public static final byte OPP_FORMAT_ICAL20      = 0x04;
+    public static final byte OPP_FORMAT_VNOTE       = 0x05;
+    public static final byte OPP_FORMAT_VMESSAGE    = 0x06;
+    public static final byte OPP_FORMAT_ANY_TYPE_OF_OBJ = (byte)0xFF;
+
+    public static final byte[] OPP_FORMAT_ALL= {
+        OPP_FORMAT_VCARD21,
+        OPP_FORMAT_VCARD30,
+        OPP_FORMAT_VCAL10,
+        OPP_FORMAT_ICAL20,
+        OPP_FORMAT_VNOTE,
+        OPP_FORMAT_VMESSAGE,
+        OPP_FORMAT_ANY_TYPE_OF_OBJ};
+
+    /* Variables to keep track of ongoing and queued search requests.
+     * mTrackerLock must be held, when using/changing sSdpSearchTracker
+     * and mSearchInProgress. */
+    static SdpSearchTracker sSdpSearchTracker;
+    static boolean mSearchInProgress = false;
+    static Object mTrackerLock = new Object();
+
+    /* The timeout to wait for reply from native. Should never fire. */
+    private static final int SDP_INTENT_DELAY = 6000;
+    private static final int MESSAGE_SDP_INTENT = 2;
+
+    // We need a reference to the adapter service, to be able to send intents
+    private static AdapterService sAdapterService;
+    private static boolean sNativeAvailable;
+
+    // This object is a singleton
+    private static SdpManager sSdpManager = null;
+
+    static {
+        classInitNative();
+    }
+
+    private native static void classInitNative();
+    private native void initializeNative();
+    private native void cleanupNative();
+    private native boolean sdpSearchNative(byte[] address, byte[] uuid);
+
+    private native int sdpCreateMapMasRecordNative(String serviceName, int masId,
+            int rfcommChannel, int l2capPsm, int version, int msgTypes, int features);
+
+    private native int sdpCreateMapMnsRecordNative(String serviceName,
+            int rfcommChannel, int l2capPsm, int version, int features);
+
+    private native int sdpCreatePbapPseRecordNative(String serviceName, int rfcommChannel,
+            int l2capPsm, int version, int repositories, int features);
+
+    private native int sdpCreateOppOpsRecordNative(String serviceName,
+            int rfcommChannel, int l2capPsm, int version, byte[] formats_list);
+
+    private native boolean sdpRemoveSdpRecordNative(int record_id);
+
+
+    /* Inner class used for wrapping sdp search instance data */
+    private class SdpSearchInstance {
+        private final BluetoothDevice mDevice;
+        private final ParcelUuid mUuid;
+        private int mStatus = 0;
+        private boolean mSearching;
+        /* TODO: If we change the API to use another mechanism than intents for
+         *       delivering the results, this would be the place to keep a list
+         *       of the objects to deliver the results to. */
+        public SdpSearchInstance(int status, BluetoothDevice device, ParcelUuid uuid){
+            this.mDevice = device;
+            this.mUuid = uuid;
+            this.mStatus = status;
+            mSearching = true;
+        }
+        public BluetoothDevice getDevice() {
+            return mDevice;
+        }
+        public ParcelUuid getUuid() {
+            return mUuid;
+        }
+        public int getStatus(){
+            return mStatus;
+        }
+
+        public void setStatus(int status) {
+            this.mStatus = status;
+        }
+
+        public void startSearch() {
+            mSearching = true;
+            Message message = mHandler.obtainMessage(MESSAGE_SDP_INTENT, this);
+            mHandler.sendMessageDelayed(message, SDP_INTENT_DELAY);
+        }
+
+        public void stopSearch() {
+            if(mSearching) {
+                mHandler.removeMessages(MESSAGE_SDP_INTENT, this);
+            }
+            mSearching = false;
+        }
+        public boolean isSearching() {
+            return mSearching;
+        }
+    }
+
+
+    /* We wrap the ArrayList class to decorate with functionality to
+     * find an instance based on UUID AND device address.
+     * As we use a mix of byte[] and object instances, this is more
+     * efficient than implementing comparable. */
+    class SdpSearchTracker {
+        private final ArrayList<SdpSearchInstance> list = new ArrayList<SdpSearchInstance>();
+
+        void clear() {
+            list.clear();
+        }
+
+        boolean add(SdpSearchInstance inst){
+            return list.add(inst);
+        }
+
+        boolean remove(SdpSearchInstance inst) {
+            return list.remove(inst);
+        }
+
+        SdpSearchInstance getNext() {
+            if(list.size() > 0) {
+                return list.get(0);
+            }
+            return null;
+        }
+
+        SdpSearchInstance getSearchInstance(byte[] address, byte[] uuidBytes) {
+            String addressString = Utils.getAddressStringFromByte(address);
+            ParcelUuid uuid = Utils.byteArrayToUuid(uuidBytes)[0];
+            for (SdpSearchInstance inst : list) {
+                if (inst.getDevice().getAddress().equals(addressString)
+                        && inst.getUuid().equals(uuid)) {
+                    return inst;
+                }
+            }
+            return null;
+        }
+
+        boolean isSearching(BluetoothDevice device, ParcelUuid uuid) {
+            String addressString = device.getAddress();
+            for (SdpSearchInstance inst : list) {
+                if (inst.getDevice().getAddress().equals(addressString)
+                        && inst.getUuid().equals(uuid)) {
+                    return inst.isSearching();
+                }
+            }
+            return false;
+        }
+    }
+
+
+    private SdpManager(AdapterService adapterService) {
+        sSdpSearchTracker = new SdpSearchTracker();
+
+        /* This is only needed until intents are no longer used */
+        sAdapterService = adapterService;
+        initializeNative();
+        sNativeAvailable=true;
+    }
+
+
+    public static SdpManager init(AdapterService adapterService) {
+        sSdpManager = new SdpManager(adapterService);
+        return sSdpManager;
+    }
+
+    public static SdpManager getDefaultManager() {
+        return sSdpManager;
+    }
+
+    public void cleanup() {
+        if (sSdpSearchTracker !=null) {
+            synchronized(mTrackerLock) {
+                sSdpSearchTracker.clear();
+            }
+        }
+
+        if (sNativeAvailable) {
+            cleanupNative();
+            sNativeAvailable=false;
+        }
+        sSdpManager = null;
+    }
+
+
+    void sdpMasRecordFoundCallback(int status, byte[] address, byte[] uuid,
+            int masInstanceId,
+            int l2capPsm,
+            int rfcommCannelNumber,
+            int profileVersion,
+            int supportedFeatures,
+            int supportedMessageTypes,
+            String serviceName,
+            boolean moreResults) {
+
+        synchronized(mTrackerLock) {
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpMasRecord sdpRecord = null;
+            if (inst == null) {
+                Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL");
+                return;
+            }
+            inst.setStatus(status);
+            if(status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                sdpRecord = new SdpMasRecord(masInstanceId,
+                                             l2capPsm,
+                                             rfcommCannelNumber,
+                                             profileVersion,
+                                             supportedFeatures,
+                                             supportedMessageTypes,
+                                             serviceName);
+            }
+            if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            sendSdpIntent(inst, sdpRecord, moreResults);
+        }
+    }
+
+    void sdpMnsRecordFoundCallback(int status, byte[] address, byte[] uuid,
+            int l2capPsm,
+            int rfcommCannelNumber,
+            int profileVersion,
+            int supportedFeatures,
+            String serviceName,
+            boolean moreResults) {
+        synchronized(mTrackerLock) {
+
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpMnsRecord sdpRecord = null;
+            if (inst == null) {
+                Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL");
+                return;
+            }
+            inst.setStatus(status);
+            if(status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                sdpRecord = new SdpMnsRecord(l2capPsm,
+                                             rfcommCannelNumber,
+                                             profileVersion,
+                                             supportedFeatures,
+                                             serviceName);
+            }
+            if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            sendSdpIntent(inst, sdpRecord, moreResults);
+        }
+    }
+
+    void sdpPseRecordFoundCallback(int status, byte[] address, byte[] uuid,
+                                        int l2capPsm,
+                                        int rfcommCannelNumber,
+                                        int profileVersion,
+                                        int supportedFeatures,
+                                        int supportedRepositories,
+                                        String serviceName,
+                                        boolean moreResults) {
+        synchronized(mTrackerLock) {
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpPseRecord sdpRecord = null;
+            if (inst == null) {
+                Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL");
+                return;
+            }
+            inst.setStatus(status);
+            if(status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                sdpRecord = new SdpPseRecord(l2capPsm,
+                                             rfcommCannelNumber,
+                                             profileVersion,
+                                             supportedFeatures,
+                                             supportedRepositories,
+                                             serviceName);
+            }
+            if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            sendSdpIntent(inst, sdpRecord, moreResults);
+        }
+    }
+
+    void sdpOppOpsRecordFoundCallback(int status, byte[] address, byte[] uuid,
+            int l2capPsm,
+            int rfcommCannelNumber,
+            int profileVersion,
+            String serviceName,
+            byte[] formatsList,
+            boolean moreResults) {
+
+        synchronized(mTrackerLock) {
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpOppOpsRecord sdpRecord = null;
+
+            if (inst == null) {
+                Log.e(TAG, "sdpOppOpsRecordFoundCallback: Search instance is NULL");
+                return;
+            }
+            inst.setStatus(status);
+            if(status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                sdpRecord = new SdpOppOpsRecord(serviceName,
+                                                rfcommCannelNumber,
+                                                l2capPsm,
+                                                profileVersion,
+                                                formatsList);
+            }
+            if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            sendSdpIntent(inst, sdpRecord, moreResults);
+        }
+    }
+
+    /* TODO: Test or remove! */
+    void sdpRecordFoundCallback(int status, byte[] address, byte[] uuid,
+            int size_record, byte[] record) {
+        synchronized(mTrackerLock) {
+
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpRecord sdpRecord = null;
+            if (inst == null) {
+                Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL");
+                return;
+            }
+            inst.setStatus(status);
+            if(status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                if(D) Log.d(TAG, "sdpRecordFoundCallback: found a sdp record of size "
+                        + size_record );
+                if(D) Log.d(TAG, "Record:"+ Arrays.toString(record));
+                sdpRecord = new SdpRecord(size_record, record);
+            }
+            if(D) Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            if(D) Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            sendSdpIntent(inst, sdpRecord, false);
+        }
+    }
+
+    public void sdpSearch(BluetoothDevice device, ParcelUuid uuid) {
+        if (sNativeAvailable == false) {
+            Log.e(TAG, "Native not initialized!");
+            return;
+        }
+        synchronized (mTrackerLock) {
+            if (sSdpSearchTracker.isSearching(device, uuid)) {
+                /* Search already in progress */
+                return;
+            }
+
+            SdpSearchInstance inst = new SdpSearchInstance(0, device, uuid);
+            sSdpSearchTracker.add(inst); // Queue the request
+
+            startSearch(); // Start search if not busy
+        }
+
+    }
+
+    /* Caller must hold the mTrackerLock */
+    private void startSearch() {
+
+        SdpSearchInstance inst = sSdpSearchTracker.getNext();
+
+        if((inst != null) && (mSearchInProgress == false)) {
+            if(D) Log.d(TAG, "Starting search for UUID: "+ inst.getUuid());
+            mSearchInProgress = true;
+
+            inst.startSearch(); // Trigger timeout message
+
+            sdpSearchNative(Utils.getBytesFromAddress(inst.getDevice().getAddress()),
+                                            Utils.uuidToByteArray(inst.getUuid()));
+        } // Else queue is empty.
+        else {
+            if(D) Log.d(TAG, "startSearch(): nextInst = " + inst +
+                    " mSearchInProgress = " + mSearchInProgress
+                    + " - search busy or queue empty.");
+        }
+    }
+
+    /* Caller must hold the mTrackerLock */
+    private void sendSdpIntent(SdpSearchInstance inst,
+            Parcelable record, boolean moreResults) {
+
+        inst.stopSearch();
+
+        Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD);
+
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, inst.getDevice());
+        intent.putExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, inst.getStatus());
+        if (record != null)  intent.putExtra(BluetoothDevice.EXTRA_SDP_RECORD, record);
+        intent.putExtra(BluetoothDevice.EXTRA_UUID, inst.getUuid());
+        /* TODO:  BLUETOOTH_ADMIN_PERM was private... change to callback interface.
+         * Keep in mind that the MAP client needs to use this as well,
+         * hence to make it call-backs, the MAP client profile needs to be
+         * part of the Bluetooth APK. */
+        sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM);
+
+        if(moreResults == false) {
+            //Remove the outstanding UUID request
+            sSdpSearchTracker.remove(inst);
+            mSearchInProgress = false;
+            startSearch();
+        }
+    }
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+            case MESSAGE_SDP_INTENT:
+                SdpSearchInstance msgObj = (SdpSearchInstance)msg.obj;
+                Log.w(TAG, "Search timedout for UUID " + msgObj.getUuid());
+                synchronized (mTrackerLock) {
+                    sendSdpIntent(msgObj, null, false);
+                }
+                break;
+            }
+        }
+    };
+
+    /**
+     * Create a server side Message Access Profile Service Record.
+     * Create the record once, and reuse it for all connections.
+     * If changes to a record is needed remove the old record using {@link removeSdpRecord}
+     * and then create a new one.
+     * @param serviceName   The textual name of the service
+     * @param masId         The MAS ID to associate with this SDP record
+     * @param rfcommChannel The RFCOMM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     * @param l2capPsm      The L2CAP PSM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     *                      Supply -1 to omit the L2CAP PSM from the record.
+     * @param version       The Profile version number (As specified in the Bluetooth
+     *                      MAP specification)
+     * @param msgTypes      The supported message types bit mask (As specified in
+     *                      the Bluetooth MAP specification)
+     * @param features      The feature bit mask (As specified in the Bluetooth
+     *                       MAP specification)
+     * @return a handle to the record created. The record can be removed again
+     *          using {@link removeSdpRecord}(). The record is not linked to the
+     *          creation/destruction of BluetoothSockets, hence SDP record cleanup
+     *          is a separate process.
+     */
+    public int createMapMasRecord(String serviceName, int masId,
+            int rfcommChannel, int l2capPsm, int version,
+            int msgTypes, int features) {
+        if(sNativeAvailable == false) {
+            throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+        }
+        return sdpCreateMapMasRecordNative(serviceName, masId, rfcommChannel,
+                l2capPsm, version, msgTypes, features);
+    }
+
+    /**
+     * Create a client side Message Access Profile Service Record.
+     * Create the record once, and reuse it for all connections.
+     * If changes to a record is needed remove the old record using {@link removeSdpRecord}
+     * and then create a new one.
+     * @param serviceName   The textual name of the service
+     * @param rfcommChannel The RFCOMM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     * @param l2capPsm      The L2CAP PSM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     *                      Supply -1 to omit the L2CAP PSM from the record.
+     * @param version       The Profile version number (As specified in the Bluetooth
+     *                      MAP specification)
+     * @param features      The feature bit mask (As specified in the Bluetooth
+     *                       MAP specification)
+     * @return a handle to the record created. The record can be removed again
+     *          using {@link removeSdpRecord}(). The record is not linked to the
+     *          creation/destruction of BluetoothSockets, hence SDP record cleanup
+     *          is a separate process.
+     */
+    public int createMapMnsRecord(String serviceName, int rfcommChannel,
+            int l2capPsm, int version, int features) {
+        if(sNativeAvailable == false) {
+            throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+        }
+        return sdpCreateMapMnsRecordNative(serviceName, rfcommChannel,
+                l2capPsm, version, features);
+    }
+
+    /**
+     * Create a Server side Phone Book Access Profile Service Record.
+     * Create the record once, and reuse it for all connections.
+     * If changes to a record is needed remove the old record using {@link removeSdpRecord}
+     * and then create a new one.
+     * @param serviceName   The textual name of the service
+     * @param rfcommChannel The RFCOMM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     * @param l2capPsm      The L2CAP PSM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     *                      Supply -1 to omit the L2CAP PSM from the record.
+     * @param version       The Profile version number (As specified in the Bluetooth
+     *                      PBAP specification)
+     * @param repositories  The supported repositories bit mask (As specified in
+     *                      the Bluetooth PBAP specification)
+     * @param features      The feature bit mask (As specified in the Bluetooth
+     *                      PBAP specification)
+     * @return a handle to the record created. The record can be removed again
+     *          using {@link removeSdpRecord}(). The record is not linked to the
+     *          creation/destruction of BluetoothSockets, hence SDP record cleanup
+     *          is a separate process.
+     */
+    public int createPbapPseRecord(String serviceName, int rfcommChannel, int l2capPsm,
+                                   int version, int repositories, int features) {
+        if(sNativeAvailable == false) {
+            throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+        }
+        return sdpCreatePbapPseRecordNative(serviceName, rfcommChannel,
+                l2capPsm, version, repositories, features);
+    }
+
+    /**
+     * Create a Server side Object Push Profile Service Record.
+     * Create the record once, and reuse it for all connections.
+     * If changes to a record is needed remove the old record using {@link removeSdpRecord}
+     * and then create a new one.
+     * @param serviceName   The textual name of the service
+     * @param rfcommChannel The RFCOMM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     * @param l2capPsm      The L2CAP PSM channel that clients can connect to
+     *                      (obtain from BluetoothServerSocket)
+     *                      Supply -1 to omit the L2CAP PSM from the record.
+     * @param version       The Profile version number (As specified in the Bluetooth
+     *                      OPP specification)
+     * @param formatsList  A list of the supported formats (As specified in
+     *                      the Bluetooth OPP specification)
+     * @return a handle to the record created. The record can be removed again
+     *          using {@link removeSdpRecord}(). The record is not linked to the
+     *          creation/destruction of BluetoothSockets, hence SDP record cleanup
+     *          is a separate process.
+     */
+    public int createOppOpsRecord(String serviceName, int rfcommChannel, int l2capPsm,
+                                  int version, byte[] formatsList) {
+        if(sNativeAvailable == false) {
+            throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+        }
+        return sdpCreateOppOpsRecordNative(serviceName, rfcommChannel,
+                 l2capPsm, version, formatsList);
+    }
+
+     /**
+      * Remove a SDP record.
+      * When Bluetooth is disabled all records will be deleted, hence there
+      * is no need to call this function when bluetooth is disabled.
+      * @param recordId The Id returned by on of the createXxxXxxRecord() functions.
+      * @return TRUE if the record removal was initiated successfully. FALSE if the record
+      *         handle is not known/have already been removed.
+      */
+    public boolean removeSdpRecord(int recordId){
+        if(sNativeAvailable == false) {
+            throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+        }
+        return sdpRemoveSdpRecordNative(recordId);
+    }
+}
index 9f8e26e..ca30b08 100755 (executable)
@@ -5,7 +5,7 @@ include $(CLEAR_VARS)
 LOCAL_MODULE_TAGS := optional
 LOCAL_CERTIFICATE := platform
 
-LOCAL_JAVA_LIBRARIES := android.test.runner telephony-common mms-common
+LOCAL_JAVA_LIBRARIES := javax.obex android.test.runner
 LOCAL_STATIC_JAVA_LIBRARIES := com.android.emailcommon
 
 # Include all test java files.
diff --git a/tests/src/com/android/bluetooth/tests/IResultLogger.java b/tests/src/com/android/bluetooth/tests/IResultLogger.java
new file mode 100644 (file)
index 0000000..01b00a1
--- /dev/null
@@ -0,0 +1,34 @@
+package com.android.bluetooth.tests;
+
+/**
+ * The interface for results - makes it easy to replace the result
+ * logger implementation at a later point if needed.
+ * @author cbonde
+ *
+ */
+public interface IResultLogger {
+    /**
+     * Add an entry to the result log.
+     * To make the first entry count, add a result of 0 bytes
+     * transfered then starting the test.
+     * Or add a result with 1 byte when e.g. the first byte is received.
+     * @param bytesTransfered The amount of bytes transfered
+     */
+    void addResult(long bytesTransfered);
+
+    /**
+     * Get the current average speed of the transfer.
+     * (based on the last entry in the log, and not the current time)
+     * @return the average speed in bytes/sec
+     */
+    int getAverageSpeed();
+
+    /**
+     * Get the current average speed of the last period of the transfer.
+     * (based on the last entry in the log, and not the current time)
+     * @param period the period over which the average is taken.
+     * @return the average speed in bytes/sec
+     */
+    int getAverageSpeed(long period);
+
+}
@@ -1,5 +1,5 @@
 /*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2015 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
 * limitations under the License.
 */
 
-package com.android.bluetooth.map;
-
-import android.bluetooth.BluetoothSocket;
+package com.android.bluetooth.tests;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
 
 import javax.obex.ObexTransport;
 
-public class BluetoothMapRfcommTransport implements ObexTransport {
-    private BluetoothSocket mSocket = null;
+public class ObexPipeTransport implements ObexTransport {
+    PipedInputStream mInStream;
+    PipedOutputStream mOutStream;
+    boolean mEnableSrm;
 
-    public BluetoothMapRfcommTransport(BluetoothSocket rfs) {
-        super();
-        this.mSocket = rfs;
+    public ObexPipeTransport(PipedInputStream inStream, 
+            PipedOutputStream outStream, boolean enableSrm) {
+        mInStream = inStream;
+        mOutStream = outStream;
+        mEnableSrm = enableSrm;
     }
 
     public void close() throws IOException {
-        mSocket.close();
+        mInStream.close();
+        mOutStream.close();
     }
 
     public DataInputStream openDataInputStream() throws IOException {
@@ -46,11 +51,11 @@ public class BluetoothMapRfcommTransport implements ObexTransport {
     }
 
     public InputStream openInputStream() throws IOException {
-        return mSocket.getInputStream();
+        return mInStream;
     }
 
     public OutputStream openOutputStream() throws IOException {
-        return mSocket.getOutputStream();
+        return mOutStream;
     }
 
     public void connect() throws IOException {
@@ -69,4 +74,17 @@ public class BluetoothMapRfcommTransport implements ObexTransport {
         return true;
     }
 
+    public int getMaxTxPacketSize() {
+        return 15432;
+    }
+
+    public int getMaxRxPacketSize() {
+        return 23450;
+    }
+
+    @Override
+    public boolean isSrmSupported() {
+        return mEnableSrm;
+    }
+
 }
diff --git a/tests/src/com/android/bluetooth/tests/ObexTest.java b/tests/src/com/android/bluetooth/tests/ObexTest.java
new file mode 100644 (file)
index 0000000..ad45522
--- /dev/null
@@ -0,0 +1,926 @@
+/*
+* Copyright (C) 2014 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.tests;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ObexPacket;
+import javax.obex.ObexTransport;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerSession;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.cardemulation.OffHostApduService;
+import android.os.Build;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.bluetooth.BluetoothObexTransport;
+import com.android.bluetooth.sdp.SdpManager;
+import com.android.bluetooth.sdp.SdpMasRecord;
+import com.android.bluetooth.tests.ObexTest.TestSequencer.OPTYPE;
+import com.android.bluetooth.tests.ObexTest.TestSequencer.SeqStep;
+
+/**
+ * Test either using the reference ril without a modem, or using a RIL implementing the
+ * BT SAP API, by providing the rild-bt socket as well as the extended API functions for SAP.
+ *
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class ObexTest extends AndroidTestCase {
+    protected static String TAG = "ObexTest";
+    protected static final boolean D = true;
+    protected static final boolean TRACE = false;
+    protected static final boolean DELAY_PASS_30_SEC = false;
+    public static final long PROGRESS_INTERVAL_MS = 1000;
+    private static final ObexTestParams defaultParams =
+            new ObexTestParams(2*8092, 0, 2*1024*1024/10);
+
+    private static final ObexTestParams throttle100Params =
+            new ObexTestParams(2*8092, 100000, 2*1024*1024/10);
+
+    private static final ObexTestParams smallParams =
+            new ObexTestParams(2*8092, 0, 2*1024);
+
+    private static final ObexTestParams hugeParams =
+            new ObexTestParams(2*8092, 0, 100*1024*1024/1000);
+
+    private static final int SMALL_OPERATION_COUNT = 1000/100;
+    private static final int CONNECT_OPERATION_COUNT = 4500;
+
+    private static final int L2CAP_PSM = 29; /* If SDP is not used */
+    private static final int RFCOMM_CHANNEL = 29; /* If SDP is not used */
+
+    public static final String SERVER_ADDRESS = "10:68:3F:5E:F9:2E";
+
+    private static final String SDP_SERVER_NAME = "Samsung Server";
+    private static final String SDP_CLIENT_NAME = "Samsung Client";
+
+    private static final long SDP_FEATURES   = 0x87654321L; /* 32 bit */
+    private static final int SDP_MSG_TYPES  = 0xf1;       /*  8 bit */
+    private static final int SDP_MAS_ID     = 0xCA;       /*  8 bit */
+    private static final int SDP_VERSION    = 0xF0C0;     /* 16 bit */
+    public static final ParcelUuid SDP_UUID_OBEX_MAS = BluetoothUuid.MAS;
+
+    private static int sSdpHandle = -1;
+
+    private enum SequencerType {
+        SEQ_TYPE_PAYLOAD,
+        SEQ_TYPE_CONNECT_DISCONNECT
+    }
+
+    private Context mContext = null;
+    private int mChannelType = 0;
+
+    public static final int STEP_INDEX_HEADER = 0xF1; /*0xFE*/
+    private static final int ENABLE_TIMEOUT = 5000;
+    private static final int POLL_TIME = 500;
+
+    public ObexTest() {
+        super();
+    }
+
+    /**
+     * Test that a connection can be established.
+     * WARNING: The performance of the pipe implementation is not good. I'm only able to get a
+     * throughput of around 220 kbyte/sec - less that when using Bluetooth :-)
+     */
+    public void testLocalPipes() {
+        mContext = this.getContext();
+        System.out.println("Setting up pipes...");
+
+        PipedInputStream clientInStream = null;
+        PipedOutputStream clientOutStream = null;
+        PipedInputStream serverInStream = null;
+        PipedOutputStream serverOutStream = null;
+        ObexPipeTransport clientTransport = null;
+        ObexPipeTransport serverTransport = null;
+
+        try {
+            /* Create and interconnect local pipes for transport */
+            clientInStream = new PipedInputStream(5*8092);
+            clientOutStream = new PipedOutputStream();
+            serverInStream = new PipedInputStream(clientOutStream, 5*8092);
+            serverOutStream = new PipedOutputStream(clientInStream);
+
+            /* Create the OBEX transport objects to wrap the pipes - enable SRM */
+            clientTransport = new ObexPipeTransport(clientInStream, clientOutStream, true);
+            serverTransport = new ObexPipeTransport(serverInStream, serverOutStream, true);
+
+            TestSequencer sequencer = createBtPayloadTestSequence(clientTransport, serverTransport);
+
+            //Debug.startMethodTracing("ObexTrace");
+            assertTrue(sequencer.run());
+            //Debug.stopMethodTracing();
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+    }
+
+    /* Create a sequence of put/get operations with different payload sizes */
+    private TestSequencer createBtPayloadTestSequence(ObexTransport clientTransport,
+            ObexTransport serverTransport)
+            throws IOException {
+        TestSequencer sequencer = new TestSequencer(clientTransport, serverTransport);
+        SeqStep step;
+
+        step = sequencer.addStep(OPTYPE.CONNECT);
+
+        step = sequencer.addStep(OPTYPE.PUT);
+        step.mParams = defaultParams;
+        step.mUseSrm = true;
+
+        step = sequencer.addStep(OPTYPE.GET);
+        step.mParams = defaultParams;
+        step.mUseSrm = true;
+if(true){
+        step = sequencer.addStep(OPTYPE.PUT);
+        step.mParams = throttle100Params;
+        step.mUseSrm = true;
+
+        step = sequencer.addStep(OPTYPE.GET);
+        step.mParams = throttle100Params;
+        step.mUseSrm = true;
+
+        for(int i=0; i<SMALL_OPERATION_COUNT; i++){
+            step = sequencer.addStep(OPTYPE.PUT);
+            step.mParams = smallParams;
+            step.mUseSrm = true;
+
+            step = sequencer.addStep(OPTYPE.GET);
+            step.mParams = smallParams;
+            step.mUseSrm = true;
+
+        }
+
+        step = sequencer.addStep(OPTYPE.PUT);
+        step.mParams = hugeParams;
+        step.mUseSrm = true;
+
+        step = sequencer.addStep(OPTYPE.GET);
+        step.mParams = hugeParams;
+        step.mUseSrm = true;
+    }
+        step = sequencer.addStep(OPTYPE.DISCONNECT);
+
+        return sequencer;
+    }
+
+    private TestSequencer createBtConnectTestSequence(ObexTransport clientTransport,
+            ObexTransport serverTransport)
+            throws IOException {
+        TestSequencer sequencer = new TestSequencer(clientTransport, serverTransport);
+        SeqStep step;
+
+            step = sequencer.addStep(OPTYPE.CONNECT);
+
+            step = sequencer.addStep(OPTYPE.PUT);
+            step.mParams = smallParams;
+            step.mUseSrm = true;
+
+            step = sequencer.addStep(OPTYPE.GET);
+            step.mParams = smallParams;
+            step.mUseSrm = true;
+
+            step = sequencer.addStep(OPTYPE.DISCONNECT);
+
+        return sequencer;
+    }
+
+    public void testBtServerL2cap() {
+        testBtServer(BluetoothSocket.TYPE_L2CAP, false, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtServerRfcomm() {
+        testBtServer(BluetoothSocket.TYPE_RFCOMM, false, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtClientL2cap() {
+        testBtClient(BluetoothSocket.TYPE_L2CAP, false, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtClientRfcomm() {
+        testBtClient(BluetoothSocket.TYPE_RFCOMM, false, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtServerSdpL2cap() {
+        testBtServer(BluetoothSocket.TYPE_L2CAP, true, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtServerSdpRfcomm() {
+        testBtServer(BluetoothSocket.TYPE_RFCOMM, true, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtClientSdpL2cap() {
+        testBtClient(BluetoothSocket.TYPE_L2CAP, true, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtClientSdpRfcomm() {
+        testBtClient(BluetoothSocket.TYPE_RFCOMM, true, SequencerType.SEQ_TYPE_PAYLOAD);
+    }
+
+    public void testBtServerConnectL2cap() {
+        for(int i=0; i<CONNECT_OPERATION_COUNT; i++){
+            Log.i(TAG, "Starting iteration " + i);
+            testBtServer(BluetoothSocket.TYPE_L2CAP, true,
+                    SequencerType.SEQ_TYPE_CONNECT_DISCONNECT);
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Log.e(TAG,"Exception while waiting...",e);
+            }
+        }
+    }
+
+    public void testBtClientConnectL2cap() {
+        for(int i=0; i<CONNECT_OPERATION_COUNT; i++){
+            Log.i(TAG, "Starting iteration " + i);
+            testBtClient(BluetoothSocket.TYPE_L2CAP, true,
+                    SequencerType.SEQ_TYPE_CONNECT_DISCONNECT);
+            try {
+                // We give the server 100ms to allow adding SDP record
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Log.e(TAG,"Exception while waiting...",e);
+            }
+        }
+    }
+
+    public void testBtServerConnectRfcomm() {
+        for(int i=0; i<CONNECT_OPERATION_COUNT; i++){
+            Log.i(TAG, "Starting iteration " + i);
+            testBtServer(BluetoothSocket.TYPE_RFCOMM, true,
+                    SequencerType.SEQ_TYPE_CONNECT_DISCONNECT);
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Log.e(TAG,"Exception while waiting...",e);
+            }
+        }
+    }
+
+    public void testBtClientConnectRfcomm() {
+        for(int i=0; i<CONNECT_OPERATION_COUNT; i++){
+            Log.i(TAG, "Starting iteration " + i);
+            testBtClient(BluetoothSocket.TYPE_RFCOMM, true,
+                    SequencerType.SEQ_TYPE_CONNECT_DISCONNECT);
+            try {
+                // We give the server 100ms to allow adding SDP record
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Log.e(TAG,"Exception while waiting...",e);
+            }
+        }
+    }
+
+    /**
+     * Create a serverSocket
+     * @param type
+     * @param useSdp
+     * @return
+     * @throws IOException
+     */
+    public static BluetoothServerSocket createServerSocket(int type, boolean useSdp)
+            throws IOException {
+        int rfcommChannel = -1;
+        int l2capPsm = -1;
+
+        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+        if(bt == null) {
+            Log.e(TAG,"No Bluetooth Device!");
+            assertTrue(false);
+        }
+        enableBt(bt);
+        BluetoothServerSocket serverSocket=null;
+        if(type == BluetoothSocket.TYPE_L2CAP) {
+            if(useSdp == true) {
+                serverSocket = bt.listenUsingL2capOn(L2CAP_PSM);
+            } else {
+                serverSocket = bt.listenUsingL2capOn(
+                        BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP);
+            }
+            l2capPsm = serverSocket.getChannel();
+        } else if(type == BluetoothSocket.TYPE_RFCOMM) {
+            if(useSdp == true) {
+                serverSocket = bt.listenUsingInsecureRfcommOn(
+                        BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP);
+            } else {
+                serverSocket = bt.listenUsingInsecureRfcommOn(RFCOMM_CHANNEL);
+            }
+            rfcommChannel = serverSocket.getChannel();
+        } else {
+            fail("Invalid transport type!");
+        }
+        if(useSdp == true) {
+            /* We use the MAP service record to be able to  */
+            // TODO: We need to free this
+            if(sSdpHandle < 0) {
+                sSdpHandle = SdpManager.getDefaultManager().createMapMasRecord(SDP_SERVER_NAME,
+                        SDP_MAS_ID, rfcommChannel, l2capPsm,
+                        SDP_VERSION, SDP_MSG_TYPES, (int)(SDP_FEATURES & 0xffffffff));
+            }
+        }
+
+        return serverSocket;
+    }
+
+    public static void removeSdp() {
+        if(sSdpHandle > 0) {
+            SdpManager.getDefaultManager().removeSdpRecord(sSdpHandle);
+            sSdpHandle = -1;
+        }
+    }
+
+    /**
+     * Server side of a two device Bluetooth test of OBEX
+     */
+    private void testBtServer(int type, boolean useSdp, SequencerType sequencerType) {
+        mContext = this.getContext();
+        System.out.println("Starting BT Server...");
+
+        if(TRACE) Debug.startMethodTracing("ServerSide");
+        try {
+            BluetoothServerSocket serverSocket=createServerSocket(type, useSdp);
+
+            Log.i(TAG, "Waiting for client to connect");
+            BluetoothSocket socket = serverSocket.accept();
+            Log.i(TAG, "Client connected");
+
+            BluetoothObexTransport serverTransport = new BluetoothObexTransport(socket);
+            TestSequencer sequencer = null;
+            switch(sequencerType) {
+            case SEQ_TYPE_CONNECT_DISCONNECT:
+                sequencer = createBtConnectTestSequence(null, serverTransport);
+                break;
+            case SEQ_TYPE_PAYLOAD:
+                sequencer = createBtPayloadTestSequence(null, serverTransport);
+                break;
+            default:
+                fail("Invalid sequencer type");
+                break;
+
+            }
+            //Debug.startMethodTracing("ObexTrace");
+            assertTrue(sequencer.run());
+            //Debug.stopMethodTracing();
+            // Same as below... serverTransport.close();
+            // This is done by the obex server socket.close();
+            serverSocket.close();
+            removeSdp();
+            sequencer.shutdown();
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+        if(TRACE) Debug.stopMethodTracing();
+        if(DELAY_PASS_30_SEC) {
+            Log.i(TAG, "\n\n\nTest done - please fetch logs within 30 seconds...\n\n\n");
+            try {
+                Thread.sleep(30000);
+            } catch (InterruptedException e) {}
+        }
+        Log.i(TAG, "Test done.");
+    }
+
+    /**
+     * Enable Bluetooth and connect to a server socket
+     * @param type
+     * @param useSdp
+     * @param context
+     * @return
+     * @throws IOException
+     */
+    static public BluetoothSocket connectClientSocket(int type, boolean useSdp, Context context)
+            throws IOException {
+        int rfcommChannel = RFCOMM_CHANNEL;
+        int l2capPsm = L2CAP_PSM;
+
+        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+        if(bt == null) {
+            Log.e(TAG,"No Bluetooth Device!");
+            assertTrue(false);
+        }
+        enableBt(bt);
+        BluetoothDevice serverDevice = bt.getRemoteDevice(SERVER_ADDRESS);
+
+        if(useSdp == true) {
+            SdpMasRecord record = clientAwaitSdp(serverDevice, context);
+            rfcommChannel = record.getRfcommCannelNumber();
+            l2capPsm = record.getL2capPsm();
+        }
+
+        BluetoothSocket socket = null;
+        if(type == BluetoothSocket.TYPE_L2CAP) {
+            socket = serverDevice.createL2capSocket(l2capPsm);
+        } else if(type == BluetoothSocket.TYPE_RFCOMM) {
+            socket = serverDevice.createRfcommSocket(rfcommChannel);
+        } else {
+            fail("Invalid transport type!");
+        }
+
+        socket.connect();
+
+        return socket;
+    }
+
+    /**
+     * Test that a connection can be established.
+     */
+    private void testBtClient(int type, boolean useSdp, SequencerType sequencerType) {
+        mContext = this.getContext();
+        mChannelType = type;
+        BluetoothSocket socket = null;
+        System.out.println("Starting BT Client...");
+        if(TRACE) Debug.startMethodTracing("ClientSide");
+        try {
+            socket = connectClientSocket(type, useSdp, mContext);
+
+            BluetoothObexTransport clientTransport = new BluetoothObexTransport(socket);
+
+            TestSequencer sequencer = null;
+            switch(sequencerType) {
+            case SEQ_TYPE_CONNECT_DISCONNECT:
+                sequencer = createBtConnectTestSequence(clientTransport, null);
+                break;
+            case SEQ_TYPE_PAYLOAD:
+                sequencer = createBtPayloadTestSequence(clientTransport, null);
+                break;
+            default:
+                fail("Invalid test type");
+                break;
+
+            }
+            //Debug.startMethodTracing("ObexTrace");
+            assertTrue(sequencer.run());
+            //Debug.stopMethodTracing();
+            // socket.close(); shall be closed by the obex client
+            sequencer.shutdown();
+
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+        if(TRACE) Debug.stopMethodTracing();
+        if(DELAY_PASS_30_SEC) {
+            Log.i(TAG, "\n\n\nTest done - please fetch logs within 30 seconds...\n\n\n");
+            try {
+                Thread.sleep(30000);
+            } catch (InterruptedException e) {}
+        }
+        Log.i(TAG, "Test done.");
+    }
+
+    /* Using an anonymous class is not efficient, but keeps a tight code structure. */
+    static class SdpBroadcastReceiver extends BroadcastReceiver {
+        private SdpMasRecord mMasRecord; /* A non-optimal way of setting an object reference from
+                                            a anonymous class. */
+        final CountDownLatch mLatch;
+        public SdpBroadcastReceiver(CountDownLatch latch) {
+            mLatch = latch;
+        }
+
+        SdpMasRecord getMasRecord() {
+            return mMasRecord;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.d(TAG, "onReceive");
+            String action = intent.getAction();
+            if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)){
+                Log.v(TAG, "Received ACTION_SDP_RECORD.");
+                ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
+                Log.v(TAG, "Received UUID: " + uuid.toString());
+                Log.v(TAG, "existing UUID: " + SDP_UUID_OBEX_MAS.toString());
+                if(uuid.toString().equals(SDP_UUID_OBEX_MAS.toString())) {
+                    assertEquals(SDP_UUID_OBEX_MAS.toString(), uuid.toString());
+                    Log.v(TAG, " -> MAS UUID in result.");
+                    SdpMasRecord record = intent.getParcelableExtra(
+                            BluetoothDevice.EXTRA_SDP_RECORD);
+                    assertNotNull(record);
+                    Log.v(TAG, " -> record: "+record);
+                    if(record.getServiceName().equals(SDP_SERVER_NAME)) {
+
+                        assertEquals(((long)record.getSupportedFeatures())
+                                &0xffffffffL, SDP_FEATURES);
+
+                        assertEquals(record.getSupportedMessageTypes(), SDP_MSG_TYPES);
+
+                        assertEquals(record.getProfileVersion(), SDP_VERSION);
+
+                        assertEquals(record.getServiceName(), SDP_SERVER_NAME);
+
+                        assertEquals(record.getMasInstanceId(), SDP_MAS_ID);
+
+                        int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS,
+                                -1);
+                        Log.v(TAG, " -> status: "+status);
+                        mMasRecord = record;
+                        mLatch.countDown();
+                    } else {
+                        Log.i(TAG, "Wrong service name (" + record.getServiceName()
+                                + ") received, still waiting...");
+                    }
+                } else {
+                    Log.i(TAG, "Wrong UUID received, still waiting...");
+                }
+            } else {
+                fail("Unexpected intent received???");
+            }
+        }
+    };
+
+
+    private static SdpMasRecord clientAwaitSdp(BluetoothDevice serverDevice, Context context) {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
+        final CountDownLatch latch = new CountDownLatch(1);
+        SdpBroadcastReceiver broadcastReceiver = new SdpBroadcastReceiver(latch);
+
+        context.registerReceiver(broadcastReceiver, filter);
+
+        serverDevice.sdpSearch(SDP_UUID_OBEX_MAS);
+        boolean waiting = true;
+        while(waiting == true) {
+            try {
+                Log.i(TAG, "SDP Search requested - awaiting result...");
+                latch.await();
+                Log.i(TAG, "SDP Search reresult received - continueing.");
+                waiting = false;
+            } catch (InterruptedException e) {
+                Log.w(TAG, "Interrupted witle waiting - keep waiting.", e);
+                waiting = true;
+            }
+        }
+        context.unregisterReceiver(broadcastReceiver);
+        return broadcastReceiver.getMasRecord();
+    }
+
+    /** Helper to turn BT on.
+     * This method will either fail on an assert, or return with BT turned on.
+     * Behavior of getState() and isEnabled() are validated along the way.
+     */
+    public static void enableBt(BluetoothAdapter adapter) {
+        if (adapter.getState() == BluetoothAdapter.STATE_ON) {
+            assertTrue(adapter.isEnabled());
+            return;
+        }
+        assertEquals(BluetoothAdapter.STATE_OFF, adapter.getState());
+        assertFalse(adapter.isEnabled());
+        adapter.enable();
+        for (int i=0; i<ENABLE_TIMEOUT/POLL_TIME; i++) {
+            switch (adapter.getState()) {
+            case BluetoothAdapter.STATE_ON:
+                assertTrue(adapter.isEnabled());
+                return;
+            case BluetoothAdapter.STATE_OFF:
+                Log.i(TAG, "STATE_OFF: Still waiting for enable to begin...");
+                break;
+            default:
+                assertEquals(BluetoothAdapter.STATE_TURNING_ON, adapter.getState());
+                assertFalse(adapter.isEnabled());
+                break;
+            }
+            try {
+                Thread.sleep(POLL_TIME);
+            } catch (InterruptedException e) {}
+        }
+        fail("enable() timeout");
+        Log.i(TAG, "Bluetooth enabled...");
+    }
+
+    public static class TestSequencer implements Callback {
+
+        private final static int MSG_ID_TIMEOUT = 0x01;
+        private final static int TIMEOUT_VALUE = 100*2000; // ms
+        private ArrayList<SeqStep> mSequence = null;
+        private HandlerThread mHandlerThread = null;
+        private Handler mMessageHandler = null;
+        private ObexTransport mClientTransport;
+        private ObexTransport mServerTransport;
+
+        private ClientSession mClientSession;
+        private ServerSession mServerSession;
+        ObexTestDataHandler mDataHandler;
+
+        public enum OPTYPE {CONNECT, PUT, GET, DISCONNECT};
+
+
+        public class SeqStep {
+            /**
+             * Test step class to define the operations to be tested.
+             * Some of the data in these test steps will be modified during
+             * test - e.g. the HeaderSets will be modified to enable SRM
+             * and/or carry test information
+             */
+            /* Operation type - Connect, Get, Put etc. */
+            public OPTYPE mType;
+            /* The headers to send in the request - and validate on server side */
+            public HeaderSet mReqHeaders = null;
+            /* The headers to send in the response - and validate on client side */
+            public HeaderSet mResHeaders = null;
+            /* Use SRM */
+            public boolean mUseSrm = false;
+            /* The amount of data to include in the body */
+            public ObexTestParams mParams = null;
+            /* The offset into the data where the un-pause signal is to be sent */
+            public int mUnPauseOffset = -1;
+            /* The offset into the data where the Abort request is to be sent */
+            public int mAbortOffset = -1;
+            /* The side to perform Abort */
+            public boolean mServerSideAbout = false;
+            /* The ID of the test step */
+            private int mId;
+
+            /* Arrays to hold expected sequence of request/response packets. */
+            public ArrayList<ObexPacket> mRequestPackets = null;
+            public ArrayList<ObexPacket> mResponsePackets = null;
+
+            public int index = 0; /* requests with same index are executed in parallel
+                                     (without waiting for a response) */
+
+            public SeqStep(OPTYPE type) {
+                mRequestPackets = new ArrayList<ObexPacket>();
+                mResponsePackets = new ArrayList<ObexPacket>();
+                mType = type;
+            }
+
+            /* TODO: Consider to build these automatically based on the operations
+             *       to be performed. Validate using utility functions - not strict
+             *       binary compare.*/
+            public void addObexPacketSet(ObexPacket request, ObexPacket response) {
+                mRequestPackets.add(request);
+                mResponsePackets.add(response);
+            }
+        }
+
+        public TestSequencer(ObexTransport clientTransport, ObexTransport serverTransport)
+                throws IOException {
+            /* Setup the looper thread to handle messages */
+//            mHandlerThread = new HandlerThread("TestTimeoutHandler",
+//                      android.os.Process.THREAD_PRIORITY_BACKGROUND);
+//            mHandlerThread.start();
+//            Looper testLooper = mHandlerThread.getLooper();
+//            mMessageHandler = new Handler(testLooper, this);
+            mClientTransport = clientTransport;
+            mServerTransport = serverTransport;
+
+            //TODO: fix looper cleanup on server - crash after 464 iterations - related to prepare?
+
+            /* Initialize members */
+            mSequence = new ArrayList<SeqStep>();
+            mDataHandler = new ObexTestDataHandler("(Client)");
+        }
+
+        /**
+         * Add a test step to the sequencer.
+         * @param type the OBEX operation to perform.
+         * @return the created step, which can be decorated before execution.
+         */
+        public SeqStep addStep(OPTYPE type) {
+            SeqStep newStep = new SeqStep(type);
+            mSequence.add(newStep);
+            return newStep;
+        }
+
+        /**
+         * Add a sub-step to a sequencer step. All requests added to the same index will be send to
+         * the SapServer in the order added before listening for the response.
+         * The response order is not validated - hence for each response received the entire list of
+         * responses in the step will be searched for a match.
+         * @param index the index returned from addStep() to which the sub-step is to be added.
+         * @param request The request to send to the SAP server
+         * @param response The response to EXPECT from the SAP server
+
+        public void addSubStep(int index, SapMessage request, SapMessage response) {
+            SeqStep step = sequence.get(index);
+            step.add(request, response);
+        }*/
+
+
+        /**
+         * Run the sequence.
+         * Validate the response is either the expected response or one of the expected events.
+         *
+         * @return true when done - asserts at error/fail
+         */
+        public boolean run() throws IOException {
+            CountDownLatch stopLatch = new CountDownLatch(1);
+
+            /* TODO:
+             * First create sequencer to validate using BT-snoop
+             * 1) Create the transports (this could include a validation sniffer on each side)
+             * 2) Create a server thread with a link to the transport
+             * 3) execute the client operation
+             * 4) validate response
+             *
+             * On server:
+             * 1) validate the request contains the expected content
+             * 2) send response.
+             * */
+
+            /* Create the server */
+            if(mServerTransport != null) {
+                mServerSession = new ServerSession(mServerTransport, new ObexTestServer(mSequence,
+                        stopLatch), null);
+            }
+
+            /* Create the client */
+            if(mClientTransport != null) {
+                mClientSession = new ClientSession(mClientTransport);
+
+                for(SeqStep step : mSequence) {
+                    long stepIndex = mSequence.indexOf(step);
+
+                    Log.i(TAG, "Executing step " + stepIndex + " of type: " + step.mType);
+
+                    switch(step.mType) {
+                    case CONNECT: {
+                        HeaderSet reqHeaders = step.mReqHeaders;
+                        if(reqHeaders == null) {
+                            reqHeaders = new HeaderSet();
+                        }
+                        reqHeaders.setHeader(STEP_INDEX_HEADER, stepIndex);
+                        HeaderSet response = mClientSession.connect(reqHeaders);
+                        validateHeaderSet(response, step.mResHeaders);
+                        break;
+                    }
+                    case GET:{
+                        HeaderSet reqHeaders = step.mReqHeaders;
+                        if(reqHeaders == null) {
+                            reqHeaders = new HeaderSet();
+                        }
+                        reqHeaders.setHeader(STEP_INDEX_HEADER, stepIndex);
+                        Operation op = mClientSession.get(reqHeaders);
+                        if(op != null) {
+                            op.noBodyHeader();
+                            mDataHandler.readData(op.openDataInputStream(), step.mParams);
+                            int responseCode = op.getResponseCode();
+                            Log.i(TAG, "response code: " + responseCode);
+                            HeaderSet response = op.getReceivedHeader();
+                            validateHeaderSet(response, step.mResHeaders);
+                            op.close();
+                        }
+                        break;
+                    }
+                    case PUT: {
+                        HeaderSet reqHeaders = step.mReqHeaders;
+                        if(reqHeaders == null) {
+                            reqHeaders = new HeaderSet();
+                        }
+                        reqHeaders.setHeader(STEP_INDEX_HEADER, stepIndex);
+                        Operation op = mClientSession.put(reqHeaders);
+                        if(op != null) {
+                            mDataHandler.writeData(op.openDataOutputStream(), step.mParams);
+                            int responseCode = op.getResponseCode();
+                            Log.i(TAG, "response code: " + responseCode);
+                            HeaderSet response = op.getReceivedHeader();
+                            validateHeaderSet(response, step.mResHeaders);
+                            op.close();
+                        }
+                        break;
+                    }
+                    case DISCONNECT: {
+                        Log.i(TAG,"Requesting disconnect...");
+                        HeaderSet reqHeaders = step.mReqHeaders;
+                        if(reqHeaders == null) {
+                            reqHeaders = new HeaderSet();
+                        }
+                        reqHeaders.setHeader(STEP_INDEX_HEADER, stepIndex);
+                        try{
+                            HeaderSet response = mClientSession.disconnect(reqHeaders);
+                            Log.i(TAG,"Received disconnect response...");
+                            // For some reason this returns -1 -> EOS
+                            // Maybe increase server timeout.
+                            validateHeaderSet(response, step.mResHeaders);
+                        } catch (IOException e) {
+                            Log.e(TAG, "Error getting response code", e);
+                        }
+                        break;
+                    }
+                    default:
+                        assertTrue("Unknown type: " + step.mType, false);
+                        break;
+
+                    }
+                }
+                mClientSession.close();
+            }
+            /* All done, close down... */
+            if(mServerSession != null) {
+                boolean interrupted = false;
+                do {
+                    try {
+                        interrupted = false;
+                        Log.i(TAG,"Waiting for stopLatch signal...");
+                        stopLatch.await();
+                    } catch (InterruptedException e) {
+                        Log.w(TAG,e);
+                        interrupted = true;
+                    }
+                } while (interrupted == true);
+                Log.i(TAG,"stopLatch signal received closing down...");
+                try {
+                    interrupted = false;
+                    Log.i(TAG,"  Sleep 50ms to allow disconnect signal to be send before closing.");
+                    Thread.sleep(50);
+                } catch (InterruptedException e) {
+                    Log.w(TAG,e);
+                    interrupted = true;
+                }
+                mServerSession.close();
+            }
+            // this will close the I/O streams as well.
+            return true;
+        }
+
+        public void shutdown() {
+//            mMessageHandler.removeCallbacksAndMessages(null);
+//            mMessageHandler.quit();
+//            mMessageHandler = null;
+        }
+
+
+        void validateHeaderSet(HeaderSet headers, HeaderSet expected) throws IOException {
+            /* TODO: Implement and assert if different */
+            if(headers.getResponseCode() != ResponseCodes.OBEX_HTTP_OK) {
+                Log.e(TAG,"Wrong ResponseCode: " + headers.getResponseCode());
+                assertTrue(false);
+            }
+        }
+
+//        private void startTimer() {
+//            Message timeoutMessage = mMessageHandler.obtainMessage(MSG_ID_TIMEOUT);
+//            mMessageHandler.sendMessageDelayed(timeoutMessage, TIMEOUT_VALUE);
+//        }
+//
+//        private void stopTimer() {
+//            mMessageHandler.removeMessages(MSG_ID_TIMEOUT);
+//        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+
+            Log.i(TAG,"Handling message ID: " + msg.what);
+
+            switch(msg.what) {
+            case MSG_ID_TIMEOUT:
+                Log.w(TAG, "Timeout occured!");
+/*                try {
+                    //inStream.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "failed to close inStream", e);
+                }
+                try {
+                    //outStream.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "failed to close outStream", e);
+                }*/
+                break;
+            default:
+                /* Message not handled */
+                return false;
+            }
+            return true; // Message handles
+        }
+
+
+
+    }
+
+}
\ No newline at end of file
diff --git a/tests/src/com/android/bluetooth/tests/ObexTestDataHandler.java b/tests/src/com/android/bluetooth/tests/ObexTestDataHandler.java
new file mode 100644 (file)
index 0000000..9c47971
--- /dev/null
@@ -0,0 +1,154 @@
+package com.android.bluetooth.tests;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import android.util.Log;
+
+public class ObexTestDataHandler {
+
+    final String TAG;
+    String TAG_BASE = "ObexTestDataHandler";
+    static final boolean V = true;
+
+    private static final long PROGRESS_INTERVAL_MS = 1000;
+    int mBufferSize = 0;
+    int mThrottle = 0;
+    long mBytesToTransfer = 0;
+    long mBytesTransfered = 0;
+    long mStartTime = 0;
+    long mLastReport = 0;
+    IResultLogger mResults;
+
+    public ObexTestDataHandler(String tag) {
+        TAG = TAG_BASE + tag;
+    }
+
+    /**
+     * Call after a sleep to calculate the number of buffers to
+     * send to match the throttle value.
+     *
+     * @param bufferSize
+     * @param throttle
+     * @return the number of buffers to send, to match the throttle value
+     */
+    private int getNumberOfBuffers() {
+        if(mThrottle == 0) {
+            return 1;
+        }
+        long deltaT = System.currentTimeMillis() - mStartTime;
+        long deltaB = deltaT*mThrottle/1000; // the amount of bytes we should have sent
+        long bytesMissing = deltaB-mBytesTransfered;
+        return (int)((bytesMissing+(mBufferSize>>1))/mBufferSize); // Round result
+    }
+
+    private void publishProgressIfNeeded() {
+        long now = System.currentTimeMillis();
+        if((now-mLastReport) > PROGRESS_INTERVAL_MS) {
+            mLastReport = now;
+            String result = "Avg: " + mResults.getAverageSpeed()/1024
+                    + " Avg(1s): " + mResults.getAverageSpeed(1000)/1024 +
+                    " mBytesTransfered: " + mBytesTransfered + "\n";
+            if(V) Log.v(TAG,result);
+        }
+    }
+
+    public void readData(InputStream inStream, ObexTestParams params) {
+        /* TODO: parse in the step params
+         * Consider introducing a testStep prepare and wait for completion interface?
+         * in stead of using OBEX headers to carry the index... */
+
+        mBufferSize = params.packageSize;
+        mThrottle = params.throttle;
+        mBytesToTransfer = params.bytesToSend;
+        mBytesTransfered = 0;
+        mResults = TestResultLogger.createLogger();
+        mStartTime = System.currentTimeMillis();
+
+        byte[] buffer = new byte[params.packageSize];
+        if(V) Log.v(TAG, "readData() started data to read: " + params.bytesToSend);
+        try {
+            while(mBytesTransfered < mBytesToTransfer) {
+                int nRx = getNumberOfBuffers();
+                for(; nRx > 0 ; nRx--) {
+                    if(V) Log.v(TAG, "Read()");
+                    int count = inStream.read(buffer);
+                    if(V) Log.v(TAG, "Read() done - count=" + count);
+                    if(count == -1) {
+                        throw new IOException("EOF reached too early mBytesTransfered=" + mBytesTransfered);
+                    }
+                    mBytesTransfered += count;
+                    if(mBytesTransfered >= mBytesToTransfer) {
+                        nRx=0; // break
+                    }
+                    mResults.addResult(mBytesTransfered);
+                    publishProgressIfNeeded();
+                }
+                if(mThrottle != 0) {
+                    // Sleep one package of time.
+                    try {
+                        long sleepTime = (1000*mBufferSize)/mThrottle;
+                        if(V) Log.v(TAG, "Throttle Sleep():" + sleepTime);
+                        Thread.sleep(sleepTime);
+                    } catch (InterruptedException e) {
+                        // Just ignore as the getNumberOfBuffersToSend will compensate
+                        // TODO: Handle Abort
+                    }
+                }
+            }
+        }
+        catch(IOException e) {
+            Log.e(TAG, "Error in readData():",e);
+            } finally {
+        }
+    }
+
+    public void writeData(OutputStream outStream, ObexTestParams params) {
+        mBufferSize = params.packageSize;
+        mThrottle = params.throttle;
+        mBytesToTransfer= params.bytesToSend;
+        mBytesTransfered = 0;
+        mResults = TestResultLogger.createLogger();
+        mStartTime = System.currentTimeMillis();
+
+        byte[] buffer = new byte[params.packageSize];
+        if(V) Log.v(TAG, "writeData() started data to write: " + params.bytesToSend);
+        try {
+            while(mBytesTransfered < mBytesToTransfer) {
+                int nTx = getNumberOfBuffers();
+                if(V) Log.v(TAG, "Write nTx " + nTx + " packets");
+                for(; nTx > 0 ; nTx--) {
+                    if(V) Log.v(TAG, "Write()");
+                    if((mBytesTransfered + mBufferSize) < mBytesToTransfer) {
+                        outStream.write(buffer);
+                        mBytesTransfered += mBufferSize;
+                    } else {
+                        outStream.write(buffer, 0, (int) (mBytesToTransfer-mBytesTransfered));
+                        mBytesTransfered += mBytesToTransfer-mBytesTransfered;
+                        nTx = 0;
+                    }
+                    mResults.addResult(mBytesTransfered);
+                    publishProgressIfNeeded();
+                    if(V) Log.v(TAG, "Write mBytesTransfered: " + mBytesTransfered);
+                }
+                if(mThrottle != 0) {
+                    // Sleep one package of time.
+                    try {
+                        long sleepTime = (1000*mBufferSize)/mThrottle;
+                        if(V) Log.v(TAG, "Throttle Sleep():" + sleepTime);
+                        Thread.sleep(sleepTime);
+                    } catch (InterruptedException e) {
+                        // Just ignore as the getNumberOfBuffersToSend will compensate
+                        // TODO: Handle Abort
+                    }
+
+                }
+
+            }
+        }
+        catch(IOException e) {
+            Log.e(TAG, "Error in ListenTask:",e);
+        }
+    }
+}
diff --git a/tests/src/com/android/bluetooth/tests/ObexTestParams.java b/tests/src/com/android/bluetooth/tests/ObexTestParams.java
new file mode 100644 (file)
index 0000000..79a0e14
--- /dev/null
@@ -0,0 +1,18 @@
+package com.android.bluetooth.tests;
+
+import javax.obex.ObexTransport;
+
+import android.bluetooth.BluetoothSocket;
+
+public class ObexTestParams {
+
+    public int packageSize;
+    public int throttle;
+    public long bytesToSend;
+
+    public ObexTestParams(int packageSize, int throttle, long bytesToSend) {
+        this.packageSize = packageSize;
+        this.throttle = throttle;
+        this.bytesToSend = bytesToSend;
+    }
+}
diff --git a/tests/src/com/android/bluetooth/tests/ObexTestServer.java b/tests/src/com/android/bluetooth/tests/ObexTestServer.java
new file mode 100644 (file)
index 0000000..b7df65c
--- /dev/null
@@ -0,0 +1,144 @@
+package com.android.bluetooth.tests;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerRequestHandler;
+
+import android.util.Log;
+
+import com.android.bluetooth.tests.ObexTest.TestSequencer.SeqStep;
+
+public class ObexTestServer extends ServerRequestHandler {
+
+    private static final String TAG = "ObexTestServer";
+    private static final boolean V = true;
+
+    ArrayList<SeqStep> mSequence;
+    CountDownLatch mStopLatch;
+
+    ObexTestDataHandler mDataHandler;
+    int mOperationIndex = 0;
+
+    public ObexTestServer(ArrayList<SeqStep> sequence, CountDownLatch stopLatch) {
+        super();
+        mSequence = sequence;
+        mDataHandler = new ObexTestDataHandler("(Server)");
+        mStopLatch = stopLatch;
+    }
+
+    /* OBEX operation handlers */
+    @Override
+    public int onConnect(HeaderSet request, HeaderSet reply) {
+        Log.i(TAG,"onConnect()");
+        int index;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try {
+            index = ((Long)request.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onConnect - aborting...");
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        }
+        /* TODO: validate request headers, and set response headers */
+        return result;
+    }
+
+    @Override
+    public void onDisconnect(HeaderSet request, HeaderSet reply) {
+        Log.i(TAG,"onDisconnect()");
+        /* TODO: validate request headers, and set response headers */
+        int index;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try {
+            index = ((Long)request.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onDisconnect...");
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        }
+        if(mOperationIndex >= (mSequence.size()-1)) {
+            /* End of test, signal test runner thread */
+            Log.i(TAG, "Sending latch close signal...");
+            mStopLatch.countDown();
+        } else {
+            Log.i(TAG, "Got disconnect with mOperationCounter = " + mOperationIndex);
+        }
+        reply.responseCode = result;
+    }
+
+    @Override
+    public int onPut(Operation operation) {
+        Log.i(TAG,"onPut()");
+        /* TODO: validate request headers, and set response headers
+         *       Also handle pause/abort */
+        // 1) Validate request
+        // 2) Open the output stream.
+        // 3) Receive the data
+        // 4) Send response OK
+        InputStream inStream;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try{
+            inStream = operation.openInputStream();
+            HeaderSet reqHeaders = operation.getReceivedHeader();
+            int index = ((Long)reqHeaders.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+            mDataHandler.readData(inStream, mSequence.get(index).mParams);
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onPut - aborting...");
+            inStream = null;
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        } finally {
+        }
+        if(result == ResponseCodes.OBEX_HTTP_OK) {
+            Log.i(TAG, "OBEX-HANDLER: operation complete success");
+        } else {
+            Log.e(TAG, "OBEX-HANDLER: operation complete FAILED!");
+        }
+        return result;
+    }
+
+    @Override
+    public int onGet(Operation operation) {
+        Log.i(TAG,"onGet()");
+        /* TODO: validate request headers, and set response headers
+         *       Also handle pause/abort */
+        // 1) Validate request
+        // 2) Open the output stream.
+        // 3) Receive the data
+        // 4) Send response OK
+        OutputStream outStream;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try{
+            outStream = operation.openOutputStream();
+            HeaderSet reqHeaders = operation.getReceivedHeader();
+            int index = ((Long)reqHeaders.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+            mDataHandler.writeData(outStream, mSequence.get(index).mParams);
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onGet - aborting...");
+            outStream = null;
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        } finally {
+        }
+        if(result == ResponseCodes.OBEX_HTTP_OK) {
+            Log.i(TAG, "OBEX-HANDLER: operation complete success");
+        } else {
+            Log.e(TAG, "OBEX-HANDLER: operation complete FAILED!");
+        }
+        return result;
+    }
+
+
+
+}
diff --git a/tests/src/com/android/bluetooth/tests/SdpManagerTest.java b/tests/src/com/android/bluetooth/tests/SdpManagerTest.java
new file mode 100644 (file)
index 0000000..c722072
--- /dev/null
@@ -0,0 +1,289 @@
+package com.android.bluetooth.tests;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerSession;
+
+import com.android.bluetooth.BluetoothObexTransport;
+import com.android.bluetooth.sdp.SdpManager;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.bluetooth.BluetoothUuid;
+import android.graphics.Paint.Join;
+import android.os.Build;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class SdpManagerTest extends AndroidTestCase {
+
+    protected static String TAG = "SdpManagerTest";
+    protected static final boolean D = true;
+
+    public static final int SDP_RECORD_COUNT = 12; /* Maximum number of records to create */
+    public static final int SDP_ITERATIONS = 2000;
+
+    public static final String SDP_SERVER_NAME = "SDP test Server";
+    public static final String SDP_CLIENT_NAME = "SDP test Client";
+
+    public static final long SDP_FEATURES   = 0x87654321L;  /* 32 bit */
+    public static final int  SDP_MSG_TYPES  = 0xf1;         /*  8 bit */
+    public static final int  SDP_MAS_ID     = 0xCA;         /*  8 bit */
+    public static final int  SDP_VERSION    = 0xF0C0;       /* 16 bit */
+    public static final int  SDP_REPOS      = 0xCf;         /*  8 bit */
+
+    SdpManager mManager = null;
+
+    public void testSdpRemove() {
+        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+        if(bt == null) {
+            Log.e(TAG,"No Bluetooth Device!");
+            assertTrue(false);
+        }
+        ObexTest.enableBt(bt);
+        mManager = SdpManager.getDefaultManager();
+        addRemoveRecords(SDP_RECORD_COUNT);
+    }
+
+    public void testSdpAdd() {
+        BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+        if(bt == null) {
+            Log.e(TAG,"No Bluetooth Device!");
+            assertTrue(false);
+        }
+        ObexTest.enableBt(bt);
+        mManager = SdpManager.getDefaultManager();
+
+        int handles[] = new int[SDP_RECORD_COUNT];
+        addRecords(handles, 1);
+
+        try {
+            Log.i(TAG, "\n\n\nRecords added - waiting 5 minutes...\n\n\n");
+            Thread.sleep(300000);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted", e);
+        }
+        Log.i(TAG, "All done - over and out!;-)");
+    }
+
+
+    private void addRecords(int handles[], int iteration) {
+        /* Create the records */
+        int record_id = -1; /* first index is 0 */
+        int count = handles.length-1; // Break condition
+        for(int c = 0; ; c++) {
+            Log.i(TAG, "Create c=" + c);
+            handles[++record_id] = mManager.createMapMasRecord(SDP_SERVER_NAME,
+                    SDP_MAS_ID, record_id, record_id+iteration, SDP_VERSION,
+                    SDP_MSG_TYPES, (int)SDP_FEATURES);
+            Log.i(TAG, "  Added record_handle=" + handles[record_id]);
+            assertTrue(handles[record_id]>=0);
+            if(record_id == count) break;
+
+            handles[++record_id] = mManager.createMapMnsRecord(SDP_SERVER_NAME,
+                    record_id, record_id+iteration, SDP_VERSION,
+                    (int)SDP_FEATURES);
+            Log.i(TAG, "  Added record_handle=" + handles[record_id]);
+            assertTrue(handles[record_id]>=0);
+            if(record_id == count) break;
+
+            handles[++record_id] = mManager.createOppOpsRecord(SDP_SERVER_NAME,
+                    record_id, record_id+iteration, SDP_VERSION, SdpManager.OPP_FORMAT_ALL);
+            Log.i(TAG, "  Added record_handle=" + handles[record_id]);
+            assertTrue(handles[record_id]>=0);
+            if(record_id == count) break;
+
+            handles[++record_id] = mManager.createPbapPseRecord(SDP_SERVER_NAME,
+                    record_id, record_id+iteration, SDP_VERSION, SDP_REPOS,
+                    (int)SDP_FEATURES);
+            Log.i(TAG, "  Added record_handle=" + handles[record_id]);
+            assertTrue(handles[record_id]>=0);
+            if(record_id == count) break;
+        }
+    }
+
+    void removeRecords(int handles[], int record_count) {
+        int record_id;
+        /* Remove the records */
+        for(record_id = 0; record_id < record_count; record_id++) {
+            Log.i(TAG, "remove id=" + record_id);
+            assertTrue(mManager.removeSdpRecord(handles[record_id]));
+        }
+    }
+
+    private void addRemoveRecords(int count) {
+        int record_count = count;
+        int handles[] = new int[record_count];
+        int iteration;
+        for(iteration = 0; iteration < SDP_ITERATIONS; iteration++) {
+
+            addRecords(handles, iteration);
+
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Interrupted", e);
+            }
+
+            removeRecords(handles, record_count);
+        }
+    }
+
+    /**
+     * Client side of SdpSearch test
+     * This test will:
+     *  1) Create a connection to a test server
+     *  2) Create a number of SDP records
+     *  3) Request the test server to read the records
+     *  4) Remove the records
+     *  5) Iterate over 2) to 4) SDP_ITERATIONS number of times
+     */
+    public void testSdpSearchClient() {
+        int count = SDP_RECORD_COUNT;
+        int record_count = count;
+        int handles[] = new int[record_count];
+        int iteration;
+        final BluetoothSocket clientSock;
+        final ClientSession mClientSession;
+        final String[] uuids = {BluetoothUuid.MAS.toString(),
+                                BluetoothUuid.MNS.toString(),
+                                BluetoothUuid.PBAP_PSE.toString(),
+                                BluetoothUuid.ObexObjectPush.toString()};
+        final String uuids_str;
+        final StringBuilder sb = new StringBuilder(uuids.length*2-1);
+        for(String str : uuids) {
+            sb.append(str).append(";");
+        }
+        uuids_str = sb.toString();
+
+        try {
+            /* This will turn on BT and connect */
+            clientSock = ObexTest.connectClientSocket(BluetoothSocket.TYPE_L2CAP, true, mContext);
+            mManager = SdpManager.getDefaultManager();
+            BluetoothObexTransport clientTransport = new BluetoothObexTransport(clientSock);
+            mClientSession = new ClientSession(clientTransport);
+            { // Connect
+                HeaderSet reqHeaders = new HeaderSet();
+                reqHeaders.setHeader(ObexTest.STEP_INDEX_HEADER, (long)0);
+                HeaderSet response = mClientSession.connect(reqHeaders);
+                assertEquals(response.responseCode, ResponseCodes.OBEX_HTTP_OK);
+            }
+
+            for(iteration = 0; iteration < SDP_ITERATIONS; iteration++) {
+                // Add the records
+                addRecords(handles, iteration);
+
+                { // get operation to trigger SDP search on peer device
+                    HeaderSet reqHeaders = new HeaderSet();
+                    reqHeaders.setHeader(ObexTest.STEP_INDEX_HEADER, (long)iteration);
+                    reqHeaders.setHeader(HeaderSet.COUNT, (long)count);
+                    reqHeaders.setHeader(HeaderSet.NAME, uuids_str);
+                    Operation op = mClientSession.get(reqHeaders);
+                    op.noBodyHeader();
+                    int response = op.getResponseCode();
+                    op.close();
+                    assertEquals(response, ResponseCodes.OBEX_HTTP_OK);
+                }
+
+                // Cleanup
+                removeRecords(handles, record_count);
+            }
+            { // disconnect to end test
+                HeaderSet reqHeaders = new HeaderSet();
+                reqHeaders.setHeader(ObexTest.STEP_INDEX_HEADER, 0L); // signals end of test
+                HeaderSet response = mClientSession.disconnect(reqHeaders);
+                assertEquals(response.responseCode, ResponseCodes.OBEX_HTTP_OK);
+            }
+        } catch (IOException e) {
+            Log.e(TAG,"IOException in testSdpSearch",e);
+        }
+
+    }
+
+    /**
+     * Server side of SdpSearch test
+     * This test will start a
+     *  1) Create a connection to a test server
+     *  2) Create a number of SDP records
+     *  3) Request the test server to read the records
+     *  4) Remove the records
+     *  5) Iterate over 2) to 4) SDP_ITERATIONS number of times
+     */
+    public void testSdpSearchServer() {
+        mManager = SdpManager.getDefaultManager();
+        try {
+            CountDownLatch stopLatch = new CountDownLatch(1);
+            BluetoothDevice clientDevice;
+            /* This will turn on BT and create a server socket on which accept can be called. */
+            BluetoothServerSocket serverSocket=ObexTest.createServerSocket(BluetoothSocket.TYPE_L2CAP, true);
+            mManager = SdpManager.getDefaultManager();
+
+            Log.i(TAG, "Waiting for client to connect...");
+            BluetoothSocket socket = serverSocket.accept();
+            Log.i(TAG, "Client connected...");
+
+            BluetoothObexTransport serverTransport = new BluetoothObexTransport(socket);
+            clientDevice = socket.getRemoteDevice();
+            ServerSession serverSession = new ServerSession(serverTransport,
+                    new SdpManagerTestServer(stopLatch, mContext, clientDevice), null);
+
+            boolean interrupted = false;
+            do {
+                try {
+                    interrupted = false;
+                    Log.i(TAG,"Waiting for stopLatch signal...");
+                    stopLatch.await();
+                } catch (InterruptedException e) {
+                    Log.w(TAG,e);
+                    interrupted = true;
+                }
+            } while (interrupted == true);
+            Log.i(TAG,"stopLatch signal received closing down...");
+            /* Give a little time to transfer the disconnect response before closing the socket */
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {}
+
+            // Cleanup
+            serverSession.close();
+            socket.close();
+            serverSocket.close();
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+        Log.i(TAG, "\n\n\nTest done - please fetch logs within 30 seconds...\n\n\n");
+        try {
+            Thread.sleep(30000);
+        } catch (InterruptedException e) {}
+        Log.i(TAG, "Test done.");
+}
+
+
+/*
+ * Tests we need:
+ * - Single threaded test:
+ *      * Add a large number of records and remove them again.
+ * - Multi-threaded rests:
+ *      * Let two or more threads perform the test above, each tasking a n-threads fraction of the RECORD_COUNT
+ *
+ * - Client/server
+ *      * Create a control connection - it might be easiest to use OBEX.
+ *      1) Add a number of records
+ *      2) Trigger read of the records
+ *      3) Remove the records
+ *      4) Validate they are gone (if they are not cached)
+ *      5) Multi thread the test on both sides?
+ *  */
+
+}
diff --git a/tests/src/com/android/bluetooth/tests/SdpManagerTestServer.java b/tests/src/com/android/bluetooth/tests/SdpManagerTestServer.java
new file mode 100644 (file)
index 0000000..2151e56
--- /dev/null
@@ -0,0 +1,315 @@
+package com.android.bluetooth.tests;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerRequestHandler;
+
+import junit.framework.Assert;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.bluetooth.btservice.AbstractionLayer;
+import com.android.bluetooth.sdp.SdpManager;
+import com.android.bluetooth.sdp.SdpMasRecord;
+import com.android.bluetooth.sdp.SdpMnsRecord;
+import com.android.bluetooth.sdp.SdpOppOpsRecord;
+import com.android.bluetooth.sdp.SdpPseRecord;
+import com.android.bluetooth.tests.ObexTest.TestSequencer.SeqStep;
+
+/**
+ * We use an OBEX server to execute SDP search operations, and validate results.
+ * @author cbonde
+ *
+ */
+public class SdpManagerTestServer extends ServerRequestHandler {
+
+    private static final String TAG = "SdpManagerTestServer";
+    private static final boolean V = true;
+
+    int mOperationIndex = 0;
+    int mResult = ResponseCodes.OBEX_HTTP_OK;
+
+    final Context mContext;
+    final CountDownLatch mStopLatch;
+    final BluetoothDevice mDevice;
+
+    public SdpManagerTestServer(CountDownLatch stopLatch, Context context,
+            BluetoothDevice device) {
+        super();
+        mStopLatch = stopLatch;
+        mContext = context;
+        mDevice = device;
+        Log.i(TAG, "created.");
+    }
+
+    /* OBEX operation handlers */
+    @Override
+    public int onConnect(HeaderSet request, HeaderSet reply) {
+        Log.i(TAG,"onConnect()");
+        int index;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try {
+            index = ((Long)request.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onConnect - aborting...");
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        }
+        return result;
+    }
+
+    @Override
+    public void onDisconnect(HeaderSet request, HeaderSet reply) {
+        Log.i(TAG,"onDisconnect()");
+        int index;
+        int result = ResponseCodes.OBEX_HTTP_OK;
+        try {
+            index = ((Long)request.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onDisconnect...");
+            result = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            // A read from null will produce exception to end the test.
+        }
+        if(mOperationIndex == 0) {
+            /* End of test, signal test runner thread */
+            Log.i(TAG, "Sending latch close signal...");
+            mStopLatch.countDown();
+        } else {
+            Log.i(TAG, "Got disconnect with mOperationCounter = " + mOperationIndex);
+        }
+        reply.responseCode = result;
+    }
+
+    /**
+     * Currently not used
+     */
+    @Override
+    public int onPut(Operation operation) {
+        Log.i(TAG,"onPut()");
+        int result = ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
+        return result;
+    }
+
+    /**
+     * Used to execute SDP search operations.
+     */
+    @Override
+    public int onGet(Operation operation) {
+        Log.i(TAG,"onGet()");
+        /* - Use the name header to transfer a ';' separated list of UUID's to search for.
+         * - For each UUID:
+         *    - start a search
+         *    - validate each result received
+         *    - ensure all records gets received (use CountDownLatch)
+         * */
+        mResult = ResponseCodes.OBEX_HTTP_OK;
+        try{
+            HeaderSet reqHeaders = operation.getReceivedHeader();
+            int index = ((Long)reqHeaders.getHeader(ObexTest.STEP_INDEX_HEADER)).intValue();
+            mOperationIndex = index;
+            /* Get the expected number of records to read. */
+            int count = ((Long)reqHeaders.getHeader(HeaderSet.COUNT)).intValue();
+            String name = (String)reqHeaders.getHeader(HeaderSet.NAME);
+            String[] uuids = name.split(";");
+
+            // Initiate search operations, Wait for results and validate
+            searchAwaitAndValidate(uuids, mDevice, count);
+        } catch (IOException e) {
+            Log.e(TAG, "Exception in onPut - aborting...");
+            mResult = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        } finally {
+        }
+        if(mResult == ResponseCodes.OBEX_HTTP_OK) {
+            Log.i(TAG, "OBEX-HANDLER: operation complete success");
+        } else {
+            Log.e(TAG, "OBEX-HANDLER: operation complete FAILED!");
+        }
+        return mResult;
+    }
+
+
+    class SdpBroadcastReceiver extends BroadcastReceiver {
+
+        boolean hasMas = false;
+        boolean hasMns = false;
+        boolean hasOppServer = false;
+        boolean hasPse = false;
+        final CountDownLatch mLatch;
+
+        public SdpBroadcastReceiver(String[] uuids, CountDownLatch latch) {
+            for(String uuid : uuids) {
+                if(uuid.toString().equals(BluetoothUuid.MAS.toString()))
+                    hasMas = true;
+                if(uuid.toString().equals(BluetoothUuid.MNS.toString()))
+                    hasMns = true;
+                if(uuid.toString().equals(BluetoothUuid.PBAP_PSE.toString()))
+                    hasPse = true;
+                if(uuid.toString().equals(BluetoothUuid.ObexObjectPush.toString()))
+                    hasOppServer = true;
+            }
+            mLatch = latch;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.d(TAG, "onReceive");
+            String action = intent.getAction();
+            if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)){
+                Log.v(TAG, "Received ACTION_SDP_RECORD.");
+                ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
+                Log.v(TAG, "Received UUID: " + uuid.toString());
+                if(hasMas && uuid.toString().equals(BluetoothUuid.MAS.toString())) {
+                    Log.v(TAG, " -> MAS UUID in result.");
+                    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
+                    Assert.assertEquals(AbstractionLayer.BT_STATUS_SUCCESS, status); /* BT_STATUS_SUCCESS == 0 - but status is not documented... */
+                    Log.v(TAG, " -> status: "+status);
+                    SdpMasRecord record = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+                    Assert.assertNotNull(record);
+                    Log.v(TAG, " -> Record: " + record);
+                    /* As the normal profiles are also running, we filter out these records */
+                    if(record.getServiceName().equals(SdpManagerTest.SDP_SERVER_NAME)) {
+                        Assert.assertEquals(((long)record.getSupportedFeatures())&0xffffffffL, SdpManagerTest.SDP_FEATURES);
+                        Assert.assertEquals(record.getSupportedMessageTypes(), SdpManagerTest.SDP_MSG_TYPES);
+                        Assert.assertEquals(record.getProfileVersion(), SdpManagerTest.SDP_VERSION);
+                        Assert.assertEquals(record.getServiceName(), SdpManagerTest.SDP_SERVER_NAME);
+                        Assert.assertEquals(record.getMasInstanceId(), SdpManagerTest.SDP_MAS_ID);
+                        int rfcommChannel = record.getRfcommCannelNumber();
+                        int l2capPsm = record.getL2capPsm();
+                        /* We set RFCOMM-channel to record_id and the l2cap PSM to iteration*record_id */
+                        Assert.assertEquals(mOperationIndex+rfcommChannel,l2capPsm);
+                        mLatch.countDown();
+                    } else {
+                        Log.i(TAG, "Wrong service name (" + record.getServiceName()
+                                + ") received, still waiting...");
+                    }
+                } else if(hasMns && uuid.toString().equals(BluetoothUuid.MNS.toString())) {
+                    Log.v(TAG, " -> MAP_MNS UUID in result.");
+                    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
+                    Assert.assertEquals(0, status); /* BT_STATUS_SUCCESS == 0 - but status is not documented... */
+                    Log.v(TAG, " -> status: "+status);
+                    SdpMnsRecord record = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+                    Assert.assertNotNull(record);
+                    Log.v(TAG, " -> Record: " + record);
+                    /* As the normal profiles are also running, we filter out these records */
+                    if(record.getServiceName().equals(SdpManagerTest.SDP_SERVER_NAME)) {
+                        Assert.assertEquals(((long)record.getSupportedFeatures())&0xffffffffL, SdpManagerTest.SDP_FEATURES);
+                        Assert.assertEquals(record.getProfileVersion(), SdpManagerTest.SDP_VERSION);
+                        Assert.assertEquals(record.getServiceName(), SdpManagerTest.SDP_SERVER_NAME);
+                        int rfcommChannel = record.getRfcommChannelNumber();
+                        int l2capPsm = record.getL2capPsm();
+                        /* We set RFCOMM-channel to record_id and the l2cap PSM to iteration*record_id */
+                        Assert.assertEquals(mOperationIndex+rfcommChannel,l2capPsm);
+                        mLatch.countDown();
+                    } else {
+                        Log.i(TAG, "Wrong service name (" + record.getServiceName()
+                                + ") received, still waiting...");
+                    }
+                } else if(hasPse && uuid.toString().equals(BluetoothUuid.PBAP_PSE.toString())) {
+                    Log.v(TAG, " -> PBAP_PSE UUID in result.");
+                    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
+                    Assert.assertEquals(0, status); /* BT_STATUS_SUCCESS == 0 - but status is not documented... */
+                    Log.v(TAG, " -> status: "+status);
+                    SdpPseRecord record = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+                    Assert.assertNotNull(record);
+                    Log.v(TAG, " -> Record: " + record);
+                    /* As the normal profiles are also running, we filter out these records */
+                    if(record.getServiceName().equals(SdpManagerTest.SDP_SERVER_NAME)) {
+                        Assert.assertEquals(((long)record.getSupportedFeatures())&0xffffffffL, SdpManagerTest.SDP_FEATURES);
+                        Assert.assertEquals(((long)record.getSupportedRepositories())&0xffffffffL, SdpManagerTest.SDP_REPOS);
+                        Assert.assertEquals(record.getProfileVersion(), SdpManagerTest.SDP_VERSION);
+                        Assert.assertEquals(record.getServiceName(), SdpManagerTest.SDP_SERVER_NAME);
+                        int rfcommChannel = record.getRfcommChannelNumber();
+                        int l2capPsm = record.getL2capPsm();
+                        /* We set RFCOMM-channel to record_id and the l2cap PSM to iteration*record_id */
+                        Assert.assertEquals(mOperationIndex+rfcommChannel,l2capPsm);
+                        mLatch.countDown();
+                    } else {
+                        Log.i(TAG, "Wrong service name (" + record.getServiceName()
+                                + ") received, still waiting...");
+                    }
+                } else if(hasOppServer && uuid.toString().equals(BluetoothUuid.ObexObjectPush.toString())) {
+                    Log.v(TAG, " -> OPP Server UUID in result.");
+                    int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
+                    Assert.assertEquals(0, status); /* BT_STATUS_SUCCESS == 0 - but status is not documented... */
+                    Log.v(TAG, " -> status: "+status);
+                    SdpOppOpsRecord record = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+                    Assert.assertNotNull(record);
+                    Log.v(TAG, " -> Record: " + record);
+                    /* As the normal profiles are also running, we filter out these records */
+                    if(record.getServiceName().equals(SdpManagerTest.SDP_SERVER_NAME)) {
+                        Assert.assertEquals(record.getProfileVersion(), SdpManagerTest.SDP_VERSION);
+                        Assert.assertEquals(record.getServiceName(), SdpManagerTest.SDP_SERVER_NAME);
+                        Assert.assertTrue(Arrays.equals(record.getFormatsList(),SdpManager.OPP_FORMAT_ALL));
+                        int rfcommChannel = record.getRfcommChannel();
+                        int l2capPsm = record.getL2capPsm();
+                        /* We set RFCOMM-channel to record_id and the l2cap PSM to iteration*record_id */
+                        Assert.assertEquals(mOperationIndex+rfcommChannel,l2capPsm);
+                        mLatch.countDown();
+                    } else {
+                        Log.i(TAG, "Wrong service name (" + record.getServiceName()
+                                + ") received, still waiting...");
+                    }
+                } else {
+                    Log.i(TAG, "Wrong UUID received, still waiting...");
+                }
+            } else {
+                Assert.fail("Unexpected intent received???");
+            }
+        }
+    };
+
+    private void searchAwaitAndValidate(final String[] uuids, BluetoothDevice serverDevice, int count) {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
+        final CountDownLatch latch = new CountDownLatch(count);
+        SdpBroadcastReceiver broadcastReceiver = new SdpBroadcastReceiver(uuids, latch);
+
+        // Register receiver
+        mContext.registerReceiver(broadcastReceiver, filter);
+
+        // Initiate searches
+        for(String uuid : uuids) {
+            if(uuid.toString().equals(BluetoothUuid.MAS.toString()))
+                serverDevice.sdpSearch(BluetoothUuid.MAS);
+            if(uuid.toString().equals(BluetoothUuid.MNS.toString()))
+                serverDevice.sdpSearch(BluetoothUuid.MNS);
+            if(uuid.toString().equals(BluetoothUuid.PBAP_PSE.toString()))
+                serverDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
+            if(uuid.toString().equals(BluetoothUuid.ObexObjectPush.toString()))
+                serverDevice.sdpSearch(BluetoothUuid.ObexObjectPush);
+        }
+
+        // Await results
+        boolean waiting = true;
+        while(waiting == true) {
+            try {
+                Log.i(TAG, "SDP Search requested - awaiting result...");
+                latch.await();
+                Log.i(TAG, "SDP Search reresult received - continueing.");
+                waiting = false;
+            } catch (InterruptedException e) {
+                Log.w(TAG, "Interrupted witle waiting - keep waiting.", e);
+                waiting = true;
+            }
+        }
+        mContext.unregisterReceiver(broadcastReceiver);
+    }
+
+}
diff --git a/tests/src/com/android/bluetooth/tests/TestResultLogger.java b/tests/src/com/android/bluetooth/tests/TestResultLogger.java
new file mode 100644 (file)
index 0000000..edc3864
--- /dev/null
@@ -0,0 +1,88 @@
+package com.android.bluetooth.tests;
+
+import java.util.ArrayList;
+
+/**
+ * Class for collecting test results, and presenting them in different formats.
+ * @author cbonde
+ *
+ */
+public class TestResultLogger implements IResultLogger {
+
+    private ArrayList<Result> mResults;
+
+    private class Result {
+        public long timeStamp; // ms precision Unix Time UTC.
+        public long receivedBytes;
+        public Result(long t, long b) {
+            timeStamp = t;
+            receivedBytes = b;
+        }
+    }
+
+    private TestResultLogger() {
+        mResults = new ArrayList<Result>(1000);
+    }
+
+    public static IResultLogger createLogger(){
+        return new TestResultLogger();
+    }
+
+    @Override
+    public void addResult(long bytesTransfered) {
+        mResults.add(new Result(System.currentTimeMillis(), bytesTransfered));
+    }
+
+    @Override
+    public int getAverageSpeed() {
+        if(mResults.size() < 1){
+            return 0;
+        }
+        Result first = mResults.get(0);
+        Result last = mResults.get(mResults.size()-1);
+        // Multiply by 1000 to convert from ms to sec without loss
+        // of precision.
+        return (int) ((1000*(last.receivedBytes + first.receivedBytes))/
+                (last.timeStamp - first.timeStamp+1));
+    }
+
+    /**
+     * Optimized to perform best when period is closest to the last
+     * result entry.
+     * If the period does not match a log entry, an estimate will be made
+     * to compensate.
+     * If the result log does not contain data to cover the entire period
+     * the resulting value will represent the average speed of the content
+     * in the log.
+     */
+    @Override
+    public int getAverageSpeed(long period) {
+        Result preFirst = null;
+        Result first = mResults.get(0);
+        int i = mResults.size()-1;
+        Result last = mResults.get(i--);
+        long firstTimeStamp = last.timeStamp - period;
+        if(first.timeStamp > firstTimeStamp || mResults.size() < 3) {
+            // Not enough data, use total average
+            return getAverageSpeed();
+        }
+        for(; i >= 0 ; i--) {
+            preFirst = mResults.get(i);
+            if(preFirst.timeStamp < firstTimeStamp) {
+                first = mResults.get(i+1);
+                break;
+            }
+        }
+        long timeError = period - (last.timeStamp-first.timeStamp);
+        long errorBytes = 0;
+        if(timeError > 0) {
+            // Find the amount of bytes to use for correction
+            errorBytes = (timeError*(preFirst.receivedBytes - first.receivedBytes))
+                            /(preFirst.timeStamp - first.timeStamp+1);
+        }
+        // Calculate the result
+        return (int) ((1000*(errorBytes+last.receivedBytes-first.receivedBytes))/period);
+    }
+
+
+}