Writing an autonomous Android app

20 Mar 2011

I have recently been working on a project for my PhD that involved installing devices in people's homes in an attempt to change behaviours around recycling and food waste. In a nutshell, I needed to unobtrusively and reliably capture the contents of a kitchen bin. It was decided that the best way to do this was to augment the bin with a 3g or wifi enabled mobile phone which would be able to to the capturing, processing and transmitting of the data in a single, small device. Given that I have experience writing Android apps, and the fact that there is a multitude of different Android devices on the market, the Android platform was clearly the best option available to me. The overall aim of the project is to run a study with multiple student households in geographically different environments over a period of six weeks. The phones are securely encased within the bins, which makes them difficult to physically access, and the problem is further complicated due to the fact I have recently moved abroad as part of my PhD and so I am not even in the same country as the devices. This post serves to document my experiences in creating an autonomous Android app that requires little to no user intervention.

Crash reporting

The first thing I considered when starting to write the app was "what's going to happen when the app crashes?" Note the "when", not "if" – if you think there is no chance that your app is going to crash at some point, you're almost entirely wrong. So before I think about how I'm going to recover from the crash, I'm going to want to see some details about it. This includes - at a minimum - a timestamp, a unique device identifier and the stack trace (so we know exactly where the error occurred and why). A friend of mine introduced me to ACRA, a fully comprehensive crash reporting tool. Of course it's possible to implement this yourself (see details in this thread) but ACRA generates crash data and uploads it directly to a Google Docs spreadsheet, giving a nice overview of all crash information. Additionally, it's possible to set up e-mail notification when the spreadsheet has been changed, so you get informed of a crash reasonably quickly. The documentation for ACRA is very helpful and allows you to add crash reporting to your application within minutes.

ACRA reports in Google Docs spreadsheet

Since I have four Android devices in my project, I need a way of identifying which one has crashed. ACRA supports adding custom fields to the report, letting me include the IMEI number of the device. I use the IMEI number throughout my code as a way of uniquely identifying the devices, using the following code.

public static String getIMEI(Context ctx) {
    TelephonyManager telephonyManager = (TelephonyManager)ctx.getSystemService(Context.TELEPHONY_SERVICE);
    return telephonyManager.getDeviceId();
}

We'll need the READ_PHONE_STATE permission to get the IMEI number, so add the following to your AndroidManifest.xml file.

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

Adding it to the ACRA report is as simple as the following:

String imei = Util.getIMEI(this);
ErrorReporter.getInstance().putCustomData("IMEI", imei);

ACRA reports back a lot of information about a crash. Sometimes this can be a little overwhelming when looking at the spreadsheet, so I suggest hiding some of the columns that are irrelevant for you. Since I know a lot about my target devices (it's not an app that's released in the wild), I can hide several fields that don't tell me anything I don't already know.

Crash recovery

Now we have a nice way of visualising the crash report data, we should think about how to recover from a crash. Now Android has different ways of dealing with unexpected events. The system might generate an ANR (Application not responding) if it receives no response to an input event within 5 seconds (reference) or it might just straight up crash if an exception is thrown that is unhandled. This manifests itself in the all-too-commonly-seen force close dialog.

Application not responding dialog and force close dialog

Since my app doesn't deal with user input at all, this is largely irrelevant. But what happens when my software generates an exception? I'm dealing a lot with bitmaps and network transmissions so there's plenty of scope for things to go wrong. This is where Thread.setDefaultUncaughtExceptionHandler() comes into play. Calling this method, and passing it your own UncaughtExceptionHandler allows you to get control in the case any unhandled exception occurs. This is a nice thing to have in place because we don't want the Android system showing a dialog saying the app is not responding and prompting the user to close it because the environment in which we're running the app is not user-oriented. Intervening in this process allows us to control what happens when an unhandled exception is thrown. The following code snippet shows how we can do this. I only have one core activity in my app that does all of the capturing and transmitting of the images, so I put this line in there.

Thread.setDefaultUncaughtExceptionHandler(new MyDefaultExceptionHandler(pintent));

That's all we need to handle those unhandled exceptions. (Note: I'm passing in a variable to the constructor, this will be explained later). Now we define our actual UncaughtExceptionHandler, like so:

public class MyDefaultExceptionHandler implements UncaughtExceptionHandler {
    private PendingIntent restartIntent;
    public MyDefaultExceptionHandler(PendingIntent pi) {
        this.restartIntent = pi;
    }
        
    public void uncaughtException(Thread arg0, Throwable arg1) {
        // Send the ACRA report
        ErrorReporter.getInstance().handleSilentException(arg1);

        // Restart the app using the PendingIntent
        AlarmManager mgr = (AlarmManager) restartContext.getSystemService(Context.ALARM_SERVICE);
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 2000, restartIntent);
        System.exit(2);
    }
}

I'll outline what the rest of the code does below - of course, it's entirely up to you what you do when you encounter an unhandled exception but my actions are as follows.

