“Watts App” — Is my phone charged yet? Let me check my watch
I’ve had an Android Wear watch for the last 18 months or so, a first generation Moto 360, and I’ve always had a vague intention to do something with it. Something cool, some project I could pump a load of code into that would make this smartwatch truly mine. I’d looked at documentation about Android Wear. I’d read blog posts. I’d seen watch apps written live on stage at conferences, and deployed, in front of an ooohing, aaahing audience. I was going to do that too, and it was going to be awesome.
Except I could never figure out what to do. There’s no documentation for the APIs in our heads that return project ideas. Well, no Javadocs anyway. Definitely nothing Swagger-compliant.
Last week I found myself looking at my watch to see if my phone was fully charged yet. I was ensconced for the day in a meeting room with some co-workers, discussing very secret things, while my phone charged at my desk, a few meters away. Surely, in this day and age, my watch can tell me if it’s done yet, right?
I did zero research. I bet there’s watch faces, and watch apps, and all sorts of tools that could solve the problem for me. But I just looked at my watch and I couldn’t figure out a way to see it. And that’s when I finally had an idea for something to code on Android Wear.
TL;DR — If you’d rather not read all the words I wrote, and would rather see the code I wrote, check out the repo on my github.
A New Idea has Entered the Game!
I wanted something to show on my watch when my phone was charging, which showed the charge level of the device as a percentage, and was updated periodically. The Creative Vision for Android Wear says its experiences should be:
- Automatically launched
- Glanceable
- All about suggest and demand
- Zero or low interaction
I’ve always thought this could be summed up as: “Notifications. Just do notifications. Properly.”
The mirroring of notifications from my phone to my watch is about 90% of the value I get from it. The other 10% is in context-specific use cases: handy controls for Google Play Music, Pocket Casts, and Runkeeper. I think I can count on one hand the number of times I’ve gone swiping through the interface to launch an app or send a message — and even less times when I’ve used voice input. And I reckon every one of those instances was when I was demoing that functionality to someone who’d never seen a smartwatch before.
So all I wanted was a notification on my phone. Cool, those are easy, the NotificationManager is my friend. What about responding to and reading battery-charging events? That’s easy too, there’s a handy article on the Android developer docs about it. What about scheduling a task to update the notification once we’d created it?
There are multiple ways to set up a repeating task in the background, like JobScheduler and AlarmManager. There’s other tools too, like GcmNetworkManager, which has a confusingly network-and-GCM-centric name, and straight-up Services were never much fun for me. Evernote released their own library, android-job, that allows you to get on with coding and not worry about which mechanism is being used under the hood. But I was all fired up and enthusiastic, and I’d used JobScheduler once before on a little project, so I went with that. It’s the latest cool new toy for scheduling background jobs! It’s supposed to be great! It’s the way forward for Android! And hey, I didn’t want have to go read a bunch of documentation!
So I chose to use JobScheduler, and immediately had to go read a bunch of documentation. It was added in API 21, Lollipop. That’s OK, my phone runs Nougat (API 25), so we’re all good. JobScheduler allows us to leverage the operating system’s intelligence to decide when to run our jobs, batching them appropriately, in order to minimise operational costs to the device. This rang a couple of bells in my head, since I only wanted my job to run when my phone was charging anyway, so any potential impact on the battery wasn’t a problem (unless it was huge). I didn’t have to worry about the consequences of waking up the CPU unnecessarily. But I forged on ahead anyway (this is commonly known as foreshadowing).
“Watts App” — The Implementation
The app I built is very, very simple. It’s written in Kotlin, because Kotlin is awesome and the semi-colon key on my keyboard is broken. I decided to call it “Watts App”, because it was about charging my battery and I don’t understand how electricity works.
The app contains:
- An
AndroidManifest,
- A
BroadcastReceiver
which gets called anytime the phone gets plugged in or out, - A
JobService
which gets scheduled if it was anACTION_POWER_CONNECTED
event or cancelled if it was anACTION_POWER_DISCONNECTED
event - And a single
Activity
, which is really only there for debug purposes. I’d never made an app without anActivity
before. It felt weird.
Declaring the BroadcastReceiver
The first thing to do with a BroadcastReceiver
is register it in your manifest file. Well, second thing — you need to decide on a name for it first. I took “inspiration” from the training article and called mine PowerConnectionReceiver
. You need to specify the broadcasts you want it to respond to. In my case, these are the battery charging/not-charging events:
You’ll notice there’s a third intent-filter
action
added here, named brendan.wattsapp.debug
. While working out the kinks in the code, it was frustrating to have to rely on plugging the phone in and out to make things happen — especially given that unplugging the phone severs the ADB connection, and therefore any debugging or logging.
Side note: I was about to write, “Yes, I know wireless ADB exists, but you need root for that, and this is the first reason I’ve had to root a phone in years” and then I decided to Google it to back up my assumption, and found that the very first result is an app called ADB Wireless (no root), so I’m feeling rather silly right now.
You can trigger a BroadcastReceiver
directly by broadcasting an Intent
with the same action as the one you defined in the manifest file. So I added a Button
to my MainActivity
, and had the button call:
And then I had a handy way to hit my BroadcastReceiver
without having to unplug my phone!
Writing the BroadcastReceiver
The onReceive
method of your BroadcastReceiver
receives the Intent
used to trigger it, as a parameter. According to the training article, you can use this to extract the current charging state and method
.
This didn’t work as advertised for me. If someone can point out a mistake, I’d be glad to learn where I went wrong. Guided by the article, I tried:
isCharging
, isUnknown
and hasBeenDisconnected
all came out of this false
for me. status
was -1, the default value if BatteryManager.EXTRA_STATUS
didn’t match an int extra. ¯\_(ツ)_/¯
Luckily, you can register a null BroadcastReceiver
with an IntentFilter
for Intent.ACTION_BATTERY_CHANGED
any time you want, and this immediately returns an Intent
with the latest battery state. I modified my PowerConnectionReceiver
to ignore the Intent
passed as a parameter to onReceive
and request one from the system.
My status
was now showing meaningful values. Once I knew whether the phone had been plugged in or out, I could decide whether to schedule or cancel my job:
Building the JobInfo
object is more interesting:
You need to instantiate a ComponentName
for the JobService
class — the actual work that you want to schedule — and use it to create a Builder
for the JobInfo
, along with a job ID. The job ID can be used later to cancel a specific job (using JobScheduler’s cancel method), or, if on API 24 or above, to retrieve the JobInfo
object for a job which has not been executed yet, using getPendingJob(jobId).
JobScheduler’s power comes from its flexibility. You can build your JobInfo
with constraints on available network type, device charging state, timing and scheduling, backoff strategy — I’d highly recommend having a look at the documentation if you haven’t seen it already.
For me, I didn’t require a network, required the device to be charging (lol), and wanted the job to run every 60 seconds. Of course, the interval you send to the operating system tells it how often you want it to run the job. The OS reserves the right to do whatever the hell it wants with that information, but it’s nice to express your opinions, even if nobody is listening (see Twitter, this post, and so forth).
Declaring the JobService
Like all Service
class, your JobService
needs to be declared in your AndroidManifest.xml
. You need to give it a name, and tell the system it’s allowed to be bound. Here’s mine:
Writing the JobService
In my PowerService
, I request another Intent
with the latest battery state, so I can show a notification with the charge level. (I had a momentary brain-fart trying to figure out where to get a Context
on which to call registerReceiver
, which made me kick myself and facepalm at the same time, in a new gesture I call the “kickface”. On the plus side, it also led me to the most efficient StackOverflow answer I’ve ever seen.)
The two important parts of the PowerService
are its interaction with the JobScheduler
and its use of the NotificationManager
.
onJobFinished
is a JobService
method that must be called in your onStartJob
, with the JobParameters
which were passed in, and an interesting boolean, needsReschedule
. This should be true
if the work you wanted to perform failed for some reason. true
tells the JobScheduler
to start applying whatever back-off strategy you set in the JobInfo
in an attempt to successfully complete the job. I like to live dangerously, so I assume that my notification was successfully posted, and return false
. The return value of the method is another boolean, which tells JobScheduler
whether your job is still doing work on another thread or not. Useful for asynchronous operations, not so much for this case. I return false
.
It’s almost beginning to seem like JobScheduler wasn’t the right tool for the job. Almost.
NotificationManager
NotificationManager’s notify method tells us:
If a notification with the same id has already been posted by your application and has not yet been canceled, it will be replaced by the updated information.
So now we know we can spam the notify method with the same notification ID, safe in the knowledge that we’ll only ever have one notification with the most up-to-date information.
Creating Notification
s is a breeze using the Notification.Builder
:
The only interesting thing here is the BatteryManager.EXTRA_LEVEL
retrieved from the battery status Intent
, which gives us the battery level as an integer between 0 and 100, and the setOnlyAlertOnce(true)
method on the Builder
. I want a notification I can look at, but I don’t want the updates bothering.
But honestly, I’m not too familiar with this API. I think you need to manually setLights
, setSound
, and setVibrate
if you want those things anyway, which I’m not doing, so there’s a good chance I can do without this method.
And that’s pretty much it! In onStopJob
, I clear out the notification with notificationManager.cancel(NOTIFICATION_ID)
. Here’s how it looks in action:
So there we have it! My first watch app, and I didn’t write a single line of code that had anything to do with Android wear! A notification that gets updated with the latest battery level every sixty seconds or so, and gets cleared when the phone is unplugged. Right?
JobScheduler
is a harsh mistress
From onStopJob
:
This method is called if the system has determined that you must stop execution of your job even before you’ve had a chance to call
jobFinished(JobParameters, boolean)
.
So this will only work if the phone gets unplugged in the time between onStartJob
being called and it calling jobFinished
. This is extremely unlikely to ever happen.
I was very excited the first time I ran the app. This was before I’d added my handy debug button in MainActivity
, so I unplugged the phone, re-plugged it in again to trigger the BroadcastReceiver
, and sat back to wait for my notification to show up. The interval for the job was set to 60 seconds, so I shouldn’t have to wait long. After much waiting, and unplugging, and re-plugging, and waiting again, I noticed this in the Logcat output:
brendan.wattsapp W/JobInfo: Specified interval for 123 is +1m0s0ms. Clamped to +15m0s0ms`
JobScheduler
gets to make its own mind up about when to run your job — that’s part of the point. But I’d expected this to be pretty inconsequential for lax parameters like mine. This log makes it look like I have a 1500% multiplier on my interval to look forward to. What gives?
Nougat is also a harsh mistress
I’m not the first person to notice this. I found an explanation on StackOverflow. As of Nougat, this tidbit appears in the documentation for JobInfo.Builder.setPeriodic
’s intervalMillis
parameter:
long
: Millisecond interval for which this job will repeat. A minimum value ofgetMinPeriodMillis()
is enforced.
getMinPeriodMillis
apparently returns fifteen minutes. You can usesetOverrideDeadline
and force the job to run — but a job can’t be both periodic and override the deadline.
Conclusion
This was a lot of fun. I wrote the first version of Watts App in a couple of hours after work earlier this week, and by the time I finished, it was mostly working. Mostly.
The notification only updates every fifteen minutes or so. That’s workable, but it’s not what I wanted. I added a really hacky line to my PowerConnectionReceiver
to cancel the notification when it was stopping the job:
So now at least it goes away automatically. But it’s clearly the wrong way to achieve that result.
One idea to improve things is to schedule two jobs: one with setOverrideDeadline
set to something very small, to show the first notification, and a second like the one above, to update periodically. Another, probably smarter idea, is to use the AlarmManager
to show and update the notification instead. The caveats of AlarmManager
are things like “it‘s not as responsible/efficient as JobScheduler” and “it doesn’t persist across reboots”, but they don’t matter to this particular use case. And I’ve never used it before. So I’ll probably try that.
Another foible of the current implementation is that the notification will show on the phone as well. That’s not really a problem, but it is a bit pointless. You can build a notification to be local-only — that is, to show only on the device that created it. So I can easily make this notification only show on my phone, and not on my watch, but that’s the opposite of what I want. to make it show on the watch, and not on the phone, I’ll have to write an Android Wear app, and create my local-only notification from there. I think.
Even More Conclusion
Writing this blog post was also a lot of fun. I’ve never written a technical blog post before. If you’ve read this far, thank you, and I hope you enjoyed it. There will be a follow-up to this post, hopefully sometime soon. In the meantime, I occasionally say things on Twitter and help organise the Dublin Android Developer Meetup.