TL;DR
You can find the essential part of the code at the bottom of this blog, and the full code here.
The Story
I got a new car recently, which has a bunch of cool built-in apps. These apps require an internet connection and for me Bluetooth tethering was the easiest way of providing that. You can enable Bluetooth tethering in your android settings like so:
Bluetooth tethering turns off automatically after a period of inactivity, which in my case meant that I had to go into my android settings every time I got into my car. Obviously, that is not tolerable by the proud developer that I am. So began my journey to auto-enable Bluetooth tethering when my phone made a connection to my car.
Two problems that I needed to solve:
- How do I know when my phone connects to my car?
- How to enable Bluetooth tethering programmatically?
The first problem is easily solved by using a BroadcastReceiver
. The second one was not so easy ... like not easy at all. This is what this blog is about.
Expectations versus Reality
I expected something like this:
- add some permission
- write code like
BluetoothSettings.setTetherState(true)
for the permissions part I was right, I just had to add these guys:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
For the code I did some googling. It appeared I needed a class called BluetoothPan
and use a method called setBluetoothTethering
. Easy peasy, right?
Except, there is no BluetoothPan class in Xamarin.Android. It simply doesn't exist.
Ok, no problem. I'll just add my own binding, I thought. So, I looked up the Java code. This is what I found:
Java Code
Class<?> classBluetoothPan = Class.forName("android.bluetooth.BluetoothPan");
Constructor<?> ctor = classBluetoothPan.getDeclaredConstructor(
Context.classBluetoothProfile.ServiceListener.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(getApplicationContext(),
neBTPanServiceListener(getApplicationContext()));
// Set Tethering ON
Class[] paramSet = new Class[1];
paramSet[0] = boolean.class;
Method setTetheringOn = classBluetoothPan.getDeclaredMethod("setBluetoothTethering", paramSet);
setTetheringOn.invoke(instance,true);
If you don't know what you're looking at, this is reflection in Java. You cannot even use the class directly in Java. You have to use bloody reflection even in the native language!
Ok, scarp the binding. I'll need to use reflection. But not C# reflection, no no, Java Reflection!
But ok, I can do this. Just translate line by line.
After the translation and some cleanup, I got this:
C# Code
public void SetBluetoothTetherState(bool isEnabled)
{
var bluetoothPanClass = Class.ForName("android.bluetooth.BluetoothPan");
var btPanCtor = bluetoothPanClass.GetDeclaredConstructor(Class.FromType(typeof(Context)),
Class.FromType(typeof(IBluetoothProfileServiceListener)));
btPanCtor.Accessible = true;
var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethod("setBluetoothTethering",
new Class[] { Class.FromType(typeof(bool)) });
var btServiceListener = new BTPanServiceListener();
var btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);
setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
}
So, we basically fetch a hidden constructor, create an instance and call the setBluetoothTethering method.
I also had to create some dummy class to pass to the constructor:
class BTPanServiceListener : Java.Lang.Object, IBluetoothProfileServiceListener
{
public void OnServiceConnected([GeneratedEnum] ProfileType profile, IBluetoothProfile proxy) {}
public void OnServiceDisconnected([GeneratedEnum] ProfileType profile) { }
}
Ok let's take it for a spin! Let's fire up the Emulator and ... Wait. um how do I test Bluetooth on an emulator? So, I turned to my good friend google and this is what google told me:
After 25 minutes of intensive original-cable-that-supports-data searching, I finally got my phone connected and ready to go. The results were not great:
Ok, I guess that makes sense. So, let's use the Java type:
var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethod("setBluetoothTethering",
new Class[] { Class.FromType(typeof(Java.Lang.Boolean)) });
Right, boolean
(lower case) and Boolean
(upper case) are two different things in Java. The first represents a primitive, the second is an object wrapper. setBluetoothTethering
requires the primitive.
Google told me to use Java.Lang.boolean
Class.FromType(typeof(Java.Lang.boolean))
No such thing ...
Google told me boolean was represented as the type 'Z' internally:
Class.FromType(typeof(Java.Lang.Z))
No such thing ...
Ok enough of this! Let's just reverse engineer it. I'll just get all the methods and inspect the parameter types.
var methods = bluetoothPanClass.GetDeclaredMethods();
var method = methods[17]; //setBluetoothTethering happend to be number 17 in the collection
var parTypes = method.GetParameterTypes();
var setBluetoothTetheringMethod = bluetoothPanClass
.GetDeclaredMethod("setBluetoothTethering", parTypes);
Now this actually worked and gave me the right method. Upon inspection of the parTypes
I found the following:
Not a lot of information other than the name. So, this I suppose?
Class.ForName("boolean")
I turned to google, bing, Boeddha, google again and crying. Nothing could tell me what to do.
Ok, screw this. I got the method just fine in the previous part. I'll just do it like this:
var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
.Single(m => m.Name == "setBluetoothTethering");
(seriously, if anyone knows how to get the boolean primitive type in Xamarin, please write it in the comments below.)
It works, I got the method, I'm fine. Let's call it now.
var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
.Single(m => m.Name == "setBluetoothTethering");
var btServiceListener = new BTPanServiceListener();
var btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);
setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
No errors?
Let's check the settings to see if I really turned in on:
.
.
.
.
.
Oh wait!
I know what's going on! Why would I need to create a ServiceListener otherwise? This BluetoothPan
class is probably a bound service. That means I have to wait for the service be ready!
Final Code
public void SetBluetoothTetherState(bool isEnabled)
{
var bluetoothPanClass = Class.ForName("android.bluetooth.BluetoothPan");
var btPanCtor = bluetoothPanClass.GetDeclaredConstructor(Class.FromType(typeof(Context)),
Class.FromType(typeof(IBluetoothProfileServiceListener)));
btPanCtor.Accessible = true;
var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
.Single(m => m.Name == "setBluetoothTethering");
setBluetoothTetheringMethod.Accessible = true;
var btServiceListener = new BTPanServiceListener();
var btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);
btServiceListener.ServiceConnected += (s, e) =>
setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
}
class BTPanServiceListener : Java.Lang.Object, IBluetoothProfileServiceListener
{
public event EventHandler ServiceConnected;
public void OnServiceConnected([GeneratedEnum] ProfileType profile, IBluetoothProfile proxy)
=> ServiceConnected?.Invoke(this, EventArgs.Empty);
public void OnServiceDisconnected([GeneratedEnum] ProfileType profile) { }
}
I raise an event when the service is connected and only then I call setBluetoothTethering
. Let's see if it works now.
I'm going to lie down now. Have fun with this. Once again, you can find the full code here.