Android Device Nudge Detection Helper Class

I recently added a feature to StarCraft 2 Build Player to start playing build orders when the users’ phone is nudged. The idea is so you don’t have to waste precious seconds looking down at your phone to tap the “Play” button, instead you can just mindlessly bump your phone on your desk and you’re off.

Anyway, it turned out to be pretty easy to factor this into a reusable class so here it is:

[sourcecode language=”java”]
package com.kiwiandroiddev.sc2buildassistant;

import java.util.ArrayList;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;

/**
* Class for reporting when the device’s acceleration (excluding gravity) exceeds
* a certain value. Compatible with all Android versions as it uses Sensor.TYPE_ACCELEROMETER
* rather than Sensor.TYPE_LINEAR_ACCELERATION.
*
* NudgeDetector objects are initially disabled. To use, implement
* the NudgeDetectorEventListener interface in your class, then register it
* to a new NudgeDetector object with registerListener(). Finally, call
* setEnabled(true) to start detecting device movement. You should add a call
* to stopDetection() in your Activity’s onPause() method to conserve battery
* life.
*
* @author kiwiandroiddev
*
*/
public class NudgeDetector implements SensorEventListener {

private ArrayList<NudgeDetectorEventListener> mListeners;
private Context mContext;
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private boolean mEnabled = false;
private boolean mCurrentlyDetecting = false;
private boolean mCurrentlyChecking = false;
private int mGraceTime = 1000; // milliseconds
private int mSampleRate = SensorManager.SENSOR_DELAY_GAME;
private double mDetectionThreshold = 0.5f; // ms^-2
private float[] mGravity = new float[] { 0.0f, 0.0f, 0.0f };
private float[] mLinearAcceleration = new float[] { 0.0f, 0.0f, 0.0f };

/**
* Client activities should implement this interface and register themselves using
* registerListener() to be alerted when a nudge has been detected
*/
public interface NudgeDetectorEventListener {
public void onNudgeDetected();
}

public NudgeDetector(Context context) {
mContext = context;
mListeners = new ArrayList<NudgeDetectorEventListener>();
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}

// Accessors follow

public void registerListener(NudgeDetectorEventListener newListener) {
mListeners.add(newListener);
}

public void removeListeners() {
mListeners.clear();
}

public void setEnabled(boolean enabled) {
if (!mEnabled && enabled) {
startDetection();
} else if (mEnabled && !enabled) {
stopDetection();
}
mEnabled = enabled;
}

public boolean isEnabled() {
return mEnabled;
}

/**
* Returns whether this detector is currently registered with the sensor manager
* and is receiving accelerometer readings from the device.
*/
public boolean isCurrentlyDetecting() {
return mCurrentlyDetecting;
}

/**
* Sets the the amount of acceleration needed to trigger a "nudge".
* Units are metres per second per second (ms^-2)
*/
public void setDetectionThreshold(double threshold) {
mDetectionThreshold = threshold;
}

public double getDetectionThreshold() {
return mDetectionThreshold;
}

/**
* Sets the minimum amount of time between when startDetection() is called
* and nudges are actually detected. This should be non-zero to avoid
* false positives straight after enabling detection (e.g. at least 500ms)
*
* @param milliseconds_delay
*/
public void setGraceTime(int milliseconds_delay) {
mGraceTime = milliseconds_delay;
}

public int getGraceTime() {
return mGraceTime;
}

/**
* Sets how often accelerometer readings are received. Affects the accuracy of
* nudge detection. A new sample rate won’t take effect until stopDetection()
* then startDetection() is called.
*
* @param rate must be one of SensorManager.SENSOR_DELAY_UI,
* SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_GAME,
* SensorManager.SENSOR_DELAY_FASTEST
*/
public void setSampleRate(int rate) {
mSampleRate = rate;
}

public int getSampleRate() {
return mSampleRate;
}

/**
* Starts listening for device movement
* after an initial delay specified by grace time attribute –
* change this using setGraceTime().
* Client Activities might want to call this in their onResume() method.
*
* The actual sensor code uses a moving average to remove the
* gravity component from acceleration. This is why readings
* are collected and not checked during the grace time
*/
public void startDetection() {
if (mEnabled && !mCurrentlyDetecting) {
mCurrentlyDetecting = true;
mSensorManager.registerListener(this, mAccelerometer, mSampleRate);

Handler myHandler = new Handler();
myHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mEnabled && mCurrentlyDetecting) {
mCurrentlyChecking = true;
}
}
}, mGraceTime);
}
}

