Networked Android Sensor Data

Posted on

Background

SensorEvents are the fundamental objects in the Android Sensor API for transmitting sensor data from the IMU to your application, whether they be accelerometer measurements (TYPE_ACCELEROMETER), gyroscope measurements (TYPE_GYROSCOPE), or some other sensor measurements. However, if you plan to use SensorEvents in a networked application, you will find that SensorEvents quickly turn out to be a pain to work with. That is because, under the auspices of privacy, Google has tried its best to deter you from using SensorEvents in networked applications.

 

Understanding why is easy… SensorEvents can be used to track your location, your daily habits, and your health (Android 20+ exposes heart rate measurements). In other words, SensorEvents often contain exactly the type of information that you do not want exposed on the network. Nonetheless, there are certainly legitimate uses for sensor data in networked applications, such as multiplayer games that use player orientations to infer whether they are looking at the same object, or baby monitoring apps that use the accelerometer to determine if a baby is wiggling in bed. In these cases, there is a clear benefit to allowing sensor data to be shared in over the network.

 

So how does the SensorEvent class protect itself from being networked? Essentially, Google has taken a 3-step approach:

  1. The SensorEvent class does not implement Serializable, meaning we would need to use another method such as JSONs to transmit events.
  2. The SensorEvent constructor is protected, meaning that even if we could transmit the data across a network, we could never reinstantiate it on the receiver side.
  3. The SensorEvent class is protected so that we cannot extend it, and then use one of the methods above.

 

This means that the only way we are going to get sensor data across a network is by transmitting the sensor data piece by piece, or by bottling up the data in a new class (unless you want to go the JNI route, in which case you can circumvent all of the protected keywords).

 

Device ID

One of the first problems that arises when dealing with sensor data on the network is that you need to identify which device produced which piece of data. There are many identifiers we could chose, but we need one that is static (so forget IP addresses) and that will work on a wide range of devices (tablets might not have a SIM card, so anything SIM-related is out). One identifier that satisfies these requirements is the Secure.ANDROID_ID, which is initialized the first time an Android device is booted, is unique to each user, cannot be turned on or off, and will only change when a factory reset is performed. Furthermore, to get the ID, we simply need to have a valid Context (typically the calling Activity) with which we can call

String id = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);

Actually, so that we don’t need to keep remembering how to retrieve this ID, we are going to make a class that saves the ID statically after being initialized with a valid Context:

public class Device {

    /**
     * The ID of the current device. This value is null until you call
     * {@link Device#init(Context)} with a valid {@link Context}.
     */
    private static String id = null;

    /**
     * Returns the {@link Device#id} field, which should be non-null after you
     * call {@link Device#init(Context)}.
     */
    public static String ID() {
        return id;
    }

    /**
     * Initializes and returns the ID of the current device by grabbing the
     * ANDROID_ID from the {@link ContentResolver} of the given {@link Context}.
     */
    public static String init(Context context) {
        if (context != null) {
            id = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
        }
        return id;
    }
}

To use this class, you would ideally place the call Device.init(this) toward the top of the onCreate method of the first activity that starts in your app. After that, whenever you need a unique device identifier, you would simply call Device.ID() from anywhere in your app.

 

A Simple SensorEvent class

Since we can’t easily work with SensorEvents on the network, we need a new class to replace it. This new class should be lightweight and contain only the core SensorEvent data because:

  1. We expect to be sending a lot of sensor data.
  2. A lot of the data inside a SensorEvent is never used.
  3. Even if we could save all of the data in a SensorEvent in our new class, we won’t be able to reinstantiate a SensorEvent on the receiver side anyway. So there really is no use.

That being said, for about 99.9% of applications, there are really only 4 things we need from a SensorEvent:

  1. The type of sensor (e.g. TYPE_ACCELEROMETER) that generated the event.
  2. The event timestamp.
  3. The event values.
  4. The ID of the device that generated the event.

A SimpleEvent is, as the name implies, a stripped-down version of the SensorEvent class containing only this information. That means that you won’t be able to reinstantiate a SensorEvent from a SimpleEvent. However, in return, SimpleEvents are Serializable, easily instantiable from SensorEvents, and lightweight enough for high-volume network transmission. So if you are in the market for something to replace SensorEvents in your networked application, you might want to try something like this:

