Exploring Device Sensors with Kotlin

http://justmobiledev.com/wp-content/uploads/2019/02/altenergy1.jpghttp://justmobiledev.com/wp-content/uploads/2019/02/altenergy1.jpghttp://justmobiledev.com/wp-content/uploads/2019/02/altenergy1.jpgExploring Device Sensors with Kotlin

I recently read an article on Android Authority about device sensors which inspired me to write this post on exploring device sensors with Kotlin.

However, in this post I wanted to focus more on how to collect sensor data asynchronously and how to update the UI efficiently without slowdowns. Also, I wanted to implement the samples in Kotlin instead of Java.

So here we go: As you know today’s smart phones are packed with device sensors for all kinds of purposes. The Android documentation lists the following main categories:

Sensor Types

Motion sensors

Motion sensors measure acceleration- and rotational forces along three axes. Some examples of sensors in this category are accelerometers, gravity sensors, gyroscopes, and rotational vector sensors.

An example of an app that uses these types of sensors is a fitness tracker app that keeps track of the steps taken throughout the day or your general activity level.

Environmental sensors

These sensors can measure characteristics of the device environment, such as ambient air temperature and pressure, illumination, and humidity. This category includes barometers, photometers, and thermometers.

An example of an app that uses these sensors may be a hiking app that displays temperature, and air pressure information.

Position sensors

Position sensors can measure the physical position of the device in terms of orientation or device heading. This category includes orientation sensors and magnetometers.

An app that could make use of these types of sensors could be a hiking app that displays the device heading and a compass.

For the purpose of this tutorial we will be implementing a light sensor, a temperature sensor, and a gyroscope sensor.

So, let’s jump right into how to interface with the device sensors using the Android framework.

The SensorManager Class

In order to interface with the different sensor types and collect data, the Android Framework provides the SensorManager class. It’s the main class that provides access to the Sensor classes such as
Accelerometers or Barometer.

On a high level, here are the steps to interface with a sensor in Kotlin:

1.First, we get a reference to the SensorManager:

1
SensorManager mySensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager;

2. To determine if the sensor we are trying to use is available on the device model the app is running on, we need to try to get the sensor reference and check if it is null:

1
2
3
4
5
6
var tempSensor = mySensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE);
if (tempSensor != null) {
   // The sensor exists
} else {
   // The sensor does not exist
}

3. Registering a listener to receive sensor data:

1
mySensorManager.registerListener(this, mSensors, SensorManager.SENSOR_DELAY_NORMAL)

4. Unregistering the sensor:

1
 mySensorManager.unregisterListener(this)

5. Implementing the SensorEventListener Interface to collect data

1
2
3
4
5
6
class MySensorCollectorClass: SensorEventListener {
    ...
    override fun onSensorChanged(p0: SensorEvent?) {
       // Process the sensor changed value
    }
}

6. Getting a list of all available sensors

1
2
3
4
val deviceSensors: List<Sensor> = mySensorManager.getSensorList(Sensor.TYPE_ALL)
Log.v("Total sensors",""+deviceSensors.size)
deviceSensors.forEach{
Log.v("Sensor name",""+it)

Collecting Data

Before implementing the data collection from the sensor, one should consider how long the collection should last, from which components in the app the data could be accessible, and where the data should be displayed.

For example, if you just want to display a one-time ambient light reading on a page to the user, you can implement the sensor collection in your controller.

However, if you want to collect sensor data throughout the app life cycle and keep the data accessible, you may want to use something like a service that collect data on a worker thread.

If you decide to use a service, there are two main things to consider: first, as you know services run on the thread that they are started on, so data collection on a separate thread will need to be implemented differently.

Then there is the choice of an appropriate service type. Android provides three main services: Foreground, Background and Bound Service. The use of Background services are discouraged and it’s features are restricted since API 26. A Foreground Service requires you to display a notification to the user while the service is running. To interface with the service from an Activity, you could also use a Bound Service. The choice really depends on the specific requirements for collecting and displaying the sensor data in your app.

For the purpose of this tutorial, I decided to just use a Singleton pattern.

To collect data on a background thread, I’m using a HandlerThread, which spins off a separate worker thread. To let the SensorManager know to use the HandlerThread, I’m getting the HandlerThread looper with a handler and passing the handler into the sensorManager.registerListener() method.

This article on MindDorks explains the relationship between the classes in more detail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 fun startSensor(){
        sensorThread = HandlerThread(TAG, Thread.NORM_PRIORITY)
        sensorThread!!.start()
        sensorHandler = Handler(sensorThread!!.getLooper()) //Blocks until looper is prepared, which is fairly quick
        sensorManager!!.registerListener(this,
            sensor, SensorManager.SENSOR_DELAY_NORMAL,
            sensorHandler
        )
    }

    fun stopSensor(){
        sensorManager!!.unregisterListener(this)
        sensorThread!!.quitSafely()
    }

Processing UI Updates

Depending on the type of sensor, the sensor events may flood in at a high speed so processing them may slow down the UI.

A better approach would be to process sensor events asynchronously and then pass them to the UI for display, e.g. via a handler.

In your Activity, you could define a handler, that processes sensor messages and display them:

1
2
3
4
5
6
7
8
9
10
11
  val handler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(inputMessage: Message) {
            // Gets the image task from the incoming Message object.
            val sensorEvent = inputMessage.obj as MySensorEvent

            // Light Sensor events
            if (sensorEvent.type == SensorType.LIGHT){
                tvLightSensorValue.text = sensorEvent.value
            }
        }
    }

