Simple Hardware Hacking: Auto "On Air" VC Indicator - Chapter 4

This post is part of a series on building an automatic busy/not busy light for your room/office/desk. If this is the first post in this series you're landing on, I recommend starting with Chapter 1. We'll be building on the groundwork that those posts laid.

As a quick summary, up until now, we've created a Chrome extension which listens for instances of Google Hangouts call windows and turns a light red or green according to whether or not I'm actively on a call. 

This is useful by itself but me being on a video call is obviously not the only reason I might not want someone to barge into my room. I'm a big fan of naps and this could be the perfect thing to avoid people unintentionally waking me up.

You might be thinking, how in the world are we supposed to detect if we're sleeping by using an android app? This might be pretty specific to me, but I almost never take a nap without setting an alarm so there being an active alarm clock is a solid approximation of whether or not I'm napping. So our goal is to create an app that updates the sign to indicate busy whenever there is an upcoming alarm clock set.

To give a quick overview, the things we'll cover in this post are:

  • Getting a simple, single activity android app up and running

  • Reviewing the pertinent parts of the generated code

  • Adding Dependencies to an android project

  • Adding a button with some functionality

  • Making Network requests

  • Requesting permissions in our AndroidManifest file

Listen for Alarms Being Set?

If we take our Chrome extension as an example, it would be useful to have a listener for changes to the alarm clock. In my searches, such a thing doesn't seem to be possible. So we'll have to come up with another way.

Whenever it's not possible to use a push model (i.e. the system calls our callback when the time is right) you need to resort to a pull model. That is we'll need to set up a repeating task to check the current state of the alarm clock and update the status accordingly. Before we get into the nitty gritty of getting something to repeat regularly let's just take a look at what doing this once will look like.

Quick Tour of the Empty Activity Starter App

Let's start from the start with one of the shell android app options. To keep things simple, I suggest using Android Studio. Create a new Android Studio project by selecting "File" > "New" > "New Project …". The first page prompts you to pick a starter template. I recommend going with Empty Activity. That option gives a good balance of including just the stuff we need and nothing that we don't. On the second page of the New Project Wizard, be sure to select a minimum SDK version of at least 23. Then you should be able to click finish to create your new project.

If you're new to Android development, there's kind of an astonishing number of files and directories that it generates. In previous posts, I've tried to include all of the code in the post itself. Unfortunately, the amount of stuff auto-generated in the basic android app makes that undesirable. I'll be highlighting all the interesting bits of the implementation here, but rest assured, you can check out the full content of the app in the repository on GitHub.

Here are the files that are relevant to us:

java/com/example/myapplication/MainActivity.kt

package com.example.myapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

This is the main entrypoint for this app. Right now, all this code is doing is saying "Use this layout file to render my app". Which brings us to:

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Hello World!"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

This file defines a layout. Specifically, it declares a "Constraint Layout" and has a TextView which displays the text "Hello World!".

manifests/AndroidManifest.xml

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

   <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">
       <activity android:name=".MainActivity">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

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

</manifest>

Finally, we have the Android Manifest. This is basically just a bunch of information about your app. This is where things like what permissions your app uses, what icon to use in the launcher etc are stored. In our case, the most pertinent thing to note is that this is where we're declaring that MainActivity is the one that should be run when the user taps on the icon in the launcher for this app.

Even though we haven't done anything yet, let's build the app and take a look at what this gets us:

Figure 1: What a vanilla Empty Activity project looks like on a tablet

Figure 1: What a vanilla Empty Activity project looks like on a tablet

Adding A Button

As described in the section above, we want to add a button to our activity which will check if there are any upcoming alarms and call our REST API accordingly. Let's update our manifest file to have a button that says "SCAN" instead of the text that says HelloWorld:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/scanButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="SCAN"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

You should be able to rebuild and see your button in place of the "Hello World!" in the original version:

Figure 2: Our new and improved SCAN button app.