/**
* Deregisters accelerometer sensor from the sensor manager.
* Does nothing if nudge detector is currently disabled.
* Client Activities should call this in their onPause() method.
*/
public void stopDetection() {
if (mEnabled && mCurrentlyDetecting) {
mSensorManager.unregisterListener(this);
mCurrentlyDetecting = false;
mCurrentlyChecking = false;
}
}

// SensorEventListener callbacks follow

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}

@Override
public void onSensorChanged(SensorEvent event) {
// alpha is calculated as t / (t + dT)
// with t, the low-pass filter’s time-constant
// and dT, the event delivery rate

final float alpha = 0.8f;

mGravity[0] = alpha * mGravity[0] + (1 – alpha) * event.values[0];
mGravity[1] = alpha * mGravity[1] + (1 – alpha) * event.values[1];
mGravity[2] = alpha * mGravity[2] + (1 – alpha) * event.values[2];

mLinearAcceleration[0] = event.values[0] – mGravity[0];
mLinearAcceleration[1] = event.values[1] – mGravity[1];
mLinearAcceleration[2] = event.values[2] – mGravity[2];

// find length of linear acceleration vector
double scalarAcceleration = mLinearAcceleration[0] * mLinearAcceleration[0]
+ mLinearAcceleration[1] * mLinearAcceleration[1]
+ mLinearAcceleration[2] * mLinearAcceleration[2];
scalarAcceleration = Math.sqrt(scalarAcceleration);

if (mCurrentlyChecking && scalarAcceleration >= mDetectionThreshold) {
for (NudgeDetectorEventListener listener : mListeners)
listener.onNudgeDetected();
}
}
}

[/sourcecode]

The reason I stuck to using Sensor.TYPE_ACCELEROMETER was because I want to support Froyo with my app. If you’re only targeting 2.3 (API level 9) and higher, you could use Sensor.TYPE_LINEAR_ACCELERATION, and simplify this code a fair bit by stripping out the gravity calculation in onSensorChanged(), etc.

Feel free to use this in your projects. Drop me a comment if you spot bugs or have any suggestions.

Data on Android device supported features

I’ve recently been experimenting with OpenGL ES 2.0 on Android for a graphical app (some excellent guides can be found at http://www.learnopengles.com/). So far so good. It turns out that gone are the days of countless fixed function calls like glBegin() glVertex3f() glColor4f() for sending vertex data, nowadays you use shaders for everything and send your vertex data to OpenGL in large chunks.  Supposedly this makes the graphics driver software a lot simpler to write and leads to better performance overall. Keeping track of all of those calls and their corresponding closing calls could end up a bit of a headache so it seems like it provides some benefit to application developers too.

Before diving in and using ES 2.0 exclusively (well, at first anyway – code for ES 1.x support can always be added later) I wanted to get an idea of how widely ES 2.0 is supported across Android devices because it could have a big effect on the market size for my app.

After filtering through some anecdotal evidence on Stackoverflow, not surprisingly the best place to find this data was straight from the horse’s mouth at the Android Dashboards page.

According to the data, ES 2.0 support is over 90% and it seems reasonable to assume it’s only going to increase in time. So that settles it – OpenGL ES 2.0 it is.

The Dashboards page also has data on the installation base for each Android version which may also be very useful to you during the research phase of developing your app.