This codelab reinforces the content learned in the last lab and shows you how to develop an IOT android application using MQTT. For this lab we'd be using the HiveMQ MQTT Client library for android.

What you'll learn

Most Android-powered devices have built-in sensors that measure motion, orientation, and various environmental conditions. These sensors are capable of providing raw data with high precision and accuracy, and are useful for general monitoring. For example:

The objective of this lab is to develop an application that uses the device's proximity sensor to determine if the device is close to, or far away from an object. In most phones, the proximity sensor is located near the phone's front facing camera. There are a lot of applications that use this sensor. For example

Create a new application. I'm going to name my app, ProximitySensorLab

Android Studio Project

User Interface

The initial user interface should contain the following

  1. A button to enable reading from the sensor
  2. A button to disable reading from the sensor
  3. A TextView that shows one of the following
    • The phone is near an object
    • The phone is not near an object
    • The phone is not receiving data from the proximity sensor

Android Studio Project

This user interface can easily be achieved if the following steps are made

  1. Add a button which will be responsible for enabling the sensor
  2. Add a button which will be responsible for disabling the sensor
  3. Give the Hello World text an ID so that we can change the words of the TextView in code

Try this yourself before going to the solutions below

Contents of my activity_main.xml file

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_sensor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Text that describes the sensor's state here!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_disable" />

    <Button
        android:id="@+id/btn_enable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="40dp"
        android:layout_marginTop="44dp"
        android:text="Enable Sensor"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_disable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="44dp"
        android:layout_marginEnd="40dp"
        android:text="Disable Sensor"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/btn_enable"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

With the initial draft of the user interface done, we can now integrate the sensor's functionality into the app. This can be done as follows:

Feature Addition Listing

To access the proximity sensor, we need to list the feature-set in the AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- Add the line below  -->
    <uses-feature android:name="android.hardware.sensor.proximity" android:required="true" />


    <application
    <!-- Redacted -->

Accessing the Sensor

Inside our ActivityMain.kt, create two instance variables as follows

    // Add the following two lines
    private lateinit var sensorManager: SensorManager
    private var proximity: Sensor? = null

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        // Add the lines below

        // Get an instance of the sensor service, and use that to get an instance of
        // the proximity sensor.
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        proximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
    }

Setting up the MainActivity to listen to sensor events

Android has an interface called SensorEventListener. Modify the MainActivity.kt as follows

  1. Add the import for the Interface

import android.hardware.SensorEventListener

  1. Modify the signature of the MainActivity to implement the interface
class MainActivity : AppCompatActivity(), SensorEventListener {
    //redacted
}
  1. Implement the missing methods
    override fun onSensorChanged(event: SensorEvent?) {
        TODO("Not yet implemented")
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        Log.e("SENSOR", "Accuracy changed")
    }
  1. Add an instance variable that would be used to keep track of if the sensor is enabled or not.
    private lateinit var sensorManager: SensorManager
    private var proximity: Sensor? = null
    
    // Add the following line
    private var isEnabled: Boolean = false
  1. Create functions to enable and disable the sensor
    fun enableSensor(view: View?){
        if (isEnabled){
            Toast.makeText(this, "Sensor already enabled", Toast.LENGTH_SHORT).show()
            return
        }
        proximity?.also { proximity ->
            sensorManager.registerListener(this, proximity, SensorManager.SENSOR_DELAY_NORMAL)
        }
    }

    fun disableSensor(view: View?){
        if (!isEnabled){
            Toast.makeText(this, "Sensor already disabled", Toast.LENGTH_SHORT).show()
            return;
        }
        sensorManager.unregisterListener(this)
        isEnabled = false
        findViewById<TextView>(R.id.tv_sensor).text = "The phone is not receiving data from the proximity sensor"
    }

We've currently laid the framework for handling the sensor data but there are still a few things outstanding. We need to link the user interface to the functionality This can be done as follows.

  1. Link the "Enable sensor" button to the enableSensor function.
  2. Link the "Disable sensor" button to the enableSensor function.
  3. Updating the text based on the sensor's state

Enable Sensor functionality

Update the activity_main.xml file and add an onClick property to the "Enable Sensor" button.

    <Button
        android:id="@+id/btn_enable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="40dp"
        android:layout_marginTop="44dp"
        android:onClick="enableSensor"
        android:text="Enable Sensor"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Disable Sensor functionality

Update the activity_main.xml file and add an onClick property to the "Disable Sensor" button.

    <Button
        android:id="@+id/btn_disable"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="44dp"
        android:layout_marginEnd="40dp"
        android:text="Disable Sensor"
        android:onClick="disableSensor"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/btn_enable"
        app:layout_constraintTop_toTopOf="parent" />

