Last week you were very briefly introduced to permissions. Apps need to be granted permissions to perform sensitive operations. Sensitive operations can include:
<application> tag. For example, to specify that we need internet permission, we
would add the line:
<uses-permission android:name="android.permission.INTERNET" />
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.
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.
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
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.
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:
PackageManager.PERMISSION_GRANTED if the permission has already been granted by the user;PackageManager.PERMISSION_DENIED if not.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:
ActivityResultContracts.RequestPermission() here);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))
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.
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.
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" />
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:
LocationListener interface - remember we met interfaces in OODD. The location listener is the class which implements the LocationListener interface, e.g. here it's the MainActivity:
public class MainActivity : ComponentActivity(), LocationListenerIn the
LocationListener we provide the methods specified by the interface. The key method is onLocationChanged(), which runs when we get a new location.
To go through the code above in more detail:
val mgr = getSystemService(Context.LOCATION_SERVICE) as LocationManager
mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this)Note the parameters:
In a LocationListener, these methods 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.
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. This opens the 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.
Let's imagine we want to use GPS location with Compose. How might we do this? You can probably imagine the general approach: whenever we get a new GPS position, we need to update a state variable within our composable hierarchy, containing the latitude and longitude. However, our location listener code takes place outside the composable hierarchy, as shown below:
class MainActivity : ComponentActivity(), LocationListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
val latLngState = mutableStateOf(LatLng(0.0, 0.0))
MyMap(latLngState.value) // imagine MyMap is our own composable
}
}
}
fun checkPermissions() { /* omitted */ }
fun startGPS() { /* omitted */ }
override fun onLocationChanged(loc: Location) {
// How can we access latLngState outside the composable hierarchy?
}
}
So how can we update a state variable within a composable, from outside that composable?. We have to use special techniques such as using a view model.
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.

Creating a ViewModel is quite easy. We need to ensure the lifecycle-runtime-ktx Jetpack library is in our dependencies (group androidx.lifecycle, name lifecycle-runtime-ktx), but this should automatically be done in current versions of Android Studio.
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.
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 by. What does this mean? We are using a Kotlin feature known asdelegates, which we will cover later - but for now, all you need to know is that we are creating an instance of the TestViewModel class using a custom, non-default object creation process.
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.
A common situation involves updating the UI whenever data in a ViewModel changes. For example, if we are storing a GPS position in a ViewModel, we probably want the map to update to display the new GPS position. We can do this manually, but this can be a bit tedious. Instead, we can employ the observer pattern (which you met on OODD). We can create observable live data within our view model and attach an observer to this live data. Inside the observer we can write code to update the UI whenever the live data changes.
ViewModel is used together with LiveData. As indicated abobe, 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:
class LatLngViewModel: ViewModel() {
var latLng = LatLng(51.05, -0.72)
set(newValue) {
field = newValue
latLngLiveData.value = newValue
}
var latLngLiveData = MutableLiveData<LatLng>()
}
Note how we use a custom setter with the latLng object within the view model (we introduced these in OODD). In the custom setter we update the live data as well as the latLng object itself. This ensures that the live data keeps in sync with the latLng object.
latLngLiveData.value = newValue
We can then update the latLng in the view model from our onLocationChanged(), when the GPS location changes:
latLngViewModel.latLng = LatLng(location.latitude, location.longitude)
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:
latLngViewModel.latLngLiveData.observe(this) {
// inside the lambda, "it" will be the LatLng being observed; use it to update the UI
}
Unfortunately on some machines the emulator appears to crash when displaying MapLibre maps (whether standard MapLibre or Ramani Maps). This is likely to be due to graphics card incompatibilities with the OpenGL rendering system running on the virtual device.. The following appear to resolve the problem, so try all these:
OpenGL ES API level to Renderer maximum. As indicated, you then need to shutdown the device and re-launch it again.
As some of you did not complete Week 3, the exercises are relatively short this week.
Please add these imports to your project, you will need them. Normally Alt+Enter will import things for you, however listing them here will save you a bit of work.
import android.content.pm.PackageManager import android.location.Location import android.location.LocationListener import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.location.LocationManager import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import com.example.ramani_test.ui.theme.RamanitestTheme import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style import org.ramani.compose.MapLibre import org.ramani.compose.CameraPosition import org.ramani.compose.MapProperties import org.ramani.compose.Symbol
Text composables. When the GPS position updates, the Text composables should update. You will need to use runtime permission checking, and the Location API. Also use a ViewModel with LiveData and an observer.Symbol composable for this:
Symbol(center = latLng,
imageId = org.maplibre.android.R.drawable.maplibre_marker_icon_default,
size = 1f)