Share this page to your:
Mastodon

Just a little background. I built a Vortex Manipulator (basically a smart watch that runs on a Teensy3 and does a bit more than a simple clock. I wanted to have notifications from my phone appear on the watch’s screen via Bluetooth, actually BLE. This is about some of the unexpected things I found to get this working.

project code is here

Structure

I knew from the start I needed three basic components. The UI, something to capture Notifications and something to handle Bluetooth. The UI could be trivial. I want to be able to start the app, connect to my BLE device and see some confirmation of that connection. Nothing more. So, while there is a lot of richness in the Android Studio for making great UIs, this was not my interest.

It needs to look something like this:

components

The way to define these relationships int Android Studio is to use the AndroidManifest.xml file. Mine looks like this.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.madurasoftware.vmble">

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />

    <application
...
        <activity
            android:name=".MainActivity"
...
        </activity>

        <service
            android:name="com.madurasoftware.vmble.BLEService"
            android:process="com.madurasoftware.vmble.VMServices"
            android:label="BluetoothService">
        </service>
        <service
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
            android:name="com.madurasoftware.vmble.NotificationService"
            android:label="NotificationService">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>
    </application>

I've shortened this a bit to focus on the relvant bits. The full file is here).

The first thing to notice is the permissions. These have to be there in any app that wants to use Bluetooth. With this in place if the phone has Bluettoth turned off when the app starts then Android will ask to turn it on.

In the application section there is the activity which is essentially the UI I mentioned above, and two service sections, one for Bluetooth and one for Notifications.

Now I want to cover what is inside these components.

Notifications.

Notifications, as noted above, are handled by a service. The service is NotificationService and it extends NotificationListenerService which is supplied by the Android environment. There is not too much to it. It hears about a notification when its onNotificationPosted() method is called. It filters the notification (there are a lot of system notifications I don't want to see) and formats the result suitable for sending, then it passes the notification to the Bluetooth service.

I'll come back to that last bit later on. It's really interesting.

The Notification service is launched from the MainActivity with this code:

val notificationIntent = Intent(context, NotificationService::class.java)
notificationIntent.putExtra(BLEService.CONNECTION,d.address)
context.startService(notificationIntent)

There is just a little more to it but that's the main part. We create an intent and launch a service. The service stays running in the background (or seems to). When a notification arrives it handles it.

Now let's go back and look at the code that passes the notification on to the Bluetooth service:

val connectIntent = Intent(this.applicationContext, BLEService::class.java)
connectIntent.putExtra(BLEService.MESSAGE,message)
connectIntent.putExtra(BLEService.CONNECTION,mBluetoothDeviceAddress)
this.applicationContext.startService(connectIntent)

Look familiar? Yes, it is just another start service call. I find it odd because I already started the service when I selected the BLE device to connect to. But believe me this is the way that works.

I tried several ways to pass the message from the NotificationService to the BLEService:

  • A LinkedBlockingQueue used such that NotificationService puts a message on the queue and BLEService takes the message off and processes it. This doesn't work because the two services do not share memory.
  • Doing the Notification handling inside the BLEService. This doesn't work because the BLEService has separate process (this is the reason for android:process="com.madurasoftware.vmble.VMServices" in the manifest) and if I put notification handling in a separate process it stops receiving notifications.
  • I also tried messing about with the bind() call but that got more complicated than it ought to be.
  • BroadcastMessage didn't work either (see below)

So let's look at what happens in the BLEService.

BLEService

These services all call onStartCommand() when they are 'started. Now, you can see if you look at the code above, that different putExtra calls are used dpedning on whether we want to start with a BLE connection or if we want to send a message. So the first thing the onStartCommand has to do is figure out which kind of start we are doing.

But also notice that in both kinds of start we pass the BLE address. So the logic in the service looks for a message and stores it in a queue if it is there, then looks for an address. If there is an address it connects to it. The connect is in a loop which allows reconnects and sending any messages from the queue until it is empty. The the service can stop.

While the service is managing its connection it uses the broadcastMessage() to pass the status to the UI. This is a built in mechanism for passing messages around. It's in the list of things I tried to use to pass the message to BLEService from NotificationService. But it didn't work. broadcastMessage() only allows you to pass information to the UI, not to a service.

There is an interesting twist when actually sending the message to BLE. There is quite a small size limit, just 19 bytes. When I used a full Bluetooth device it was so much larger I did not have to look for a limit, I could just send the whole string. With BLE I have to chunk it into 19 byte segments.

Conclusion

Be careful when designing Android systems that rely on backend services. Communicating between them is not particularly well documented. I've been referring to these different components as 'processes' but I suspect they aren't. There are various references to them being different threads, but sometimes that they run on the main thread even though they are separate processes. So I had to experiment to figure out what actually worked. Hopefully some of this can shorten your path a little.

Previous Post Next Post