Next, you pass your handler to the entity that implements the data collection or make it accessible for it to call. In my implementation, I’m just setting the handler on my Singleton instance.

1
LightSensorManager.setHandler(handler)

On sensor, updates, I’m performing the sensor event data processing and sending a handler message back to the Activity for display:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    override fun onSensorChanged(event: SensorEvent?) {
        if (event != null && event.values.isNotEmpty()) {

            var msgEvent = MySensorEvent()
            msgEvent.type = SensorType.GYRO

            // Format event values
            msgEvent.value = "x: "+event.values[0]+"\ny: "+event.values[1]+"\nz: "+event.values[2]

            // Send message to MainActivity
            sendMessage(msgEvent)
        }
    }

    fun sendMessage(sensorEvent: MySensorEvent) {
        if (handler == null) return

        handler?.obtainMessage(sensorEvent.type.ordinal, sensorEvent)?.apply {
            sendToTarget()
        }
    }

Other Implementation Considerations

The decision on how you implement the collection of sensor data depends on the specific requirements for your app. Here some things to consider:

  • If you are planning to display sensor live data, you want the UI updates to be as smooth as possible. Here Android data binding and classes like ViewModel and LiveData can help. They ensure that data is kept in sync with your view and that it is available throughout the Activity or Fragment lifecycle, even if that object is recreated, e.g. due to device orientation changes.
  • If you want the app to start collecting data when the device reboots and while the app is not in the foreground, you can start a service that is triggered from the Device Boot Complete Broadcast. However, this approach may drain the device battery and Google strongly discourages services that run indefinitely in the background. You can do this as a foreground service, which requires you to display a notification which informs the user that your app is running a service int he background.
  • If you want to track sensor data over time, you some kind of persistent data store. For simple conversion of sensor data into a database you can use Realm or Room.

Sampling Period

As an argument to the registerListener() method you can specify a sampling rate to set the speed of collecting data from the sensor. The following options are available:

  1. SENSOR_DELAY_NORMAL: The default rate suitable for most operations.
  2. SENSOR_DELAY_UI: The sampling rate suitable for UI updates.
  3. SENSOR_DELAY_GAME: Sample rate suitable for games.
  4. SENSOR_DELAY_FASTEST: The fastest way to sample data (may affect battery usage).

Implementing a Light Sensor

This section shows you how to continuously retrieve light information from the light sensor in illuminance (lx) units.

Illuminance is a measure of how much luminous flux is spread over a given area. One can think of illuminance as a measure of the intensity of illumination on a surface. (See Wiki)

In the implementation example below we create a singleton LightSensorManager which can collect sensor events throughout the app life cycle.

As discussed above, the sensor collection is done asynchronously on a HandlerThread.

The class first gets the SensorManager from the system services and then gets a light sensor using by getting a sensor of type LIGHT from the sensor manager.

Event updates are communicated via a message handler. Activities or services can register a handler with the LightSensorManager and it will send a message to the handler with a MySensorEvent object that has a property for the sensor type and a value.

