Make a Xamarin.Android widget to enable/disable Bluetooth

August 23, 2014

In the previous post, I showed how to build a simple Xamarin.Android widget for monitoring the device’s Bluetooth adapter status. It didn’t do much, simply displaying whether or not the Bluetooth adapter was turned on. It covers a number of other aspects of widget development though, so if there’s something you don’t understand in this post, go back and have a read of that one first. No rush. I’ll wait :).

This post develops onwards from that one. It shows you how to monitor the connectivity status and, as promised, how to use the widget to enable and disable Bluetooth on your device (which actually makes it a little more useful).

The full source of this project is on GitHub, so pop that open and have a look, if you want to use it to follow along…

I’ve made some minor changes to the naming of things (for example the main class is now called BTToggleWidget), but the principles are exactly the same.

Some New Status Images

I won’t rehash the details of how to set up the images and drawables, but I did add a couple of new ones for ‘connected’ and ‘connecting’/’disconnecting’.

  • bluetooth connected
  • bluetooth connecting/disconnecting

On faster devices, you won’t really get to see the transition from ‘Connecting’ to ‘Connected’ to ‘Disconnecting’, but I don’t think that’s a problem.

Adding the Connectivity State Intent

Monitoring the connectivity status changes of the Bluetooth adapter is done exactly the same way as it was for monitoring the Bluetooth adapter power state. All you have to do to catch notifications of changes which occur when a device connects and disconnects is to add a new intent type to the filter, and a handler for it.

The new intent is called BluetoothAdapter.ActionConnectionStateChanged, and you can just add it to the current list of intents which are filtered by the widget:

[BroadcastReceiver(Label = "Bluetooth Toggle Widget")]
[IntentFilter(new string[]
{ "android.appwidget.action.APPWIDGET_UPDATE",
BluetoothAdapter.ActionStateChanged,
BluetoothAdapter.ActionConnectionStateChanged
})]
[MetaData("android.appwidget.provider", Resource = "@xml/bt_widget")]
public class BTToggleWidget : AppWidgetProvider

Then add an additional filter handler for when one of these BluetoothAdapter.ActionConnectionStateChanged intents arrives, to the OnReceive method:

/// <summary>
/// This event fires for every intent you're filtering for. There can be lots of them,
/// and they can arrive very quickly, so spend as little time as possible processing them
/// on the UI thread.
/// </summary>
/// <param name="context">The Context in which the receiver is running.</param>
/// <param name="intent">The Intent being received.</param>
public override void OnReceive(Context context, Intent intent)
{
  Log.Info(Constants.APP_NAME, "OnReceive received intent: {0}", intent.Action);

  // ...some code omitted for brevity...

  if(intent.Action == Android.Bluetooth.BluetoothAdapter.ActionConnectionStateChanged)
  {
    Log.Info(Constants.APP_NAME, "Received BT Connection State change message");
    ProcessBTConnectionStateChangeMessage(context, intent);
    return;
  }
}

…and add a new method (I called mine ProcessBTConnectionStateChangeMessage) for processing the connection state change Extra payload:

private void ProcessBTConnectionStateChangeMessage(Context context, Intent intent)
{
  int prevState = intent.GetIntExtra(BluetoothAdapter.ExtraPreviousConnectionState, -1);
  int newState = intent.GetIntExtra(BluetoothAdapter.ExtraConnectionState, -1);
  string message = string.Format("Bluetooth Connection state change from {0} to {1}", (State)prevState, (State)newState);
  Log.Info(Constants.APP_NAME, message);
  UpdateWidgetDisplay(context, newState);
}

As you can see, this functions in exactly the same way as the ProcessBTStateChangeMessage mentioned in the previous article. It checks the previous and new states, and then sends the new connection state on to the UpdateWidgetDisplay method.