public class SimpleEvent implements Serializable {

    /**
     * The auto-generated {@link UUID} associated with this class, used by the
     * {@link Serializable} interface.
     */
    private static final long   serialVersionUID        = -2844988702907222858L;

    /**
     * ---------------------------------------------
     *
     * Public Fields
     *
     * ---------------------------------------------
     */
    /**
     * The type of {@link SimpleEvent}. This could be one of Android's built-in
     * types, such as {@link Sensor#TYPE_ACCELEROMETER}, or a user-defined type.
     */
    public int                  type;
    public static final int     typeDefault             = 0;

    /** See {@link SensorEvent#timestamp}. */
    public long                 time;
    public static final long    timeDefault             = 0;

    /** See {@link SensorEvent#values}. */
    public float[]              data;
    public static final float[] dataDefault             = null;

    /** The ID of the device that generated this {@link SimpleEvent}. */
    public String               device                  = null;

    /**
     * ---------------------------------------------
     *
     * Constructors
     *
     * ---------------------------------------------
     */
    /**
     * Create an empty {@link SimpleEvent} on the current device, as defined by
     * {@link Device#ID}.
     */
    public SimpleEvent() {
        this(typeDefault, timeDefault, dataDefault, Device.ID());
    }

    /**
     * Create an empty {@link SimpleEvent} with the specified device ID (the
     * device which generated the event).
     */
    public SimpleEvent(String device) {
        this(typeDefault, timeDefault, dataDefault, device);
    }

    /**
     * Create an empty {@link SimpleEvent} of the specified type on the current
     * device, as defined by {@link Device#ID}.
     */
    public SimpleEvent(int type) {
        this(type, timeDefault, dataDefault, Device.ID());
    }

    /**
     * Create an empty {@link SimpleEvent} of the specified type on the
     * specified device.
     */
    public SimpleEvent(int type, String device) {
        this(type, timeDefault, dataDefault, device);
    }

    /**
     * Create a {@link SimpleEvent} from the specified arguments on the current
     * device, as defined by {@link Device#ID}.
     */
    public SimpleEvent(int type, long time, float[] data) {
        this(type, time, data, Device.ID());
    }

    /** Create a {@link SimpleEvent} from the specified arguments. */
    public SimpleEvent(int type, long time, float[] data, String device) {
        this.type = type;
        this.time = time;
        this.data = data;
        this.device = device;
    }

    /**
     * Create a {@link SimpleEvent} from the {@link SensorEvent}, and assume
     * that this device created the event, where the current device is defined
     * by {@link Device#ID}. Note that since a {@link SimpleEvent} is a stripped-down 
     * version of a {@link SensorEvent}, you will not be able to reconstruct the
     * {@link SensorEvent} from the new {@link SimpleEvent}.
     */
    public SimpleEvent(SensorEvent event) {
        this(event.sensor.getType(), event.timestamp, event.values, Device.ID());
    }

    /**
     * ---------------------------------------------
     *
     * Setters
     *
     * ---------------------------------------------
     */
    /**
     * Copy the fields from the specified {@link SensorEvent}, assuming that the
     * {@link SensorEvent} occurred on the current device.
     */
    public SimpleEvent copyFrom(SensorEvent event) {
        type = event.sensor.getType();
        time = event.timestamp;
        data = event.values;
        device = Device.ID();
        return this;
    }

    /** Reset all of the fields to their default values. */
    public SimpleEvent reset() {
        type = typeDefault;
        time = timeDefault;
        data = dataDefault;
        device = Device.ID();
        return this;
    }

    /**
     * ---------------------------------------------
     *
     * Getters
     *
     * ---------------------------------------------
     */
    /** Indicates whether this {@link SimpleEvent} occurred on this device. **/
    public boolean onThisDevice() {
        return device == null || device.equals(Device.ID());
    }
}

This class is not the end-all in terms of what you might need for your application, but I would recommend getting used to using something like this SimpleEvent class in your applications. If you have even the slightest inkling that you might be dealing with sensor data from the network in the near future, then doing so will certainly ease the transition when that time comes.