Building Hoot showed Christina and I the ugly side of Android fragmentation. Hoot is currently running on over 1600 different device models on five different versions of the Android OS (>= 4.0), and it makes extensive use of the hardware: multiple cameras, the light sensor, and the orientation sensor. Unsurprisingly, given the number of different configurations, we’ve seen some weird behavior: crashes, incorrectly-rotated videos, and even videos recorded in sixplicate!

In the beginning, our patient friends handed over their phones for debugging; we’d plug the phones into our laptops and either step through Eclipse’s debugger or print useful information to logcat, Android’s logger. Though it was helpful, it didn’t scale: our friends collectively had fewer than 20 of the 1600+ models we eventually saw.

We looked at services that read, save, and send logs from phones remotely, but they required more effort from end users than we wanted to put our friends through – let alone users we didn’t know. As of Android Jelly Bean, reading from logcat requires root access, which is a permission we didn’t want to require in the public app either.

Instead, we built PhoneHome. PhoneHome retrieves Hoot’s logs from mischievous devices without asking anything from the user. We define the logs we want by specifying device model, OS version, app version, and username on the backend. If a user matches a criteria set, Hoot sends its logs to us, so we can see exactly what happened when the devices misbehaved.

Here’s how it works:

Setup

Configure PhoneHome in the onCreate() of your main activity (the one associated with the android.intent.action.MAIN intent). PhoneHome must be configured initially, but it can always be changed or turned off later.

The most important configuration options are enable() and logSink(). enable() toggles log flushing to the backend, and logSink() specifies how the logs are flushed. When a batch of logs is ready to be flushed, it’s passed to the flushLogs() method of the PhoneHomeSink object specified in logSink().

It’s important to differentiate between logging to logcat (normal logging behavior) and flushing a batch of logs. If a log event goes through PhoneHome and meets the parameters specified in debugLogLevel() and productionLogLevel(), it’s immediately logged to logcat locally and is added to the current log queue that’ll be sent to your PhoneHomeSink, where it is later flushed to your sink.

Here’s an example configuration:

PhoneHomeConfig.getInstance()
    // set .enabled(true) to enable log flushing
    .enabled(false)
    // wait until we have this many events to flush a batch of logs...
    .batchSize(100)
    // ... or until this many seconds have passed since our last flush
    .flushIntervalSeconds(1800)
    // when developing, log all messages to logcat
    // (everything is flushed to our sink)
    .debugLogLevel(android.util.Log.VERBOSE)
    // in production, only log INFO messages and above to logcat
    // (everything is flushed to our sink)
    .productionLogLevel(android.util.Log.INFO)
    // specify the sink that receives flushed logs
    // (required if you ever enable log flushing!)
    .logSink(new PhoneHomeSink() {
        public void flushLogs(final List<PhoneHomeLogEvent> logEvents) {
            // flush the log events to your backend...
        }
    });

Collection

Using PhoneHome’s logger instead of Android’s system logger is easy.

First, here’s a typical pattern that uses Android’s system logger:

public class MyClass {
    private static final TAG = "MyClass";

    MyClass() {
        Log.d(TAG, "Debug!");
        Log.i(TAG, "Info!");
        Log.e(TAG, "Oh dear.");
    }
}

PhoneHome’s logger works similarly. Construct a PhoneHomeLogger instance for each TAG. (Now, you don’t have to type the first parameter over and over.) Then, use the PhoneHomeLogger instance as you would Android’s system logger:

public class MyClass {
    private static final PhoneHomeLogger Log = PhoneHomeLogger.forClass(MyClass.class);

    MyClass() {
        Log.d("Debug!");
        Log.i("Info!");
        Log.e("Oh dear.");
    }
}

Eligibility

To avoid collecting unnecessary logs (and save your users’ battery and data plans!), we recommend checking if a user matches a particular criteria set before flushing logs. We’ve included an example of this in the sample app and backend in the GitHub repository. Once you’ve determined whether a user should phone logs home, enabling, disabling, or re-enabling PhoneHome is as easy as:

boolean isEligible =...; // determine eligibility

PhoneHomeConfig.getInstance().enabled(isEligible);

Shipment

When a batch of logs is ready, it’s passed to your PhoneHomeSink object. From there, you choose what to do, though typically, we think you’ll want to send it to your backend with a network request. Since there isn’t a standard Android networking library and backend APIs are different, you’ll want to work it into your existing patterns for network requests and API calls. Here’s an example of how you might do with this with the AndroidHttpClient

We recommend specifying the device configuration associated with log events to simplify deduping. One strategy is sending along the Android device model, SDK version, app versionCode, username, and/or other identifying information with the log events. After all, logs from a misbehaving device aren’t very helpful if you can’t tell from which device they came!

Our example app and backend show one way to send device information with each request.

Display

We built a barebones, Bootstrap’d web dashboard to display logs we collected, but you can just as easily look at the lines, by user and device, in your database. In the sample backend application, the logs are stored in the logcat_events table.

Similarly, you could set user-log configurations using our simple web form or by editing the database directly.

Phone home!

We’ve open-sourced PhoneHome’s Android pieces, providing hooks for you to turn on/off logging and log sending. We’re not providing a backend library, but we are including a sample backend implementation that might help as you add PhoneHome to your Android app.

Happy logcatting! Please let us know what you think!