The sampling can be controlled by start and stop functions which register and unregister a listener of this sensor type with the sensor manager.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
object LightSensorManager :
    HandlerThread("LightSensorManager"), SensorEventListener {
    private val TAG : String = "LightSensorManager"
    private var handler: Handler? = null
    private var sensorManager : SensorManager?= null
    private var sensor : Sensor?= null
    private var sensorExists = false
    private var sensorThread: HandlerThread? = null
    private var sensorHandler: Handler? = null


    init{
        sensorManager = (MyApplication.getApplicationContext().getSystemService(Service.SENSOR_SERVICE)) as SensorManager
        sensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_LIGHT)

        // Check sensor exists
        if (sensor != null) {
            sensorExists = true
        } else {
            sensorExists = false
        }
    }

    fun startSensor(){
        sensorThread = HandlerThread(TAG, Thread.NORM_PRIORITY)
        sensorThread!!.start()
        sensorHandler = Handler(sensorThread!!.getLooper()) //Blocks until looper is prepared, which is fairly quick
        sensorManager!!.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, sensorHandler)
    }

    fun stopSensor(){
        sensorManager!!.unregisterListener(this)
        sensorThread!!.quitSafely()
    }

    fun sensorExists() : Boolean{
        return sensorExists
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        Log.d(TAG, ""+accuracy)
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if (event != null && event.values.isNotEmpty()) {
            // ground!!.updateMe(event.values[1] , event.values[0])
            //Log.d(TAG, ""+event.values[1]+", "+event.values[0])
            //Log.d(TAG, "onSensorChanged: "+event.values[0])

            var msgEvent = MySensorEvent()
            msgEvent.type = SensorType.LIGHT
            msgEvent.value = event.values[0].toString()

            // Send message to MainActivity
            sendMessage(msgEvent)
        }
    }

    fun setHandler(handler: Handler){
        this.handler = handler
    }

    fun sendMessage(sensorEvent: MySensorEvent) {
        if (handler == null) return

        handler?.obtainMessage(sensorEvent.type.ordinal, sensorEvent)?.apply {
            sendToTarget()
        }
    }
}

Implementing a Temperature Sensor

Temperature can be measured by using a sensor type of TYPE_TEMPERATURE or TYPE_AMBIENT_TEMPERATURE:

1
sensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)

The sensor events are delivered in Celsius, so you’d have to convert them into Farenheit:

Here my sample of a temperature sensor collection class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
object TempSensorManager :
    HandlerThread("TempSensorManager"), SensorEventListener {
    private val TAG : String = "TempSensorManager"
    private var handler: Handler? = null
    private var sensorManager : SensorManager?= null
    private var sensor : Sensor?= null
    private var sensorExists = false
    private var sensorThread: HandlerThread? = null
    private var sensorHandler: Handler? = null

    init{
        sensorManager = (MyApplication.getApplicationContext().getSystemService(Service.SENSOR_SERVICE)) as SensorManager
        sensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE)

        // Check sensor exists
        if (sensor != null) {
            sensorExists = true
        } else {
            sensorExists = false
        }
    }

    fun startSensor(){
        sensorThread = HandlerThread(TAG, Thread.NORM_PRIORITY)
        sensorThread!!.start()
        sensorHandler = Handler(sensorThread!!.getLooper()) //Blocks until looper is prepared, which is fairly quick
        sensorManager!!.registerListener(this,
            sensor, SensorManager.SENSOR_DELAY_NORMAL,
            sensorHandler
        )
    }

    fun stopSensor(){
        sensorManager!!.unregisterListener(this)
    }

    fun sensorExists() : Boolean{
        return sensorExists
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        Log.d(TAG, ""+accuracy)
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if (event != null && event.values.isNotEmpty()) {

            var msgEvent = MySensorEvent()
            msgEvent.type = SensorType.TEMPERATURE
            msgEvent.value = event.values[0].toString()

            // Send message to MainActivity
            sendMessage(msgEvent)
        }
    }

    fun setHandler(handler: Handler){
        this.handler = handler
    }

    fun sendMessage(sensorEvent: MySensorEvent) {
        if (handler == null) return

        handler?.obtainMessage(sensorEvent.type.ordinal, sensorEvent)?.apply {
            sendToTarget()
        }
    }
}

Other Sensor Types

The Android framework provides access to a large variety of sensors and exploring them all would extend over the scope of this tutorial. However, here a list of other sensors, if you’d like to explore further:

Motion Sensors

SENSOR_TYPE_MOTION_DETECT
SENSOR_TYPE_SIGNIFICATION_MOTION
SENSOR_TYPE_ACCELEROMETER
SENSOR_TYPE_GYROSCOPE
SENSOR_TYPE_ROTATION_VECTOR
SENSOR_TYPE_STATIONARY_DETECT
SENSOR_TYPE_STEP_COUNTER
SENSOR_TYPE_STEP_DETECTOR

Environmental Sensors

SENSOR_TYPE_AMBIENT_TEMPERATURE
SENSOR_TYPE_PRESSURE
SENSOR_TYPE_LIGHT
SENSOR_TYPE_RELATIVE_HUMIDITY

Position Sensors

SENSOR_TYPE_MAGNETIC_FIELD
SENSOR_TYPE_MAGNETOMETER
SENSOR_TYPE_GYROSCOPE
SENSOR_TYPE_PROXIMITY
SENSOR_TYPE_ORIENTATION

Sample App for this Post

If you’d like to play with a working sample of a Kotlin app that collects samples from the light sensor, temperature sensor, and gyroscope, please check out my sample application on my GitHub repository here.

Author Description

justmobiledev

No comments yet.

Join the Conversation