14. Working With Sensors

14.1. Motion Sensor

Motion sensors are useful for monitoring device movement, such as tilt, shake, rotation, or swing. The movement is usually a reflection of direct user input, but it can also be a reflection of the physical environment in which the device is sitting.

All of the motion sensors return multi-dimensional arrays of sensor values for each SensorEvent. For example, the accelerometer returns acceleration force data for the three coordinate axes.

In this exercise, we will use accelerometer and display sensor values. These values can be used in many ways - e.g., in mobile games where the user moves an object by tilting the device.

  1. Create a new empty project, and remove the Hello World TextView.

  2. Implement the SensorEventListener interface, including all methods.

    class MainActivity : AppCompatActivity(),SensorEventListener{
    
       ...
    
       override fun onAccuracyChanged(event: Sensor?, p1: Int) {
       }
    
       override fun onSensorChanged(event: SensorEvent) {
       }
    }
    
  3. The following code shows you how to get an instance of the default acceleration sensor:

    val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    
  4. Then register a SensorEventListener for the given sensor at the desired sampling frequency. In this case, it is registered with SENSOR_DELAY_NORMAL, which is the default. You could also use SENSOR_DELAY_FASTEST to get sensor data as fast as possible, or SENSOR_DELAY_GAME to get sensor data at a rate suitable for games.

    sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
    
  5. Now, get sensor data and display in a TextView.

    event.values[0] is the acceleration force along the x axis (including gravity).

    event.values[1] is the acceleration force along the y axis (including gravity).

    event.values[2] is the acceleration force along the z axis (including gravity).

    override fun onSensorChanged(event: SensorEvent) {
    
       textView.setText("x = ${event.values[0]}\n"
             +"y = ${event.values[1]}\n"
             +"z = ${event.values[2]}\n")
    }
    

    Note: To measure the real acceleration of the device, the force of gravity must be removed from the accelerometer data.

14.2. User Location

There are several ways that the location of a device can be determined: GPS, Wi-Fi, and cell signal. However, getting location requires a lot of power, so we need to take battery usage into account. In general, high accuracy ,high frequency and low latency lead to more battery usage.

Here are several ways to define accuracy:

  1. PRIORITY_HIGH_ACCURACY provides the most accurate location possible, computed using as many inputs as necessary (GPS, Wi-Fi, cell, and a variety of Sensors), and may cause significant battery drain.

  2. PRIORITY_BALANCED_POWER_ACCURACY provides accurate location while optimising for power. Very rarely uses GPS. Typically uses a combination of Wi-Fi and cell information to compute device location.

  3. PRIORITY_LOW_POWER largely relies on cell towers and avoids GPS and Wi-Fi inputs, providing city-level accuracy with minimal battery drain.

  4. PRIORITY_NO_POWER receives locations passively from other apps for which location has already been computed.

The PRIORITY_BALANCED_POWER_ACCURACY and PRIORITY_LOW_POWER options satisfy most apps’ needs.

Here are several ways to define frequency:

  1. setinterval() specifies the interval at which location is computed for your app.

  2. setFastestInterval() to specify the interval at which location computed for other apps is delivered to your app.

Latency can be set using the setMaxWaitTime() method, typically passing a value that is several times larger than the interval specified in the setInterval() method. This setting delays location delivery, and multiple location updates may be delivered in batches. These two changes help minimise battery consumption.

14.2.1. Get the last known location

getLastLocation() returns the most recently available location (which in rare cases may be null). This method provides a simple way of getting location and doesn’t incur costs associated with actively requesting location updates.

In most cases, you are interested in the user’s current location, which is usually equivalent to the last known location of the device.

The fused location provider is one of the location APIs in Google Play services. It manages the underlying location technology and provides a simple API so that you can specify requirements at a high level, like high accuracy or low power. It also optimises the device’s use of battery power.

  1. To use fused location provider, first we need to set up Google Play services. In app.gradle, add:

    implementation 'com.google.android.gms:play-services-location:17.0.0'
    
  2. Then request this permission with the uses-permission element in your app manifest:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
  3. Now, create an instance of FusedLocationProviderClient and call the getLastLocation method on this object to request the last known location:

    private lateinit var fusedLocationClient: FusedLocationProviderClient
    
    override fun onCreate(savedInstanceState: Bundle?) {
      fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
      fusedLocationClient.lastLocation.addOnSuccessListener { location : Location? ->
      }
    }
    
  4. Create a button and a textView in activity_main.xml, so that when the button is clicked, the app starts to get latitude and longitude of the location, which will be displayed in textView.

  5. Using location services requires special run-time permission. The following code requests run time-permission for ACCESS_COARSE_LOCATION:

    // If permission is not granted
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
    
       // If this is not the first time it required permission, explain the purpose of this permission
       // After user's response, try again to ask permission
       if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.ACCESS_COARSE_LOCATION)) {
    
           val builder = AlertDialog.Builder(this)
            builder.setMessage("You have to give permission to access feature")
                .setPositiveButton("OK", DialogInterface.OnClickListener { dialog, i ->
                    ActivityCompat.requestPermissions(this,
                        arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                        1
                    )
                })
                .setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, i ->
                    dialog.dismiss()
                })
                .create()
                .show()
    
        }
        // If this is the first time asking permission, request permission without explanation
        else {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                1
            )
        }
    }
    else {
       // Permission is granted
    }
    
  6. Now you can add a setOnClickListener on the button, and place all the code associated with requesting permission inside the setOnClickListener, so that the app only requires permission if the user wants to get their location.

  7. Then place the code to get the last location in the ‘Permission is granted’ else block. The lambda function that is executed when last location is retrieved successfully should display the latitude and longitude in textView:

    fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
       if (location != null) {
            var latitude = location.latitude
            var longitude = location.longitude
            textView.setText("Latitude = ${latitude}" + "\nLongitude = ${longitude}")
       }
    }
    

    Note that location may be null in the following situations:

    • Location is turned off in the device settings. The result could be null even if the last location was previously retrieved because disabling location also clears the cache.

    • The device never recorded its location, which could be the case of a new device or a device that has been restored to factory settings.

    • Google Play services on the device has restarted, and there is no active FusedLocationProviderClient that has requested location after the services restarted. To avoid this situation you can create a new client and request location updates yourself.