Update the text

The sensor gives us data in the onSensorChanged function. The data that comes back is the distance to the nearest object.

Update the onSensorChanged function as follows

    override fun onSensorChanged(event: SensorEvent?) {
        if (event == null) return
        if (event.sensor.type != Sensor.TYPE_PROXIMITY) return

        val distanceToNearestObject = event.values[0]
        val textView = findViewById<TextView>(R.id.tv_sensor)

        Log.e("SENSOR", "DISTANCE = $distanceToNearestObject")
        if (distanceToNearestObject < event.sensor.maximumRange){
            textView.text = "The phone is near an object"
        } else {
            textView.text = "The phone is not near an object"
        }
    }

Currently we have an android app that is fully functional. It tells us whether the phone is close to a device or not. We need to build upon this functionality and integrate the MQTT functionality to notify the broker.

The steps we need to perform are as follows

  1. Add an external library to our app to give the MQTT client functionality
  2. Use that library to relay messages to our broker

Add the external library

  1. Expand the Gradle Scripts in the explorer panel
  2. Open the build.gradle.kts (Module: app) file
  3. Add implementation(libs.hivemq.mqtt.client) in the dependencies block OR implementation("com.hivemq:hivemq-mqtt-client:1.3.3")
  4. Sync the project
  5. Add the following to the android block above packagingOptions { resources.excludes.add("META-INF/*") }

Android Studio Project

Integrate the MQTT functionality

Add the internet permission to the manirfest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:name="android.hardware.sensor.proximity" android:required="true" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
    <!-- redacted -->

Add a new instance variable to the MainActivity class.

    private lateinit var sensorManager: SensorManager
    private var proximity: Sensor? = null
    private var isEnabled: Boolean = false
    // Add the line below
    private var client: Mqtt5BlockingClient? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        //redacted
    }

Inside the onCreate in MainActivity.kt, create a new client that will handle the MQTT calls

        // Get an instance of the sensor service, and use that to get an instance of
        // the proximity sensor.
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        proximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)

        //Add the lines below
        client = Mqtt5Client.builder()
            .identifier(UUID.randomUUID().toString())
            .serverHost("broker.sundaebytestt.com")
            .serverPort(1883)
            .build()
            .toBlocking()

Modify the enableSensor function, let it also connect to the broker once the sensor is enabled

        proximity?.also { proximity ->
            sensorManager.registerListener(this, proximity, SensorManager.SENSOR_DELAY_UI)
            isEnabled = true
            //Add the lines of code below
            try {
                client?.connect()
            } catch (e:Exception){
                Toast.makeText(this,"An error occurred when connecting to broker", Toast.LENGTH_SHORT).show()
            }
        }

Handle sending of messages

For this lab, we will send messages to the topic lab/proximity

Modify the onSensorChanged function to send a message on that topic to the broker

    override fun onSensorChanged(event: SensorEvent?) {
        if (event == null) return
        if (event.sensor.type != Sensor.TYPE_PROXIMITY) return

        val distanceToNearestObject = event.values[0]
        val textView = findViewById<TextView>(R.id.tv_sensor)

        Log.e("SENSOR", "DISTANCE = $distanceToNearestObject")
        val textToSend = if (distanceToNearestObject < event.sensor.maximumRange) {
            "Kwasi's phone is near an object"
        } else {
            "Kwasi's phone is NOT near an object"
        }

        textView.text = textToSend
        try{
            client?.publishWith()?.topic("lab/proximity")?.payload(textToSend.toByteArray())?.send()
        } catch (e:Exception){
            Toast.makeText(this,"An error occurred when sending a message to the broker", Toast.LENGTH_SHORT).show()
        }
    }

Handle the disconnection

We want to disconnect from the broker when we stop listening to sensor updates. Update the disconnect function as follows

    fun disableSensor(view: View?){
        if (!isEnabled){
            Toast.makeText(this, "Sensor already disabled", Toast.LENGTH_SHORT).show()
            return;
        }
        sensorManager.unregisterListener(this)
        isEnabled = false
        findViewById<TextView>(R.id.tv_sensor).text = "The phone is not receiving data from the proximity sensor"

        try {
            client?.disconnect()
        } catch (e:Exception){
            Toast.makeText(this,"An error occurred when disconnecting from broker", Toast.LENGTH_SHORT).show()
        }
    }

https://developer.android.com/develop/sensors-and-location/sensors/sensors_overview https://hivemq.github.io/hivemq-mqtt-client/docs/installation/