private void UpdateWidgetDisplay(Context context, int newState)
{
  var appWidgetManager = AppWidgetManager.GetInstance(context);
  var remoteViews = new RemoteViews(context.PackageName, Resource.Layout.initial_layout);
  var thisWidget = new ComponentName(context, this.Class);

  int imgResource = Resource.Drawable.bluetooth_off;
  State currentState = (Android.Bluetooth.State)newState;
  switch(currentState)
  {
    case Android.Bluetooth.State.Off:
    case Android.Bluetooth.State.TurningOn:
    {

  // ... snip ...

    case Android.Bluetooth.State.Connecting:
    case Android.Bluetooth.State.Disconnecting:
    {
      imgResource = Resource.Drawable.bluetooth_connecting;
      break;
    }
    case Android.Bluetooth.State.Connected:
    {
      imgResource = Resource.Drawable.bluetooth_connected;
      break;
    }
    case Android.Bluetooth.State.Disconnected:
    {
      imgResource = Resource.Drawable.bluetooth_on;
      break;
    }
    // ... snip ...
  }
  remoteViews.SetImageViewResource(Resource.Id.imgBluetooth, imgResource);

  // ... snip ...

This leaves us with a widget that can track and display the connection-state changes of the Bluetooth adapter, as other devices connect and disconnect. However, it also leaves us with the same problem as in the previous article: it only responds to changes in status, but doesn’t check to see what the connection state is when it starts up.

Easily fixed, yes? As before, all we have to make a call to ask the adapter whether it’s connected when we initially place the widget on the home screen.

Except it turns out it’s not quite that simple. It’s not a big deal though. It’s just that there’s no genericky ‘adapter.IsConnected’ or ‘adapter.ConnectionStatus’ property or function we can call directly. We have to query each Bluetooth connection profile type individually, to see if it’s connected to something (and if we care, what it’s connected to).

So I slapped together a quick method to do just that:

 /// <summary>
/// Clunky implementation of getting connection state for the different profiles.
/// Sufficient for our purposes though. Could expand it to include what we're connected
/// to as well.
/// </summary>
/// <returns>The Bluetooth connection state.</returns>
private ProfileState GetBluetoothConnectionState()
{
  var profileTypes = new ProfileType[] { ProfileType.A2dp, ProfileType.Gatt, ProfileType.GattServer, ProfileType.Headset, ProfileType.Health };
  bool connected = profileTypes.Any(pt => BluetoothAdapter.DefaultAdapter.GetProfileConnectionState(pt) == ProfileState.Connected);
  return connected ? ProfileState.Connected : ProfileState.Disconnected;
}

This just iterates over the list of available profiles to see which (if any) are connected. At this point, I’m only interested in whether it’s connected to at least one device. I’m not interested in what it’s actually connected to. That’s an exercise for another day.

Now that I can quickly determine whether I am connected to something, I just need to invoke that function from where the widget is initialised. This is up where the widget update/initialisation android.appwidget.action.APPWIDGET_UPDATE intent is caught (i.e. up at the top of OnReceive):

public override void OnReceive(Context context, Intent intent)
{
  Log.Info(Constants.APP_NAME, "OnReceive received intent: {0}", intent.Action);
  if(intent.Action == "android.appwidget.action.APPWIDGET_UPDATE")
  {
    // ... snip ...

    Log.Info(Constants.APP_NAME, "BT adapter state currently {0}", currentState);
    UpdateWidgetDisplay(context, (int)currentState);
    if(currentState == State.On)
    {
      Log.Debug(Constants.APP_NAME, "Checking Bluetooth connection state...");
      ProfileState currentConnectedState = GetBluetoothConnectionState();
      UpdateWidgetDisplay(context, (int)currentConnectedState);
    }
    return;
  }

  // ... snip ...

It makes sense to only bother checking the connectivity state if the Bluetooth adapter state is already set to ‘On’.

So now we can update the display to show not only whether your Bluetooth adapter is turned on, but also whether it’s connected to something.

So what? I can see the same information in the phone’s notification/status bar.

Widget and notification/status bar

The original goal was to build a widget that could enable and disable the Bluetooth adapter without the Nexus 5’s flipper-draggingly stupid process of ‘using-two-fingers-swipe-down-and-tap-bluetooth/wifi/whatever-then-wait-then-tap-‘on’/’off’-to-enable/disable-then-tap-‘back’-twice’.

I want to be able to do it with a single tap. Once for on. Once for off.

Making The Widget Do Stuff

We need the widget to react like a button, when tapped. And it’s one of the reasons I picked an ImageButton to start off with. We need something with a click (or click-like) event. Unfortunately, due to the way widgets are designed to work, you can’t just add code into an OnClick event, and go, as you would with a normal button.

There’s a chance you may have more than one widget of the same type on your home screen (well, ok, probably not more than one of these specific widgets). And you need each widget to behave the same way and do the same thing, regardless of which widget you’ve just tapped. This means that even if we have five widgets on the screen, we want to be able to tap any one of them, and have the same operation occur. We’d need their OnClick handlers to be in sync, and run the same code, independently of which widget is reacting to an update or tap.

Fortunately, this is quite easy to do. The RemoteViews object can bind certain events and properties to other objects, for precisely this purpose. We’re already doing it with setting the status image into the ImageView Resource on the ImageButton, with the remoteViews.SetImageViewResource(Resource.Id.imgBluetooth, imgResource) call.

Unfortunately, it doesn’t allow us to assign actual code to those events. Annoying on the face of it, but if you think about it, it’s the smart thing to do. Depending on what your requirements, you’d have to set up some sort of closure, with an action or function which would potentially be long-running, maybe with multiple threads, perhaps needing a callback attached, and probably some exception handling. And it would need to be locked for possible thread safety issues. And/or re-entrancy. Oh, and then you’d probably have to serialise data into and out of it somehow, so you could pass it around using Android’s intent messaging system. And if you break something, it may cause your widget to ANR, which is A Bad Thing. And because widgets can’t be attached/debugged, you could end up completely screwed if it doesn’t work, because you’ll never know what broke…

Yuck.

So, they force you to push the code for the operations you need to perform to somewhere a little more sane. And they do this by getting you to set up a PendingIntent, which points to the process that you would like to occur when you click the widget. This then turns the widget into a kind of launcher, instead. Things suddenly become more loosely coupled, and they’re isolated, attachable, and swappable.

Like I said: a little more sane.

It’s a lot easier for the RemoteViews instance to hook up a PendingIntent to your OnClick, let you tack on whatever data you need, and then pass that little bundle of joy to all the other widgets of this type. Then they become launchers too. Then you build the actual process you want to run into a Service or Activity. It’ll then be that object’s responsibility to manage all that stuff. This is as it should be.

So let’s make some PendingIntents. We’ll need two. One to enable the Bluetooth adapter, and one to disable it. We’ll also need a tiny bit of coding logic to turn them into a toggle, depending on the current state of the Bluetooth adapter.

Building the ‘Enable’ PendingIntent

The first one we’ll make is to request the Bluetooth adapter to become enabled. Best practices say you shouldn’t just enable the adapter without the user’s permission anyway. Fair enough, it’s all about privacy these days, so no problem there. We’ll do it the way they suggest, and as you’ll see, it takes care of asking the user for us, too.

Set up an Intent for the request (it’s called BluetoothAdapter.ActionRequestEnable), wrap it in a PendingIntent and ask the remoteViews instance to bind it to the ‘click’ method of the Image button. Then tell all the other widgets of this type to do the same thing:

Inside the UpdateWidgetDisplay method, after you update the ImageButton’s display with the current status image, place the following code:

private void UpdateWidgetDisplay(Context context, int newState)
{

// ... snip ...

  remoteViews.SetImageViewResource(Resource.Id.imgBluetooth, imgResource);
  switch(currentState)
  {
    case State.Off:
    {
      Log.Info(Constants.APP_NAME, "Adapter state is 'off', adding click delegate to turn on Bluetooth ");
      Intent enableBluetoothIntent = new Intent(BluetoothAdapter.ActionRequestEnable);
      PendingIntent pendingIntent = PendingIntent.GetActivity(context, 0, enableBluetoothIntent, PendingIntentFlags.UpdateCurrent);
      remoteViews.SetOnClickPendingIntent(Resource.Id.imgBluetooth, pendingIntent);
      break;
    }
    default:
    {
      // we'll be back here in a moment, to turn it off again
      break;
    }
  }
  appWidgetManager.UpdateAppWidget(thisWidget, remoteViews);

}

We use PendingIntentFlags.UpdateCurrent to ensure that if there’s already a pending intent attached to the other widgets, then they’re updated with this one. If there aren’t any, then this one will simply become the pending intent attached to each widget’s OnClick event. This isn’t a real issue with this particular widget because we’re not attaching anything extra to it, but if you had additional data to pass along with the intent, this would swap out any existing pending intent for this new one.

The intent to enable the Bluetooth is then wrapped in a PendingIntent (think of it as a kind of closure function pointer in this case), and attached to the imgBluetooth ImageButton via remoteViews.SetOnClickPendingIntent(Resource.Id.imgBluetooth, pendingIntent).

If you build this project at this point, upload the widget, and then tap it when Bluetooth is off, you’ll be presented with the following ‘permission request’ dialog (it may differ slightly from device to device):

Request permission to enable Bluetooth

If you click ‘Allow’, the Bluetooth adapter will become enabled (this may take second or two). If you click ‘Deny’, then nothing will happen.

Now let’s get it to turn it off again.

Building the ‘Disable’ PendingIntent

So, there’s no BluetoothAdapter.ActionRequestDisable intent. Or similar. Bet you saw that one coming. Turns out that if we want to turn the adapter off, we whave to actually call the BluetoothAdapter.DefaultAdapter.Disable() method.

Why? Why couldn’t they just give us an ActionRequestDisable? With a corresponding permission dialog? How hard would it be?!

*Sigh*

OK then. We know we can’t put code into the OnClick event directly, so we’ll need to put that Single Line Of Code into its own Activity or Service, and launch it as described above. I actually picked an IntentService for this because it’s simpler, runs on its own thread, and cleans up after itself. There’s no need to bind to it or call StopSelf() at the end.

So here’s my IntentService (the whole thing):

using Android.App;
using Android.Appwidget;
using Android.Content;
using Android.Bluetooth;
using Android.Util;
using Android.Widget;
namespace BluetoothToggleWidget
{
  [Service]
  class DisableBluetoothService : IntentService
  {
    public DisableBluetoothService() : base("DisableBluetoothService")
    {}

    protected override void OnHandleIntent(Intent intent)
    {
      Log.Info(Constants.APP_NAME, "Received request to disable Bluetooth");
      BluetoothAdapter.DefaultAdapter.Disable();
    }
  }
}

Nothing fancy. We don’t even need to add an IntentFilter or BroadcastReceiver to it, because it only does one thing.

And then set up our widget to launch it, using a PendingIntent:

remoteViews.SetImageViewResource(Resource.Id.imgBluetooth, imgResource);
switch(currentState)
{
  case State.Off:
  {
    // ...snip... we already did this bit
  }
  default: // anything OTHER than 'Off'
  {
    Log.Info(Constants.APP_NAME, string.Format("Adapter state is {0}, adding click delegate to turn off BT", currentState.ToString()));
    Intent disableBluetoothIntent = new Intent(context, typeof(DisableBluetoothService));
    PendingIntent pendingIntent = PendingIntent.GetService(context, 0, disableBluetoothIntent, PendingIntentFlags.UpdateCurrent);
    remoteViews.SetOnClickPendingIntent(Resource.Id.imgBluetooth, pendingIntent);
    break;
  }
}
appWidgetManager.UpdateAppWidget(thisWidget, remoteViews);

Basically the same thing as before: we create an Intent pointing at a DisableBluetoothService type, get a pointer to it via the PendingIntent.GetService() call (note that it’s GetService and not GetActivity), and finish up as before.

And we’re done.

Build and upload it to the device (with Bluetooth turned off), and tap once to turn it on. ‘Allow’ the Bluetooth to be enabled. And when it’s enabled, just tap the widget again to turn it off. Notice that it doesn’t ask you if you want to turn it off, it just clobbers it (or at least it does on the three devices I’ve tested it on). Best practice advises us to get the user to confirm the disabling of it too, but I’ll leave that up to you to implement.

Enabling the Adapter Without Permission

UPDATE: If you want to just enable the Bluetooth adapter without forcing the user to go through a permission dance, then you don’t need to use the intent-method explained above. All you need to do is use the same methodology yuou did for disabling it, i.e. create an EnableBluetoothService, with a corresponding BluetoothAdapter.DefaultAdapter.Enable() method call in it. That’ll have the desired effect.

Conclusion

OK, so it’s not quite a single-tap toggle switch, but it’s good enough for me at this point. I can live with tap-tap for on, and tap-for-off. It does what I need it to do.

If you’ve been following along in the code, you’ll notice I’ve omitted a lot of the logging calls here. This was just for brevity. But if you hook your device up to your development environment, and run the Android monitor (in the SDK tools, probably under C:\Users\userName\AppData\Local\Android\android-sdk\tools\lib\monitor-x86\monitor.exe), you can see what it’s doing under the hood.

Below is a dump of the log as the widget undergoes a full life-cycle

  • The widget being added to the home screen (Bluetooth off)
  • Tapping to enable Bluetooth
  • Bluetooth becoming enabled
  • Automatic connection to my Bluetooth headset
  • The headset disconnecting as I turn it off
  • Tapping to disable the Bluetooth.
  • Removing the widget from the home screen.

(I added a ‘BTToggleWidget’ filter using a log tag of BTToggleWidget and set log level to ‘Verbose’, and then just copied and pasted)

: I/BTToggleWidget(4669): OnReceive received intent: android.appwidget.action.APPWIDGET_UPDATE
: I/BTToggleWidget(4669): Received AppWidget Update
: I/BTToggleWidget(4669): BT adapter state currently Off
: I/BTToggleWidget(4669): Adapter state is 'off', adding click delegate to turn on BT 
: I/BTToggleWidget(4669): OnReceive received intent: com.motorola.blur.home.ACTION_SET_WIDGET_SIZE
: I/BTToggleWidget(4669): OnReceive received intent: mobi.intuitit.android.hpp.ACTION_READY
: I/BTToggleWidget(4669): OnReceive received intent: android.appwidget.action.APPWIDGET_UPDATE_OPTIONS
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Action State change message
: I/BTToggleWidget(4669): Bluetooth Adapter state change from Off to TurningOn
: I/BTToggleWidget(4669): Adapter state is TurningOn, adding click delegate to turn off BT
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Connection State change message
: I/BTToggleWidget(4669): Bluetooth Connection state change from Disconnected to Connecting
: I/BTToggleWidget(4669): Adapter state is Connecting, adding click delegate to turn off BT
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Action State change message
: I/BTToggleWidget(4669): Bluetooth Adapter state change from TurningOn to On
: I/BTToggleWidget(4669): Adapter state is On, adding click delegate to turn off BT
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Connection State change message
: I/BTToggleWidget(4669): Bluetooth Connection state change from Disconnected to Connected
: I/BTToggleWidget(4669): Adapter state is Connected, adding click delegate to turn off BT
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Connection State change message
: I/BTToggleWidget(4669): Bluetooth Connection state change from Connected to Disconnected
: I/BTToggleWidget(4669): Adapter state is Disconnected, adding click delegate to turn off BT
: I/BTToggleWidget(4669): Received request to disable bluetooth
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Action State change message
: I/BTToggleWidget(4669): Bluetooth Adapter state change from On to TurningOff
: I/BTToggleWidget(4669): Adapter state is TurningOff, adding click delegate to turn off BT
: I/BTToggleWidget(4669): OnReceive received intent: android.bluetooth.adapter.action.STATE_CHANGED
: I/BTToggleWidget(4669): Received BT Action State change message
: I/BTToggleWidget(4669): Bluetooth Adapter state change from TurningOff to Off
: I/BTToggleWidget(4669): Adapter state is 'off', adding click delegate to turn on BT 

There’s a lot of redundant setting of things as the intents arrive, and I guess there’s no real way around that short of introducing a bunch of flags for controlling program flow. And in my opinion, for what this widget needs to do, it’ll add a metric buttload of unnecessary complexity, with no real benefit.

Google Play

I’ve also just released it as an app on the Google Play store, because reasons, and why not?

References

Some additional references which you may find useful:

License

The code for this project has been released under the MIT License, so you can do what you like with it. However one of the Bluetooth symbol images is provided by a third-party, and was distributed as ‘freeware, not for commercial use’. So if you plan on using the code in this repo as the basis for a commercial product (go right ahead!), you’ll need to source your Bluetooth symbol image(s) from elsewhere.


comments powered by Disqus