MAD Topic 4: Android Permissions, GPS, ViewModel

Android Runtime Permissions

Revision: The manifest file and Permissions

Last week you were very briefly introduced to permissions. Apps need to be granted permissions to perform sensitive operations. Sensitive operations can include:

The permissions an app needs are specified in the AndroidManifest.xml file, within the manifests directory, before the <application> tag. For example, to specify that we need internet permission, we would add the line:
<uses-permission android:name="android.permission.INTERNET" />

The classic permissions model

Before Android 6, permissions were specified at install time, so a user was informed what permissions an app needed when they installed the app. However, this model had some disadvantages. It is not very flexible; a user might want to use some permissions but turn others off. For example, if a user is installing a mapping app which allows the user to take photos, they might wish to use the GPS but not the camera, for privacy reasons.

Runtime Permissions

Android 6 (API level 23) uses a runtime permissions model, in which permissions can be turned on and off at run time, rather than install time. With Android 6 permissions, you can grant an app certain permissions but not others, at run time. You can also turn permissions on and off at any time via the device's Settings.

To enforce the use of Android 6 permissions, the targetSdkVersion of your app must be at least 23; we looked at targetSdkVersion last week.

Using the Android 6 permissions model

Dangerous permissions

Certain permissions in Android are more sensitive than others, these are called dangerous permissions. Dangerous permissions include location, using the GPS (for privacy reasons), camera (even more so) and file I/O (because of the risk of a rogue app reading private data on your device). It is these dangerous permissions which require runtime permission checking, and these permissions which can be turned on or off in the Settings.

Less sensitive permissions, such as internet, can be done purely via the manifest, as before; these do not need runtime permission checking.

See this documentation on the Android site

What happens from the user point of view?

As well as granting the permission at runtime, a user can grant permission by going into the phone's Settings, selecting "Apps" and then the appropriate app, and then turning the appropriate setting on. A user can also revoke the permission through the Settings.

Implementation details

Checking whether a permission has been granted

You use the checkSelfPermission() call for this.

Here is an example of checking for the ACCESS_FINE_LOCATION (listen to GPS) permission. This is in a custom function called checkPermissions(), which would be called from onCreate().

fun checkPermissions() {
    val requiredPermission = Manifest.permission.ACCESS_FINE_LOCATION

    if(checkSelfPermission(requiredPermission) == PackageManager.PERMISSION_GRANTED) { 
        startGPS() // a function to start the GPS - see below
    } else {
        // Request the permission (see below for code)...
    }
}
Note how checkSelfPermission() takes the permission we are interested in (ACCESS_FINE_LOCATION) as an argument.

This method will return one of these constants:

If you are targeting API level 23 and above, Android Studio's lint tool will produce an error if you leave this check out (and if you disable the lint tool, the app will crash at runtime with a SecurityException)

Requesting permissions in your code

If checkSelfPermission() returns PackageManager.PERMISSION_DENIED, then you should request that permission from the user in your code. This is done using a launcher. A launcher is an Android API class which is used to launch various types of task involving user interaction (such as a permissions dialog), and runs a callback function (typically a lambda function) once the user has performed the required interaction. The launcher is created with the method registerForActivityResult() with parameters of:

Here is an example:

val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
    if(isGranted) {
        startGPS() // A function to start the GPS - see below
    } else {
        // Permission not granted
        Toast.makeText(this, "GPS permission not granted", Toast.LENGTH_LONG).show()
    }
}
permissionLauncher.launch(requiredPermission)

Note how in this example, we create a launcher called permissionLauncher and specify a lambda function which takes one parameter, isGranted. This lambda will run as soon as the user either grants or denies the permission. The isGranted parameter will be either true or false, as a result. So the lambda will include logic to start the GPS (if the permission was granted) or inform the user that the permission was not granted (if not). Note how we use a Toast here - a small popup message which appears on the screen above the UI. Toast takes this form:

Toast.makeText (this, message, length).show()
this refers to the activity. length can either be Toast.LENGTH_SHORT or Toast.LENGTH_LONG.

We actually launch the launcher with :

permissionLauncher.launch(requiredPermission)

We can request multiple permissions with ActivityResultContracts.RequestMultiplePermissions() and then pass an array of the required permissions to the launcher, e.g:

permissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA))

Location

Having covered runtime permission handling, we are now going to look at a key piece of functionality which uses it; obtaining your current location. Many of the most interesting mobile apps are location-based apps, in other words, the app is sensitive to the user's location on earth. We might, for instance, have an app which displays a map of your current location. Or an app which looks up the nearest pubs, bus stops or railway stations. Such apps send the user's location to a web server, which then delivers data near that location. Most smartphones contain a GPS chip which obtains the device's location by communicating with Global Positioning System (GPS) satellites: the same mechanism used in car satnav systems. So, since location is so important in mobile app development, we are going to look at it now.

Ethical issues with geolocation

There are important ethical issues when it comes to location. Potentially, a malicious app could gather your location and send it to a server with a user's personal details, which could allow the user to be tracked without their consent. For this reason, location tracking is treated as a dangerous permission and must be granted by the user at runtime.

Furthermore, it is recommended to create an explicit privacy policy on your app's website (which can be linked to from Google Play) which spells out to users why the app needs location information and what will be done with it.

Adding location permission to the manifest file

There are two permissions for location: ACCESS_FINE_LOCATION for accurate (GPS) position, and ACCESS_COARSE_LOCATION (for less-accurate (wifi and cell-tower) position. As for any permission (dangerous and non-dangerous), you need to add the appropriate permission to the manifest:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Basic use of Geolocation

This incomplete example shows how to obtain your location. Note that a fully-working example would also need to check whether the ACCESS_FINE_LOCATION permission has been granted at runtime, and request it if not, using the technique described above.

package com.example.nickw.location

class MainActivity : ComponentActivity(), LocationListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        // code omitted...
    }

    fun checkPermissions() {
        // code omitted....
    }
 
    // The linter will warn you that you are trying to request GPS updates
    // without checking whether the permission has been granted.
    // However, we have already checked for GPS permission in the
    // checkPermissions() method, so we can turn this linter check off.
    @SuppressLint("MissingPermission")
    fun startGPS() {
        val mgr = getSystemService(LOCATION_SERVICE) as LocationManager
        mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this )
    }

    // Compulsory - provide onLocationChanged() method which runs whenever
    // the location changes
    override fun onLocationChanged(location: Location) {
          Toast.makeText(this, "Latitude: ${location.latitude}, Longitude: ${location.longitude}", Toast.LENGTH_SHORT).show() 
    }

    // Optional - runs when the user enables the GPS
    override fun onProviderEnabled(provider: String) {
          Toast.makeText(this, "GPS enabled", Toast.LENGTH_LONG).show() 

    }

    // Optional - runs when the user disables the GPS
    override fun onProviderDisabled(provider: String) {
          Toast.makeText(this, "GPS disabled", Toast.LENGTH_LONG).show() 
    }

}
This code sample shows how to use the Location API. It will listen to the device's GPS and update the main content view with the current latitude and longitude (position on earth). So how does it work? The key thing is that there are three main components involved in obtaining your location:

To go through the code above in more detail: