Chapter 5: Using an Android App to create a "Nap Detector" - Part 2

Quick Recap: This is the latest in a series to automatically control a "Do not disturb" light based on various different inputs. (See here for chapter one.) Our most recent evolution to the system is an Android app that attempts to detect that we're napping based on whether or not we've got an alarm set.

In our last chapter, we got started with a very simple app that will update our light when we press a button. Kind of cool, but not actually that useful. This week we're going to be updating our app so that it checks the status on a regular basis and updates the status indicator without manual intervention.

In order to do this, we're going to use Intents and Alarms. In my opinion, these are some of the more confusing concepts when getting started with android app development so I'll try to be thorough when going through the implementations.

Implementing our Check with Intents

In our last post, we implemented our check directly in the body of our click listener. While that works fine if it's just a button, it's not sufficient for us to schedule this on a repeating basis. For that we'll need to convert our update check to a BroadcastReceiver. A broadcast receiver is essentially a way for us to register a handle to some bit of code that we have that should get called by someone outside of our app. It could be some other app or it could be the Android OS itself. For right now, we just want to be able to call our update function by name but there are other types of broadcast receivers that allow our functions to be called in a number of different circumstances.

package com.example.myapplication


import android.app.AlarmManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley

val ENDPOINT = "http://192.168.1.2:5000"

class UpdateStatus : BroadcastReceiver() {
   override fun onReceive(context: Context, intent: Intent) {
       val alarmManager =
           context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
       val clockInfo = alarmManager.nextAlarmClock

       val url: String
       if (clockInfo == null) { // there's no alarm so we'll say not busy.
           url = "$ENDPOINT/off"
       } else {
           url = "$ENDPOINT/on"
       }
       val stringRequest = StringRequest(
           Request.Method.GET,
           url,
           Response.Listener<String> { response ->
               val toast = Toast.makeText(context, "Worked!", Toast.LENGTH_LONG)
               toast.show()
           },
           Response.ErrorListener {
               val toast = Toast.makeText(context, "Didn't Work: ${it.message}", Toast.LENGTH_LONG)
               toast.show()
           })

       Volley.newRequestQueue(context).add(stringRequest)
   }
}

This code is almost exactly identical to our code from the end of the previous post. The main difference is that this is now in the body of the onReceive method of our new UpdateStatus class. And we're no longer in an Activity class so instead of using this, we'll refer to the provided context.

Now that we have our BroadcastReceiver implemented, next we'll need to tell android about it by registering it in our AndroidManifest.xml. 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.myapplication">

   <uses-permission android:name="android.permission.INTERNET" />

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/AppTheme"
       android:usesCleartextTraffic="true">
       <activity android:name=".MainActivity" >
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
       <receiver android:name=".UpdateStatus"/>
   </application>

</manifest>

This is everything we need to receive broadcasts on Android. Now, we'll update our button to use our shiny new broadcast receiver where we were previously implementing the check directly in the on click handler. To do that we'll broadcast an intent. This is what our updated onclick handler looks like:

val updateStatusIntent: Intent = Intent(this, UpdateStatus::class.java)
this.sendBroadcast(updateStatusIntent)

Mostly these lines look like business as usual. The thing that stands out to me, though, is the UpdateStatus::class.java expression. You might have already understood that it was a reference to the UpdateStatus class we defined above. But let's break it down. The double colon operator is Kotlin's syntax for getting a reference to the class (read more here). But that gives us a Kotlin Class (or KClass). Unfortunately, the Intent constructor needs a good, old-fashioned Java class. So we reference the java field on the KClass and pass that into our Intent constructor. Not too bad!

So now we'll run the test again. If we have an alarm set and we click our button, our light should turn red. If we don't have any alarms, it should turn green.

Whoo! Now our button … does exactly the same thing as it did before... But it's using intents and broadcast receivers!

But we want this to happen repeatedly. For that, we'll use Alarms.

Repeating our check with Alarms

Now, instead of sending the broadcast ourselves we're going to ask Android to call the broadcast receiver for us at regular intervals. Here's what the updated button clicklistener looks like: 

val updateStatusIntent: Intent = Intent(context, UpdateStatus::class.java)
val alarmIntent =
   PendingIntent.getBroadcast(context, 0, updateStatusIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val alarmMgr =
   context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MINUTE_IN_MILLIS, alarmIntent)

Okay let's walk through this. The first line is exactly the same as it was before. We're just creating the intent that references our UpdateStatus broadcast receiver. Next we create a PendingIntent for our broadcast receiver. Pending intents are what represents the alarms ability to call our broadcast receiver via an intent at a later date (hence pending). The next line is us getting the AlarmManager. Finally, we use our AlarmManager and our new pending intent to set a repeating alarm which will call our update status broadcast receiver every minute.

A Note about battery usage and optimization:

It probably goes without saying that Android wants users to have a good battery life. As such, they limit the circumstances under which they will allow apps to run their alarms. The default behavior for setting alarms in android is now "inexact". So now, app developers who want specific behavior need to use new apis to get precise timing for their alarms. In our case, I went with a trivial approach and turned off battery optimization for my app in the Android settings. Obviously, for a real production app we'd need to be more careful but for right now we'll go the easy route.

Great! Now our button is triggering a cascade of update status checks every minute. The last thing that would be great to address in this post is that our app still requires us to hit the button once if our device restarts which is still one time two many in my opinion!

In the next section we'll see how we can use our new-found broadcastreceiver-fu to get these checks to start right away when we turn on our device.

Start our repeating check when our device starts up

We're going to use broadcast receivers to start our check when our device boots, so let's create another class which implements BroadcastReceiver and does the same thing as pressing our button did in the last version of the app:

class AutoStart : BroadcastReceiver() {
   override fun onReceive(context: Context?, intent: Intent?) {
       val updateStatusIntent: Intent = Intent(context, UpdateStatus::class.java)
       val alarmIntent =
           PendingIntent.getBroadcast(context, 0, updateStatusIntent, PendingIntent.FLAG_UPDATE_CURRENT)
       val alarmMgr =
           context?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
       alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MINUTE_IN_MILLIS, alarmIntent)
   }
}

Now, we’ll update AndroidManifest.xml to ask Android to call this broadcast receiver when the device starts up. Let's look at what that looks like:

<receiver android:name=".AutoStart">
   <intent-filter>
       <action android:name="android.intent.action.BOOT_COMPLETED" />
   </intent-filter>
</receiver>

You’ll add this below your previous receiver stanza. To explain what’s going on, with our previous receiver stanza we are just declaring that someone could call this broadcast by name. With this code, we’re indicating to Android a situation under which this BroadcastReceiver should be called. Namely on boot. As you might expect, there are other types of actions you can listen to, (A lot of others in fact), making this a very powerful feature.

In order to listen to this system event, you'll need to add an additional permission: 

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

And with that, we’ve let Android know about our new BroadcastReceiver and that we’d like it to be called whenever the device starts. Pretty nifty!

Conclusion

Now, we should have an app that:

  • Checks whether we have an upcoming alarm every minute

  • sets the status of the light accordingly 

  • starts automatically when our device turns on.

Okay so now we've extended our meeting status indicator light to support nap status indication as well. This will probably be the last in this series for a bit but there's always more that I'm changing about it so don't be surprised if I come back with a new enhancement before long. Thanks for reading!

References: