MAD Topic 6: 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 a task which requires 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 registerForActivityResult() with parameters of ActivityResultContracts.RequestPermission(), plus the lambda.

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 {

    fun startGPS() {
        val mgr = getSystemService(LOCATION_SERVICE) as LocationManager
        mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this )
    }

    override fun onLocationChanged(location: Location) {
          Toast.makeText(this, "Latitude: ${location.latitude}, Longitude: ${location.longitude}", Toast.LENGTH_LONG).show() 
    }

    override fun onProviderEnabled(provider: String) {
          Toast.makeText(this, "GPS enabled", Toast.LENGTH_LONG).show() 

    }

    override fun onProviderDisabled(provider: String) {
          Toast.makeText(this, "GPS disabled", Toast.LENGTH_LONG).show() 
    }

    // Deprecated at API level 29, but must still be included, otherwise your
    // app will crash on lower-API devices as their API will try and call it
    override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
    
    }
}
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:

In a LocationListener, you must provide four methods to handle different location events:

TheonLocationChanged() method takes a single parameter - a Location object representing our current location. It has two properties, latitude and longitude, for getting the actual latitude and longitude. So, in this app, we simply display the current latitude and longitude with a Toast.

Testing with a virtual device

If you do not have a real device, or are located indoors (as you will probably be during the class; indoor GPS signals are not reliable) it is possible to test it in a development environment (with a GPS provider) by sending "virtual" locations to the emulator.

This is quite straightforward. Click on the three dots (...) at the top of the controls of the virtual device:
Accessing emulator extended controls
This will take you to a dialog showing a map, by default located in Silicon Valley, California. You can change location by moving the map or searching for locations and then clicking "Set Location. This will send a virtual GPS signal to the device, containing the latitude and longitude entered.
Emulator extended controls

Updating the UI using the GPS position

How might you set the position of an osmdroid map to the GPS position, or even simply display the latitude and longitude on the UI? This is a little tricky as the map position is stored in state in a Compose application, as we saw two weeks ago, and state cannot be accessed outside composables (the GPS code will be outside our composables). How then can we communicate the GPS coordinates to the state variable of a composable?

The answer is to use a view model with live data.

The ViewModel

A common aim in object-oriented programming is to make all classes as concise, and focused on one thing, as possible. This makes the classes smaller and easier to understand, and thus maintain. A small, tightly-focused class (focused on one particular thing) has high cohesion. Achieving high cohesion is well-known good software engineering practice, as you should know from OODD.

However, in Android development, if we are not careful our activities can quickly become very large and "bloated", and end up containing a lot of variables hoding the application's data and methods to manipulate that data. Thus they have low cohesion, as they are trying to do too many things. It would be better to try and separate out the activity from its data so that the activity only manages core operations (such as onCreate() and setting up the UI) and have another class which stores the data.

Luckily we can do this quite easily with Android thanks to the use of a ViewModel (see the Android documentation). What is a ViewModel? It's an object which holds the data which needs to be displayed by the application, and is responsible for "preparing data for the UI" (Android docs), as part of the Model-View-ViewModel (MVVM) architecture, which we will return to in the database topic.

As well as enabling higher cohesion, using a ViewModel in Android has the advantage that it is persistent throughout the lifetime of the application, even if other components such as activities are destroyed. An important feature of Android that might not be obvious is that an activity is destroyed and re-created when it's rotated. This can cause difficulties if you want to preserve data; by default, all data stored in an activity will be destroyed when it's rotated, as it belongs to a particular instance of the activity. If you use a ViewModel instead, the data will be preserved and will be accessible by the activity when it is re-created.

Furthermore, use of a ViewModel allows data to be updated from non-UI parts of your application (e.g. the Location API as seen above) and then observed from your UI, such as Jetpack Compose. In this way, Compose UIs can be updated when data external to them, such as GPS location or a database, updates.

ViewModel

Creating a ViewModel

Creating a ViewModel is quite easy. We need to ensure the lifecycle-runtime-ktx Jetpack library into our build.gradle dependencies, e.g:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
and create a class inheriting from androidx.lifecycle.ViewModel.
package com.example.viewmodel1

import androidx.lifecycle.ViewModel

class TestViewModel : ViewModel() {
    var firstName = ""
    var lastName = ""
}
This is creating a simple ViewModel to store someone's first and last name.

Using the ViewModel from an Activity

We then need to initialise the ViewModel from an Activity. The example below shows how to do this:

class MainActivity :AppCompatActivity() {

    val viewModel : TestViewModel by viewModels()

