Contents
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.
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.
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;
}
BluetoothDeviceInfo selecteddevice = bldialog.SelectedDevice;
BluetoothEndPoint remoteEndPoint = new
BluetoothEndPoint(selecteddevice.DeviceAddress,
BluetoothService.ObexFileTransfer);
client = new BluetoothClient();
try
{
client.Connect(remoteEndPoint);
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.
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);
using (ObexGetStream str = session.Get(null, ObexConstant.Type.FolderListing))
{
ObexFolderListingParser parser = new ObexFolderListingParser(str);
parser.IgnoreUnknownAttributeNames = true;
ObexFolderListingItem item = null;
List<ListViewItem> items = new List<ListViewItem>();
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);
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.
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 (isFolder)
{
return 1;
}
if (imlSmall.Images.ContainsKey(extension))
{
return imlSmall.Images.IndexOfKey(extension);
}
Icon small = IconHandler.IconHandler.IconFromExtension(extension,
IconSize.Small);
if (small != null)
{
imlSmall.Images.Add(extension, small);
}
Icon large = IconHandler.IconHandler.IconFromExtension(extension,
IconSize.Large);
if (large != null)
{
imlLarge.Images.Add(extension, large);
}
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;
}
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.
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();
}
}
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
{
session.SetPath(folderName);
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
ListViewItem[] previousItems =
new ListViewItem[lsvExplorer.Items.Count];
lsvExplorer.Items.CopyTo(previousItems, 0);
lsvExplorer.Items.Clear();
previousItemsStack.Push(previousItems);
SetControlState(false);
tsStatusLabel.Text = "Operation started";
bgwWorker.RunWorkerAsync();
}
Downloading and uploading files is discussed later in the article.
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()
{
if (previousItemsStack.Count > 0)
{
SetControlState(false);
try
{
session.SetPathUp();
}
catch (IOException ex)
{
ExceptionHandler(ex);
return;
}
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'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 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 (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)
{
e.CancelEdit = true;
lsvExplorer.Items[e.Item].BeginEdit();
}
else
{
lsvExplorer.LabelEdit = false;
lsvExplorer.BeginInvoke((MethodInvoker)(() =>
{
lsvExplorer.Items.RemoveAt(e.Item);
}));
}
}
else
{
e.CancelEdit = false;
lsvExplorer.LabelEdit = false;
lsvExplorer.Items[e.Item].Name = e.Label;
SetControlState(false);
try
{
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.
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);
}
}
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];
bgwWorker.ReportProgress((int)(((progress * 100) / totalsize)), i + 1);
string filename = download ? Path.Combine(dir, currentfile) : currentfile;
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
{
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)
{
long result = download ?
ProcessStreams(remotestream, hoststream, progress, currentfile)
:ProcessStreams(hoststream, remotestream, progress, currentfile);
if (result == 0)
{
e.Cancel = true;
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)
{
byte[] buffer = new byte[1024 * 4];
while (true)
{
bgwWorker.ReportProgress((int)(((progress * 100) / totalsize)), progress);
if (bgwWorker.CancellationPending)
{
currentSession.Abort();
return 0;
}
try
{
int length = source.Read(buffer, 0, buffer.Length);
if (length == 0) break;
destination.Write(buffer, 0, length);
progress += length;
}
catch (IOException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return 0;
}
catch (ObexResponseException ex)
{
exceptionoccured = true;
ExceptionMethod(ex);
return 0;
}
}
return progress;
}
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.
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.
It was really annoying that removing the last item from the ListView
in AfterLabelEdit
caused an exception.
- 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