Exploring OBEX Devices Connected via Bluetooth






4.77/5 (41 votes)
A sample application that shows how to browse an OBEX device and transfer files to it.

Contents
- Introduction
- Requirements
- How the Application Works
- Final Notes
- Points of Interest
- References
- History
Introduction
In this article, I will present a program that lets you browse any device connected to your computer via Bluetooth and allows you to upload/download files to/from the device. The device should have OBEX support. In order to connect to a device via Bluetooth and to perform OBEX operations, I use these libraries: 32feet.Net and Brecham OBEX. I would like to thank the authors of these libraries as I could not have written this program without the libraries mentioned.
Requirements
In order for this application to function, you need to have Bluetooth on your computer that uses the Microsoft Bluetooth stack and another device with Bluetooth which you will connect to use this program. If your Bluetooth device uses a non-Microsoft stack, then it is possible to disable it and install the Microsoft stack. Have a look at this guide for further instructions.
This program uses the OBEX library that communicates to a device, so a general understanding of what OBEX is and how it works is preferable, but not desired.
How the Application Works
Connecting
When you run the application, the first thing you should do is connect to the device. You can choose the device you want to connect to using a dialog that shows the available Bluetooth devices. After the device has been selected, we connect to it and initiate a new session. The code snippet shows how it is done:
private void Connect()
{
using (SelectBluetoothDeviceDialog bldialog =
new SelectBluetoothDeviceDialog())
{
bldialog.ShowAuthenticated = true;
bldialog.ShowRemembered = true;
bldialog.ShowUnknown = true;
if (bldialog.ShowDialog() == DialogResult.OK)
{
if (bldialog.SelectedDevice == null)
{
MessageBox.Show("No device selected", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
//Create new end point for the selected device.
//BluetoothService.ObexFileTransfer means
//that we want to connect to Obex service.
BluetoothDeviceInfo selecteddevice = bldialog.SelectedDevice;
BluetoothEndPoint remoteEndPoint = new
BluetoothEndPoint(selecteddevice.DeviceAddress,
BluetoothService.ObexFileTransfer);
//Create new Bluetooth client..
client = new BluetoothClient();
try
{
//... and connect to the end point we created.
client.Connect(remoteEndPoint);
//Create a new instance of ObexClientSession
session = new ObexClientSession(client.GetStream(), UInt16.MaxValue);
session.Connect(ObexConstant.Target.FolderBrowsing);
}
catch (SocketException ex)
{
ExceptionHandler(ex, false);
return;
}
catch (ObjectDisposedException ex)
{
ExceptionHandler(ex, false);
return;
}
catch (IOException ex)
{
ExceptionHandler(ex, false);
return;
}
bgwWorker.RunWorkerAsync();
}
}
}
First, we show a dialog that displays the available Bluetooth devices. In addition to the devices that are present at the moment, it will also show those that were connected to the computer in the past but might not be available now. This can be turned off by setting the ShowRemembered
property of SelectBluetoothDeviceDialog
to false
. However, in that case, if you want to connect to a remembered device, it will not be shown by the dialog.
After the device has been selected, we create a remote end point based on the address of the device. The second parameter specifies the service we want to connect to. In our case, it is BluetoothService.ObexFileTransfer
, meaning that we will be able to transfer files using the OBEX protocol. Next, we need to create an instance of the BluetoothClient
class and connect to the end point we created earlier. When the connection has been established, we create an instance of the ObexClientSession
class. According to documentation, "[ObexClientSession
is] A client-side connection to an OBEX server, supports Put
, Get
, and most other operation types." The instance we create will be used for all the OBEX operations we will perform. Next, we connect to the folder browsing service so that we can browse the device using OBEX.
Now, when we are connected to the folder browsing service of the device, we can start exploring it. We will be able to show the files and folders, create new folders, delete existing ones, and refresh folder content.
Exploring the Device
Displaying Folder Content
In order to get folder content, we need to send such a request to the device. Then, we need to parse the response we get from the device and retrieve the information we need. The advantage of the Brecham.Obex
library is that it hides all the low level OBEX protocol specific stuff. It also provides a full parser for the OBEX Folder-Listing objects. All we need to do is call the Get
method of the ObexClientSession
class, passing the necessary command and passing the result to the parser. We can then use the items returned by the parser to populate the listview
that shows the folder content. All this is done using the BackGroundWorker
class so that the UI is not blocked.
private void bgwWorker_DoWork(object sender, DoWorkEventArgs e)
{
DateTime old = DateTime.Now;
TimeSpan dr = TimeSpan.FromMilliseconds(200);
//Request current folder's content
using (ObexGetStream str = session.Get(null, ObexConstant.Type.FolderListing))
{
//Pass the response stream to folder listing parser
ObexFolderListingParser parser = new ObexFolderListingParser(str);
parser.IgnoreUnknownAttributeNames = true;
ObexFolderListingItem item = null;
List<ListViewItem> items = new List<ListViewItem>();
//Iterate through the items and construct listview items.
while ((item = parser.GetNextItem()) != null)
{
if (item is ObexParentFolderItem)
continue;
ObexFileOrFolderItem filefolderitem = item as ObexFileOrFolderItem;
bool isfolder = filefolderitem is ObexFolderItem;
ListViewItem temp = new ListViewItem(new string[] {filefolderitem.Name,
FormatSize(filefolderitem.Size,isfolder),
FormatDate(filefolderitem.Modified),
FormatDate(filefolderitem.Accessed),
FormatDate(filefolderitem.Created)},
GetIconIndex(Path.GetExtension(filefolderitem.Name), isfolder));
temp.Tag = isfolder;
temp.Name = filefolderitem.Name;
items.Add(temp);
//Report progress
if (old.Add(dr) < DateTime.Now)
{
old = DateTime.Now;
bgwWorker.ReportProgress(0, temp.Text);
}
}
e.Result = items.ToArray();
}
}
As you can see from the above code, we pass null
and ObexConstant.Type.FolderListing
to the Get
method and then pass the response stream to the folder object parser. We then iterate through the items returned by the parser and construct the listview
items.
Extracting Icon by Extension
All of the items in the listview have an image associated with them. The image is the icon associated with the current item's extension, or if it is a folder, then it is just a folder icon. In order to retrieve the icon, I have used code from this article: IconHandler. The retrieved icon is stored in the imagelist associated with the listview. The icon for each extension is not retrieved if the imagelist already contains it.
private int GetIconIndex(string extension, bool isFolder)
{
//If it is a folder just return index for the folder icon
if (isFolder)
{
return 1;
}
//If the icon for the extension has already
//been retrieved then return its index
if (imlSmall.Images.ContainsKey(extension))
{
return imlSmall.Images.IndexOfKey(extension);
}
//Retrieve small icon
Icon small = IconHandler.IconHandler.IconFromExtension(extension,
IconSize.Small);
if (small != null)
{
imlSmall.Images.Add(extension, small);
}
//Retrieve large icon
Icon large = IconHandler.IconHandler.IconFromExtension(extension,
IconSize.Large);
if (large != null)
{
imlLarge.Images.Add(extension, large);
}
//If we managed to retrieve only one icon, use it for both sizes.
if (small != null & large == null)
{
imlLarge.Images.Add(extension, small);
}
if (small == null & large != null)
{
imlSmall.Images.Add(extension, large);
}
int result = small == null & large == null ? 0 :
imlSmall.Images.IndexOfKey(extension);
small.Dispose();
large.Dispose();
return result;
}
Navigating through Folders
When a user double-clicks an item in the listview, the item is processed according to its type. If it is a folder, the program moves into the chosen sub-folder and displays its content. If it is a file, it is downloaded. But, before an item can be processed, it is first necessary to determine the item which was clicked.
Determining the Clicked Item
In order to determine which item was double-clicked, we can use the HitTest
method of the ListView
class. This method accepts a Point
parameter, and returns an instance of the ListViewHitTestInfo
class. This class has an Item
property which, as you might have already guessed, points to the item that was double-clicked.
private void lsvExplorer_MouseDoubleClick(object sender, MouseEventArgs e)
{
ListViewItem clicked = lsvExplorer.HitTest(e.Location).Item;
if (clicked != null)
{
if ((bool)clicked.Tag)
ProcessFolder(clicked.Text);
else
DownloadFiles();
}
}
Moving to a Sub-folder
If the clicked item represents a folder, we need to set the path on the connected device to a sub-folder location. After that, we can display its content by starting a BackGroundWorker
like we did to display the initial view. But, before new content is displayed, the current items displayed by the listview are pushed into a stack. They will be used later when the user moves one folder up.
private void ProcessFolder(string folderName)
{
try
{
//Set path on the device
session.SetPath(folderName);
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
//Push current items into stack
ListViewItem[] previousItems =
new ListViewItem[lsvExplorer.Items.Count];
lsvExplorer.Items.CopyTo(previousItems, 0);
lsvExplorer.Items.Clear();
previousItemsStack.Push(previousItems);
SetControlState(false);
tsStatusLabel.Text = "Operation started";
//Display current folder's content.
bgwWorker.RunWorkerAsync();
}
Downloading and uploading files is discussed later in the article.
Moving One Folder Up
The user can move one folder up by clicking the 'Up' button on the menu. When this button is clicked, we need to change the current path to the parent folder's path and display its content. As we have pushed the parent folder's content into the stack, we don't need to request items for the second time.
private void MoveUp()
{
//Check if we are at the topmost folder.
if (previousItemsStack.Count > 0)
{
SetControlState(false);
try
{
//Set path to parent folder.
session.SetPathUp();
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
//Clear current items and display saved ones.
lsvExplorer.Items.Clear();
lsvExplorer.Items.AddRange(previousItemsStack.Pop());
SetControlState(true);
}
}
As the items displayed by the listview were fetched some time ago, the folder's content may not reflect the current content. In order to view the current items, you can click the 'Refresh' button.
Refreshing the Current Folder
Refreshing the current folder's content is quite easy as the path is already set. We just need to run our BackGroundWorker
once again.
private void RefreshFolder()
{
SetControlState(false);
tsStatusLabel.Text = "Operation started";
lsvExplorer.Items.Clear();
bgwWorker.RunWorkerAsync();
}
Creating New Folders
Creating a new folder is a little bit trickier than any other operation. Before we can create a new folder, we should make sure that such a folder does not already exist. If the folder does not exist, we can create it. When a user clicks the 'New Folder' button, a new item is added to the listview and the BeginEdit()
method is called for the item.
private void CreateNewFolder()
{
ListViewItem newitem = new ListViewItem("", 1);
lsvExplorer.Items.Add(newitem);
lsvExplorer.LabelEdit = true;
newitem.BeginEdit();
}
When the user ends typing the name for the new folder, the AfterLabelEdit
event of the ListView
class is fired. In the event handler, we check whether the folder exists or not, and create it, if it does not exist.
private void lsvExplorer_AfterLabelEdit(object sender, LabelEditEventArgs e)
{
if (string.IsNullOrEmpty(e.Label))
{
e.CancelEdit = true;
lsvExplorer.Items.RemoveAt(e.Item);
return;
}
//If folder already exists show a messagebox.
if (lsvExplorer.Items.ContainsKey(e.Label))
{
if (MessageBox.Show(string.Format("There is already a folder called {0}",
e.Label), "Error", MessageBoxButtons.OKCancel,
MessageBoxIcon.Error) == DialogResult.OK)
{
//If OK is clicked continue editing the item.
e.CancelEdit = true;
lsvExplorer.Items[e.Item].BeginEdit();
}
else
{
//If Cancel is clicked, we need to remove item from the listview.
lsvExplorer.LabelEdit = false;
lsvExplorer.BeginInvoke((MethodInvoker)(() =>
{
lsvExplorer.Items.RemoveAt(e.Item);
}));
}
}
//Folder does not exist.
else
{
e.CancelEdit = false;
lsvExplorer.LabelEdit = false;
lsvExplorer.Items[e.Item].Name = e.Label;
SetControlState(false);
try
{
//Create new folder and move up one folder
//so that path is not set to newly created folder.
session.SetPath(BackupFirst.DoNot, e.Label, IfFolderDoesNotExist.Create);
session.SetPathUp();
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
catch (ObexResponseException ex)
{
ExceptionHandler(ex);
}
SetControlState(true);
}
}
As you can see from the above code, we first check if a folder with such a name already exists. If yes, we show a message box informing the user about it. If the user clicks OK, then editing continues, and a new name can be specified for the folder. If Cancel is clicked, we need to remove the item which we added. As you can see, it is done on another thread. The reason for such a behavior is that if you try to remove it in the event handler, you will get an exception that you will be unable to catch. For further details, see this blog post: AftetLabelEdit and Removing Last Item from ListView.
In case the folder does not exist, we create it by calling the SetPath
method and passing the name of the new folder. As we want to create a folder, we also specify IfFolderDoesNotExist.Create
indicating that a folder should be created if it does not exist. After that, the current path is set to the newly created folder, so we need to move one folder up.
Deleting Folders and Files
In order to delete files or folders from the device, we can use the Delete
method of the ObexClientSession
class and pass the name of the item we want to delete. When deleting a folder, its contents are deleted too, so be careful.
private void DeleteSelectedItems()
{
if (MessageBox.Show("Do you really want to delete selected items?",
"Confirm", MessageBoxButtons.OKCancel,
MessageBoxIcon.Question) == DialogResult.OK)
{
lsvExplorer.BeginUpdate();
SetControlState(false);
foreach (ListViewItem item in lsvExplorer.SelectedItems)
{
try
{
session.Delete(item.Text);
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
item.Remove();
}
lsvExplorer.EndUpdate();
SetControlState(true);
}
}
Downloading and Uploading Files
In order to download or upload files, you can use the GetTo
or PutFrom
methods. However, to report progress, you will need to create a new stream type and use it in conjunction with the Decorator Pattern. You can read more about it here: OBEX library — Programmer’s guide. A simpler way for progress reporting is to use the Get
and Put
methods. Both of them return a Stream
object. In the case of downloading, we should read from the returned stream and write to the FileStream
object, and in the case of uploading, we should read from the FileStream
and write to the stream returned by the Put
method. In both cases, we can count how many bytes we have read and report progress depending on it. This is done using the BackgroundWorker
too.
private void bgwWorker_DoWork(object sender, DoWorkEventArgs e)
{
long progress = 0;
DateTime start = DateTime.Now;
for (int i = 0; i < filesToProcess.Count; i++)
{
string currentfile = filesToProcess[i];
//Report that we started downloading new file
bgwWorker.ReportProgress((int)(((progress * 100) / totalsize)), i + 1);
string filename = download ? Path.Combine(dir, currentfile) : currentfile;
//Stream on our file system. We will need to either read from it or write to it.
FileStream hoststream = download ?
new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None)
: new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.None);
AbortableStream remotestream = null;
try
{
//Stream on our device. We will need to either read from it or write to it.
remotestream = download ? (AbortableStream)currentSession.Get(currentfile, null)
: (AbortableStream)currentSession.Put(Path.GetFileName(currentfile), null);
}
catch (IOException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return;
}
catch (ObexResponseException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return;
}
using (hoststream)
{
using (remotestream)
{
//This is the function that does actual reading/writing.
long result = download ?
ProcessStreams(remotestream, hoststream, progress, currentfile)
:ProcessStreams(hoststream, remotestream, progress, currentfile);
if (result == 0)
{
e.Cancel = true;
//Even if we are cancelled we need to report how many files we have already
//uploaded so that they are added to the listview. Or if it is download we
//need to delete the partially downloaded last file.
filesProcessed = i;
return;
}
else
progress = result;
}
}
}
DateTime end = DateTime.Now;
e.Result = end - start;
}
As the process is similar in both cases, there is one function that does the actual work. The function reads from the source stream and writes to the destination stream. This is how it works:
private long ProcessStreams(Stream source, Stream destination, long progress,
string filename)
{
//Allocate buffer
byte[] buffer = new byte[1024 * 4];
while (true)
{
//Report downloaded file size
bgwWorker.ReportProgress((int)(((progress * 100) / totalsize)), progress);
if (bgwWorker.CancellationPending)
{
currentSession.Abort();
return 0;
}
try
{
//Read from source and write to destination.
//Break if finished reading. Count read bytes.
int length = source.Read(buffer, 0, buffer.Length);
if (length == 0) break;
destination.Write(buffer, 0, length);
progress += length;
}
//Return 0 as if operation was cancelled so that processedFiles is set.
catch (IOException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return 0;
}
catch (ObexResponseException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return 0;
}
}
return progress;
}
Uploading Dropped Files
When files are dropped on the main form from Windows Explorer, they are automatically uploaded to the device. In order to detect dropped files, I used the library from this book: Windows Forms 2.0 Programming. We just need to subscribe to the FileDropped
event.
private void DragDrop_FileDropped(object sender, FileDroppedEventArgs e)
{
UploadFiles(e.Filenames);
}
That's all for downloading and uploading. The download/upload dialog reports the time spent and the average speed.
Final Notes
I have tested this application with my Sony Eriksson phone and it works well. I have not done anything to target it specifically for my phone, so it should work with other phones too, though I have not tested. The application works on 64 bit Vista Ultimate SP1, but should also work on 32 bit systems as well as other versions of Windows.
Points of Interest
It was really annoying that removing the last item from the ListView
in AfterLabelEdit
caused an exception.
References
History
- 1st October, 2008 - Initial release
- 13th October, 2008 - Version 1.1
- Updated to Brecham.Obex 1.7
- Fixed minor bugs
- 24th October, 2008 - Version 1.2
- Added drag and drop support for files; Dropped files are uploaded automatically
- Added some shortcuts: Pressing F5 refreshes current folder, pressing Delete will delete selected items