    // ... class continues ...
Note how we declare a variable viewModel within our activity, of class TestViewModel (our ViewModel class shown above). Note also how we initialise it with a delegate (see last week).

Updating the ViewModel and the UI

Later on, at any point, we can update the ViewModel's data, e.g.

viewModel.apply {
    firstName = "Fred"
    lastName = "Jones"
}

Clearly this is a very simple example, but for more complex apps, which need to store and display large amounts of data, a ViewModel makes sense. Furthermore, even in this simple example, a ViewModel can be advantageous, as the data remains in memory even when the device is rotated. If we simply stored the first name and last name in the activity, it would be lost when the device is rotated, as the activity (and all its data) is destroyed and re-created. The ViewModel by contrast is stored independently to the activity in memory.

Using LiveData with ViewModel

Commonly, ViewModel is used together with LiveData. A ViewModel can hold LiveData which is observed by an Observer. When the data changes, the Observer callback function - a lambda, typically - receives the altered data and uses it to update the UI. In a Compose application, a state variable would be updated with the altered live data from the view model.

We will use location data as our example of LiveData. The latitude and longitude (from the Location API) will be stored in a ViewModel and made available as LiveData which the UI can then observe (so for example, a map can update with the latitude and longitude contained within the LiveData.)

First the ViewModel, which contains a LatLon object and a LiveData object wrapping that LatLon:

data class LatLon(val lat: Double, val lon: Double) // our LatLon class

class LatLonViewModel: ViewModel() {
    var latLon = LatLon(51.05, -0.72)
        set(newValue) {
            field = newValue
            latLonLiveData.value = newValue
        }

    var latLonLiveData = MutableLiveData<LatLon>()
}

Note how we use a custom setter with the latLon object within the view model (we introduced these in topic 5). In the custom setter we update the live data as well as the latLon object itself. This ensures that the live data tracks the latLon object itself and keeps in sync with it.

latLonLiveData.value = newValue

We can then update the latLon in the view model from our onLocationChanged(), when the GPS location changes:

latLonViewModel.latLon = LatLon(location.latitude, location.longitude)

Observing the LiveData

We observe the LiveData from our UI by calling its observe() method. observe() expects either an Observer object, or a lambda which will run whenever the live data changes:

latLonViewModel.latLonLiveData.observe(this) {
    // inside the lambda, "it" will be the LatLon being observed; use it to update the UI
}

Map Markers

This topic is relatively easy compared to some of the recent topics but is important to know if you are developing any sort of geographically-aware app: the ability to add markers to a mapping application. Many geographically-aware apps show points of interest as markers on the map.

How to code markers in osmdroid

Adding markers to a map

The incomplete example below (only relevant code is shown) demonstrates how to add markers to a map:

import org.osmdroid.views.overlay.Marker // needed for marker

AndroidView (

    factory =  { ctx ->
           Configuration.getInstance()
            .load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx))

        val map1 = MapView(ctx).apply { 
            setMultiTouchControls(true)
            setTileSource(TileSourceFactory.MAPNIK)
            controller.setZoom(14.0)
        }
        val marker = Marker(map1)
        marker.apply {
            position = GeoPoint(51.05, -0.72)
            title = "Fernhurst, village in West Sussex"
        }
        map1.overlays.add(marker)
        map1 // last statement is return value of lambda
    }
    // ...
}
Note what we are doing here:

Custom markers

So far we have used the built-in osmdroid marker. However we can use custom markers for our app. Here is how we do this :

marker.apply {
    icon = getDrawable(R.drawable.marker)
}

Note how the we use the method getDrawable(). This is an object of type Drawable representing the marker icon. We obtain a Drawable by supplying the resource ID R.drawable.marker as a parameter to getDrawable(). The resource ID R.drawable.marker points to an image marker.png in the drawable folder (the folder storing drawables, i.e. images and icons) within the res folder of the project.

Exercises

  1. Use the code examples above to produce a complete Jetpack Compose app which displays the current GPS latitude and longitude on-screen within Text composables. When the GPS position updates, the Text composables should update. You will need to use runtime permission checking, the Location API, and a ViewModel with LiveData.
  2. Extend the first question to include an osmdroid map. The centre of the map should be set to the GPS position.
  3. Find points of interest in your home town (such as pubs, churches, restaurants, sports grounds, etc) in your home town, and add markers to each. Test it out by setting the fake GPS position in the emulator to your home town.
  4. Use different markers for different point of interest types. There are some sample markers here.
  5. Advanced: Store the points of interest in your ViewModel as a list of items and make them available as LiveData. Observe the LiveData from your UI and add markers for each point of interest in the LiveData.