Tuesday, December 1, 2009

Provide Bluetooth FTP profile in Android

About providing Bluetooth FTP & OPP profile issue, I've merged all related source code to 0xdroid begle-donut branch. I verified these two services in both Beagle board and Android Dev phone. It looks good now.

When we turn BT on, we start 'obex-client' service in the background. It would register a record to SDP server in bluez and then other BT devices can find we provide this service. How it looks like? We provide channel 7 for FTP. Other BT device can browse our file system using rfcomm. They can pull data from us, modify file name, delete file, or push data to us. Below it's FTP record in SDP database:


Service Name: File Transfer server
Service RecHandle: 0x10003
Service Class ID List:
"OBEX File Transfer" (0x1106)
Protocol Descriptor List:
"L2CAP" (0x0100)
"RFCOMM" (0x0003)
Channel: 7
"OBEX" (0x0008)
Profile Descriptor List:
"OBEX File Transfer" (0x1106)
Version: 0x0100





Let's explain it and trace the source code.
Eg. CreateSession method call in obex-client dbus API

How we connect obex-client with Bluetooth UI? [using JNI, aidl, and d-bus]
We register a FTP service from AndroidRuntime. We also create few methods and it's based on obex client API document. Below texts are related source code:

AndroidRuntime.cpp

REG_JNI(register_android_server_BluetoothFtpService),

android_server_BluetoothFtpService.cpp

int register_android_server_BluetoothFtpService(JNIEnv *env)
{
jclass clazz = env->FindClass("android/server/BluetoothFtpService");

method_onCreateSessionComplete = env->GetMethodID( clazz, "onCreateSessionComplete", "(Ljava/lang/String;Ljava/lang/String;Z)V" );
method_onChangeFolderComplete = env->GetMethodID( clazz, "onChangeFolderComplete", "(Ljava/lang/String;Ljava/lang/String;Z)V" );

return AndroidRuntime::registerNativeMethods(env,
"android/server/BluetoothFtpService", sMethods, NELEM(sMethods));
}


Below text is the sequence from UI to native code and then back to UI.

RemoteFileManagerActivity.java (Pick up one device from remote tab in Bluetooth UI)

public boolean onOptionsItemSelected(MenuItem item) {
// Intent intent;
switch (item.getItemId()) {
case MENU_SELECT_SERVER:
if (mFTPClient.isConnectionActive()) {
Toast.makeText(this, R.string.error_ftp_connect_timeout, Toast.LENGTH_LONG).show();
} else {
/* Connect to Server */
handleServerSelect();
}

public void handleServerSelect() {
mDirectoryButtons.removeAllViews();
currentDirectory = "/";
if (mContext.isBluetoothEnabled()) {
Intent intent = new Intent(getApplicationContext(), BluetoothDevicePicker.class);
intent.setAction(BluetoothAppIntent.ACTION_SELECT_BLUETOOTH_DEVICE);
intent.putExtra(BluetoothAppIntent.PROFILE, BluetoothAppIntent.PROFILE_FTP);
intent.setData(Uri.parse("file://" + currentDirectory));
try {
startActivityForResult(intent, SUBACTIVITY_PICK_BT_DEVICE);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "No Activity for : " + BluetoothAppIntent.ACTION_SELECT_BLUETOOTH_DEVICE, e);
}
}
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

switch (requestCode) {
case SUBACTIVITY_PICK_BT_DEVICE:
if (resultCode == RESULT_OK && data != null) {
/* Obtain the Server name and Address */
String serverAddress = data.getStringExtra(BluetoothDevicePicker.ADDRESS);
String serverName = data.getStringExtra(BluetoothDevicePicker.NAME);
if (mFTPClient != null) {
mFTPClient.setAddress(serverAddress);
mFTPClient.setName(serverName);
mServerConnectButtonLayout.setVisibility(View.GONE);
if(mFTPClient.createSession() != true) {
String szStr = getResources().getString(R.string.ftp_connect_failed, mFTPClient.getName());
Toast.makeText(this, szStr, Toast.LENGTH_LONG).show();
updateServerStatus();
} else {
String szStr = getResources().getString(R.string.ftp_connect_device, mFTPClient.getName());
showBusy(mFTPClient.getName(), szStr);
}
}
}
break;
}
} /* onActivityResult */

BluetoothFtpService.java (handle from Android Frameworks)

public synchronized boolean createSession(String address, IBluetoothFtpCallback callback) {
/*
* Need to register callback before calling native code
* (which could potentially call callback)
*/
BluetoothObexDatabase.SessionDbItem dbItem =
mSessionDb.new SessionDbItem(address,null,callback);
mSessionDb.insert(dbItem);

boolean ret = createSessionNative(address);

if (!ret) {
mSessionDb.deleteByAddress(address);
}

return ret;
}

android_server_BluetoothFtpService.cpp (use JNI and send a dbus method call to obex-client)
 
static jboolean createSessionNative(JNIEnv* env, jobject object, jstring address )
{
DBusMessage *msg = dbus_message_new_method_call(OBEXD_DBUS_CLIENT_SVC,
OBEXD_DBUS_CLIENT_PATH,
OBEXD_DBUS_CLIENT_IFC,
OBEXD_DBUS_CLIENT_CREATE);

pending = (dbus_async_call_t *) malloc(sizeof(dbus_async_call_t));
if (pending)
{
DBusPendingCall *call;
char *context_address = (char *) calloc(BTADDR_SIZE,
sizeof(char));
strlcpy(context_address, c_address, BTADDR_SIZE); // for callback
pending->env = env;
pending->user_cb = onCreateSessionComplete;
pending->user = context_address;
pending->nat = nat;
dbus_bool_t reply = dbus_connection_send_with_reply(nat->conn,
msg,
&call,
10*1000);

/external/obexd/client/main.c (handle from obex-client)

static DBusMessage *create_session(DBusConnection *connection,
DBusMessage *message, void *user_data)
{
if (session_create(source, dest, target, create_callback, data) == 0)
return NULL;

/external/obexd/client/session.c (use rfcomm to connect with specific channel )

int session_create(const char *source,
const char *destination, const char *target,
session_callback_t function, void *user_data)
{
if (session->channel > 0) {
err = rfcomm_connect(&session->src, &session->dst,
session->channel, rfcomm_callback, callback);

android_server_BluetoothFtpService.cpp (we get the callback!)
 
static void onCreateSessionComplete(DBusMessage *msg, void *user, void *nat_cb)
{
char* c_address = (char *)user;
JNIEnv *env = NULL;
nat->vm->GetEnv((void**)&env, nat->envVer);
jstring address = env->NewStringUTF(c_address);

env->CallVoidMethod(nat->me,
method_onCreateSessionComplete,
obj_path,
address,
is_error);

BluetoothFTPClient.java (handle from Bluetooth UI)

public void onCreateSessionComplete(boolean isError) {
Message msg = Message.obtain();
msg.what = TYPE_CREATE_SESSION_COMPLETE;
Bundle b = new Bundle();
b.putBoolean("isError", isError);
msg.obj = b;
mHandler.sendMessage(msg);
}


RemoteFileManagerActivity.java (device is connected, going to refresh panel to display Remote file system)

public void onCreateSessionComplete(boolean isError) {
mSessionCreated = !isError;
hideBusy();
if (mFTPClient != null) {
if (isError == false) {
mContext.onServerConnected();
goHomeDir();
} else {
String szStr = getResources().getString(R.string.ftp_connect_failed, mFTPClient.getName());
Toast.makeText(this, szStr, Toast.LENGTH_LONG).show();
}
}
updateServerStatus();
refreshDirectoryPanel();
}

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.