Sensors Tutorial for Android: Getting Started

In this sensors tutorial, you’ll learn about different types of sensors and how to use sensor fusion (accelerometer with magnetometer) to develop a compass. By Aaqib Hussain.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Getting Values From the Accelerometer and Magnetometer

In LocatyService, create the following variables:

private val accelerometerReading = FloatArray(3)
private val magnetometerReading = FloatArray(3)

These variables will hold the latest accelerometer and magnetometer values.

For this tutorial, you only need to use onSensorChanged since you get all the latest sensor values there. So in onSensorChanged, add the following snippet:

override fun onSensorChanged(event: SensorEvent?) {
    // 1
    if (event == null) {
        return
    }
    // 2
    if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
        // 3
        System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
    } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
        System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
    }
}

Here’s what the code above does:

  1. If the event is null, then simply return
  2. Check the type of sensor
  3. System.arrayCopy copies values from the sensors into its respective array.

Calculating Orientation in onSensorChanged

To find the device’s orientation, you first need to determine its rotation matrix.

Note:A rotation matrix helps map points from the device’s coordinate system to the real-world coordinate system.

Start by creating two arrays as follows:

private val rotationMatrix = FloatArray(9)
private val orientationAngles = FloatArray(3)

These two arrays will hold the values of the rotation matrix and orientation angles. You’ll learn more about them soon.

Next, create a function and name it updateOrientationAngles, then add the following code to it. Import kotlin.math.round as a rounding function.

fun updateOrientationAngles() {
  // 1
  SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading)
  // 2
  val orientation = SensorManager.getOrientation(rotationMatrix, orientationAngles)
  // 3
  val degrees = (Math.toDegrees(orientation.get(0).toDouble()) + 360.0) % 360.0
  // 4
  val angle = round(degrees * 100) / 100

}

Here’s how it works:

In the variable orientation, you get values that represent

All these values are in radians.

  1. First, it gets the rotation matrix.
  2. It then uses that rotation matrix, which consists of an array of nine values, and maps it to a usable matrix with three values.

    In the variable orientation, you get values that represent

    • orientation[0] = Azimuth (rotation around the -ve z-axis)
    • orientation[1] = Pitch (rotation around the x-axis)
    • orientation[2] = Roll (rotation around the y-axis)

    All these values are in radians.

  3. Next, it converts the azimuth to degrees, adding 360 because the angle is always positive.
  4. Finally, it rounds the angle up to two decimal places.

Now, you need to call updateOrientationAngles inside onSensorChanged at the very end. It should look like this:

override fun onSensorChanged(event: SensorEvent?) {
  // Rest of the code

  updateOrientationAngles()
}

Adding Direction Based on Angle

For your next step, you need to determine which direction the user is facing. To do so, add the following code:

private fun getDirection(angle: Double): String {
   var direction = ""

   if (angle >= 350 || angle <= 10)
       direction = "N"
   if (angle < 350 && angle > 280)
       direction = "NW"
   if (angle <= 280 && angle > 260)
       direction = "W"
   if (angle <= 260 && angle > 190)
       direction = "SW"
   if (angle <= 190 && angle > 170)
       direction = "S"
   if (angle <= 170 && angle > 100)
       direction = "SE"
   if (angle <= 100 && angle > 80)
       direction = "E"
   if (angle <= 80 && angle > 10)
       direction = "NE"

   return direction
}

Here’s what you’re doing with this code:

You find the cardinal and intercardinal directions based on the angle you pass.

Intercardinal directions are the intermediate directions: Northeast is 45°, southeast is 135°, southwest is 225° and northwest is 315°.

Note: Cardinal directions are north, east, south and west. They define a clockwise rotation from north to west, with west and east being perpendicular to north and south.

Intercardinal directions are the intermediate directions: Northeast is 45°, southeast is 135°, southwest is 225° and northwest is 315°.

The theory behind the function above is that, according to cardinal directions, north is 0° or 360°, east is 90°, south is 180° and west is 270°.

Next, add the code below to the end of updateOrientationAngles:

fun updateOrientationAngles() {

  val direction = getDirection(degrees)
}

In the above variable direction, you’ll get a String for the user’s direction based on the angle that you pass.

Now that you have the angle and direction, it’s time to pass them to MainActivity.

Sending Data to MainActivity

First, create a set of keys in LocatyService:

companion object {
  val KEY_ANGLE = "angle"
  val KEY_DIRECTION = "direction"
  val KEY_BACKGROUND = "background"
  val KEY_NOTIFICATION_ID = "notificationId"
  val KEY_ON_SENSOR_CHANGED_ACTION = "com.raywenderlich.android.locaty.ON_SENSOR_CHANGED"
  val KEY_NOTIFICATION_STOP_ACTION = "com.raywenderlich.android.locaty.NOTIFICATION_STOP"
}

These are keys that you’ll use to send data from LocatyService to MainActivity.

After that, import LocalBroadcastManager in LocatyService:

import androidx.localbroadcastmanager.content.LocalBroadcastManager

Then add the following code in updateOrientationAngles:

fun updateOrientationAngles() {
// 1
val intent = Intent()
intent.putExtra(KEY_ANGLE, angle)
intent.putExtra(KEY_DIRECTION, direction)
intent.action = KEY_ON_SENSOR_CHANGED_ACTION
// 2
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}

Take a look at this code, step-by-step:

  1. Create an intent object and put data in it with respect to its keys.
  2. You then send out a local broadcast with the intent

Open MainActivity and add the following code in onCreate. Also, import LocalBroadcastManager.

LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver,  IntentFilter(LocatyService.KEY_ON_SENSOR_CHANGED_ACTION))

Also import android.content.BroadcastReceiver and add the following in your MainActivity.

private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
   override fun onReceive(context: Context, intent: Intent) {
     // 1
     val direction = intent.getStringExtra(LocatyService.KEY_DIRECTION)
     val angle = intent.getDoubleExtra(LocatyService.KEY_ANGLE,0.0)
     val angleWithDirection = "$angle  $direction"
     binding.directionTextView.text = angleWithDirection
     // 2
     binding.compassImageView.rotation = angle.toFloat() * -1
   }
}

Here’s what you’re doing above:

  1. You retrieve and assign data to views.
  2. Since the angle you get is in a counter-clockwise direction and the views in Android rotate in a clockwise manner, you need to mirror the angle so that it becomes clockwise as well. To do this, you multiply it by -1.

Next, paste the following code in onDestroy:

override fun onDestroy() {
  LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
  super.onDestroy()
}

This will unregister your BroadcastReceiver when it’s no longer needed.

In startForegroundServiceForSensors, add the following code:

// 1 
val locatyIntent = Intent(this, LocatyService::class.java)
locatyIntent.putExtra(LocatyService.KEY_BACKGROUND, background)
// 2
ContextCompat.startForegroundService(this, locatyIntent)

With this code, you’re:

  1. Create intent for service.
  2. Starting foreground service.

Then, in onResume, add the following:

override fun onResume() {
  super.onResume()
  startForegroundServiceForSensors(false)
}

As soon as your activity starts, onResume is called. You pass a false in the function because the app is in the foreground.

Also in onPause, do the following:

override fun onPause() {
  super.onPause()
  startForegroundServiceForSensors(true)
}

onPause is called when app goes in the background. Thus, you pass true in the function to let LocatyService know that app is no longer in the foreground.