Send an ACRA report

I want to report this exception as I would any other exception the app generates, which means sending an ACRA report to my Google Docs spreadsheet. Assuming you've set up ACRA as defined in the documentation, all you need to do is send a silent report.

ErrorReporter.getInstance().handleSilentException(arg1);

Restart the app

I want to restart my application right away, so the phone can continue to do its job. It's no good handling unexpected crashes then doing nothing. To restart the app, we can do the following. In my main activity which is running constantly, I create a PendingIntent that will remain in existence even if the activity that created it no longer exists.

PendingIntent pintent = PendingIntent.getActivity(this.getBaseContext(), 0, new Intent(getIntent()), getIntent().getFlags());

This PendingIntent is passed to my UncaughtExceptionHandler above so it can be used with the AlarmManager to restart my app. In the overridden uncaughtException() method, we get an instance of the AlarmManager class, which allows us to schedule events for the future. As the above code shows, an event is scheduled 2000ms in the future (just to give a bit of a pause before we start the app again) and then the PendingIntent is passed to the alarm manager so it's given something to do (i.e. start the PendingIntent). Finally we exit so the process is killed.

It is probably a good idea to only restart the app a certain number of times within a given timeframe. If there is a fundamental - and therefore consistent - problem with your code, your app is going to crash, restart, crash, restart, etc. continually. A simple rule saying "if we've restarted n times in the past m minutes, don't restart the app, just close it and send a notification". Maybe the phone needs a reboot, which is covered below.

We also want to make sure the app starts up when the phone boots. This way, if any manual intervention has to occur, we can just turn the phone off, turn it back on again and put it back in its housing. Once the phone boots up, the app will start automatically. To do this, we need to create a BroadcastReceiver which will receive a notification when the phone boots up. When it receives this notification, we can tell it to start the app. We'll need to create a new class for the BroadcastReceiver.

public class BootReceiver extends BroadcastReceiver {
    public void onReceive(Context ctx, Intent arg1) {
        Intent i = new Intent(ctx, MyActivity.class);
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
        ctx.startActivity(i);
    }
}

Each time the phone successfully boots, it'll create an intent to start MyActivity (you should replace this with the activity you want to start).

We'll need to add the following to the AndroidManifest.xml file now to create the intent filter. Additionally, we'll require the RECEIVE_BOOT_COMPLETED permission.

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

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

Conserving battery

Depending on your situation, your phone might not be constantly connected to a mains power source, in which case you'll need to minimise your battery usage. I used a couple of techniques to keep the power consumption down.

Turning off the radio

The majority of the time my app was just sitting there waiting for an event to happen in which the accelerometer triggered an action. Only then would a picture be uploaded to the server. It makes sense, then, that the radio only needs to be enabled when the transmission is happening, and it should be turned off the rest of the time. The simplest way to do this is to enable airplane mode when we don't need data connectivity, and disable it when we do. Airplane mode will cut all radio activity and therefore decrease battery consumption. We can write a function that enables airplane mode when we pass true, and disable it when we pass false.

private void airplaneMode(boolean enable) {
    Settings.System.putInt(getContentResolver(), Settings.System.AIRPLANE_MODE_ON, enable ? 1 : 0);
    Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    intent.putExtra("state", enable);
    sendBroadcast(intent);
}

Dimming the screen

While this probably won't make much difference as the screen will turn off after a few seconds anyway, I kept the screen dimmed as much as possible while the app was running - no user intervention means nobody is going to be looking at the screen anyway.

public static void screenBrightness(Window window, float val) {
    WindowManager.LayoutParams lp = window.getAttributes();
    lp.screenBrightness = val;
    window.setAttributes(lp);
}

To set the screen brightness to its lowest value:

screenBrightness(getWindow(), 0.05f);

Status information console

I wanted a way to remotely view the status of each of my devices and provide me with a way to manage them to some degree. This is of course invaluable if physical access to the devices is infeasible. A web interface is the most obvious solution here, especially as Android makes it easy to interface with HTTP services. I drafted a list of information I'd like a quick overview of for the devices.

A very minimally designed remote console

I wrote a PHP script that handles the transmitting of the information - which are supplied via GET parameters - and updates a database. Again, I used the phones' IMEI numbers to identify each one. Each time the app starts, it sends the current timestamp to the server and each time it captures a photo, it uploads the photo and current timestamp, battery level, and available memory. The following code creates an HttpClient and executes a call to the PHP script in question.

HttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet("http://www.my-server.com/update.php?action=an_action&imei=xxx");
httpClient.execute(request);

Needless to say, we'll need the INTERNET permission in order to do this, so this needs adding to the AndroidManifest.xml file.

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

Time at which the app last started up

This information lets me know when the app was last started. This is useful to know as it lets me see when the last time the app crashed. Of course this information is available in the ACRA report but it's nice to have it on the console - which I visit more regularly.