Figure 2: Our new and improved SCAN button app.

Defining the Click Listener

And now we'll add a click listener for that button to do the checking. In our click listener, we'll be making an HTTP call to our REST server. To do that we'll have to do a bit of setup. Namely:

  • Add a dependency to Volley (which is a utility for making http requests on Android)

  • Add a declaration to our Android Manifest that we need access to the internet

Add the Volley Dependency

In your project view, below the implementation of the app are the "Gradle Scripts". In that folder, you'll find two build.gradle files. One is for the overall project and the other is specifically for your app. That may be slightly confusing given the current state of our project because our project IS the app right now, but that would be useful in the case that you had more than one app in a single Android Studio project.

But I digress, the main point is just to make sure you're modifying the right build.gradle file. The easiest way I use is to check in the project outline on the left side. The one we'll modify is the one labelled "(Module: app)"

Once you've opened the right file, you should find a section labelled dependencies at the bottom:

dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
   implementation 'androidx.appcompat:appcompat:1.1.0'
   implementation 'androidx.core:core-ktx:1.2.0'
   implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
   implementation 'com.android.volley:volley:1.1.1'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test.ext:junit:1.1.1'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Add the highlighted line to your dependencies section.

Add the INTERNET permission to AndroidManifest.xml


Open your AndroidManifest.xml and add the following line:

<?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
   OTHER STUFF HERE ... 
   </application>

</manifest>

Now we can implement the click listener for our button:

package com.example.myapplication

import android.app.AlarmManager
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
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.6:5000";

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       findViewById<Button>(R.id.scanButton).setOnClickListener {
           val alarmManager =
               this.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(applicationContext, "Worked!", Toast.LENGTH_LONG)
                toast.show()
                },
                Response.ErrorListener {
                  val toast = Toast.makeText(applicationContext, "Didn't Work: ${it.message}", Toast.LENGTH_LONG)
                  toast.show()
                })


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

To walk through this, after we set our content view to be our activity_main layout. We grab our button by id (this is the id declared in the layout file) and set an onclick listener.

In the onclick listener, we get the AlarmManager using the getSystemService utility, and we get the alarm clock info from the AlarmManager.

Now we have the information we were looking for. If nextAlarmClock returns null, that means there's not currently an alarm set, otherwise, there is. We use that information to construct which url we will be making our request to. Note that we declare our endpoint at the top of the file. This will be the local address of your Flask Server. The same one that your Chrome Extension should be configured to talk to (if you've followed along with that blog post).

Next, we construct our StringRequest Object and finally we make our request by constructing a one-off queue and adding our StringRequest to it.

When building our StringRequest, we provide two callbacks. One that should run if the request succeeded and one if it failed. In either case, we create a toast to make it easy to see whether or not our request was successful.

Okay Great! So now we have our button wired up so that it will actually do something.

So go ahead and run your app and try clicking on your button. 

Most likely, you saw a toast that said. "Didn't work: java.io.IOException: Cleartext HTTP traffic to 192.168.1.6 not permitted"

It's complaining because, by default, Android doesn't allow your app to communicate over plain HTTP. Generally, it's super important to make all calls over HTTPS to make sure that third parties can't access your data. In this case though, we're sending non-sensitive data over our local network so we'll lift this restriction to our app. To do that, we'll need to go back to our AndroidManifest.xml and add an attribute to our application tag:

<?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>
   </application>

</manifest>

 Now, if we go back to our app, (assuming that our python server is listening on the provided endpoint) we should see that our request has now gone through successfully.

Conclusion

As they say, the first step's a doozy! To recap what we've accomplished, we have an android app that observes the state of the device that it's on and makes a call to our REST endpoint accordingly. The main shortcoming of our current version is that we need to push the button every time we want it to check which definitely limits its usefulness.

In our next post, we'll dig into Android Alarms (not to be confused with AlarmClock) which will allow our app to repeat this check continually without us needing to push any buttons.

See you then!