Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Exploring OBEX Devices Connected via Bluetooth

4.77/5 (42 votes)
26 Oct 2008CPOL9 min read 1   7.7K  
A sample application that shows how to browse an OBEX device and transfer files to it.
ObeXplorer.jpg

Contents

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:

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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:

C#
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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)