Battery level

This is not necessarily a requirement but it's a nice thing to know. The devices are to be deployed into student houses (with a wall adapter so the phone has a continuous power source) but there's always the possibility that they'll get unplugged for whatever reason. So each time the phone uploads a photo, it'll also send the current battery level. If the battery level on any of the phones falls below a certain threshold, I get an e-mail notification and I can let the people in the household know that the phone has been unplugged. This the code you need to get the battery level.

public static double getBatteryLevel(Context ctx) {
    Intent batteryIntent = ctx.getApplicationContext().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    int rawlevel = batteryIntent.getIntExtra("level", -1);
    double scale = batteryIntent.getIntExtra("scale", -1);
    double level = -1;
    if (rawlevel &gt;= 0 &amp;&amp; scale &gt; 0) {
        level = rawlevel / scale;
    }
    return level;
}

You could do a nice visualisation to show battery level on your console, I just show the percentage. I should mention that this method will return a number between 0 and 1, so you'll need to multiply by 100 to get the percentage.

Last Activity

This is quite project specific, as my phones are taking pictures and uploading them - which provides the whole data source for the project - so I'd like to know if there's a lull in activity from any of the phones which might indicate something is wrong. To this end, each time an upload happens, I send the current timestamp to the server so I know how long ago the last activity was.

Available memory

As I'm dealing with bitmaps (and doing some resizing on the device), it's nice to know if the device is low on physical memory. Sending the amount of available memory on each image upload lets me see this. ACRA reports a the same data but only on a crash, this lets me see the memory availability on a more frequent basis.

Remote management console

Being able to see various bits of information about the status of each device is all well and good but it would be really nice if we could actually control the device remotely. Really the only feature I wanted from this was the ability to reboot a device. It would be handy also to be able to do this from within the app, if, for example, the app is restarting itself over and over again the phone could be rebooted to see if this solves the problem. Helpfully, the PowerManager class provides us with the handy reboot() method. Easy, right? Well yes and no. When I was exploring how to remotely reboot the device, I found this method and went about writing the code to do it. My strategy was simple: use the web console to provide a link to reboot the device which, upon clicking, updates a flag in the database for the phone. When the app on the device starts, it executes an HTTP GET request to a script on the server which, if I've clicked the "reboot" link, returns a given string (e.g. "reboot"). The phone gets the response from the server and if it equals "reboot", we reboot the device. Very simple but effective. It was only when I got to calling the PowerManager.reboot() method that I realised it's only available for API level 8 and higher (i.e. Android 2.2). The phones I chose for this project are Sony Ericsson XPeria X10 minis - chosen for their small physical size and having an on-board camera flash - which run Android 1.6. People have managed to put 2.1 onto the phones, but seemingly not 2.2, so I was really stumped. I am yet to come up with a method of rebooting the devices (short of rooting them, which is difficult since they're in a different country). This method, however, would work fine on 2.2 and above.

Further work

One feature I'm missing which would be extremely useful is the ability to do a remote update of the app. I've got all this infrastructure to inform me of problems and let me view device stats but no way of being able to really fix the problems. There's only one way I can think of to achieve this which involves a couple of issues for me: Android 2.2 and upwards support automatic updating of installed apps. However, this requires both devices running Android 2.2 (which mine do not) and also that the app is installed via the Android Market (which I wanted to avoid as it's not a publicly released app). The latter issue could be resolved by restricting the app to run on devices with specific IMEI numbers, and showing a message on other devices telling the user to uninstall the app as it will serve no purpose. This is a bit of a hack but it would solve the problem.

I suspect with a rooted device it would be possible to put the APK on a server and use the methods outlined above to check if an update is needed, download the APK and install it locally. Since my devices aren't rooted, I haven't looked into this but I expect if you can get shell access, you could do this.

Do a pilot study

It is best when embarking on a project like this to schedule a pilot study first. This is especially invaluable if, like me, your testing strategies aren't all that extensive. Running the software for a short period of time before the real study will help you iron out any problems you might experience, or add any additional features you think are required.

Conclusions

Android is a great platform for using in projects like this – the flexibility, ease of creating apps and feature-packed SDK really allow for software to be written that requires minimal, if any, user intervention. However, Android was not designed for such an environment and that sometimes gets in the way. The platform is intended to provide end users with a rich interactive experience – quite the opposite of what I wanted here. It's also created in such a way that attempts to protect the user from any unscrupulous activity from less-than-honest apps (that's a good thing!) which makes it more difficult when you're creating something that you know will only be deployed on one device and you'd like to get real system level access and do things that maybe wouldn't be looked upon too kindly if distributed to the entire Android population. Still, as I've outlined here, it's certainly not an impossibility.

If you're going to use Android devices for a similar project, I'd suggest using ones that can run 2.2 and upwards so you can remotely reboot it.

The pilot study for this project will begin in the coming week so I'll be able to see how well these ideas worked.

Comments