Location Access Restrictions

One of the bigger privacy issues in Android is the availability of location data. Users like certain types of apps, such as navigation assistants, knowing where the user is. Users do not like arbitrary use of location data, though, and various ad networks and malicious apps have been harvesting location data inappropriately.

So, in Android 10, location access gets locked down even further than before.

Background Location Access

The change that will get the most attention is that there are new limitations on getting location data in the background. There are two main scenarios for this:

  1. The app had been in the foreground, but the user switches to another app. For example, the user might be using a navigation app but then receive a phone call, at which point the device UI switches to an in-call screen. Ideally, the navigation app will continue to receive location data, despite being (temporarily) in the background… but in Android 10, this requires a bit of additional work.
  2. The app is operating purely in the background (e.g., JobScheduler jobs) and wants to get the user’s location. This requires an additional permission on Android 10.
You can learn more about LocationManager in the "Accessing Location-Based Services" chapter of The Busy Coder's Guide to Android Development!

Started from Foreground

Your app might mostly need locations in the foreground, but its UI might be moved to the background based on user interactions. You might want to keep getting the location updates while your UI is not in the foreground, so when you do return to the foreground, you have up-to-date location data.

The recommended pattern to make this work is to start a foreground service when your app moves to the background, where that service has android:foregroundServiceType="location" on its <service> manifest element. Then, the service can continue receiving notification updates, even though the UI is not in the foreground.

The LocationForeground sample module in the book’s sample project illustrates this process.

The app has an activity that displays the latitude, longitude, and fix time of the latest GPS fix. It gets those from a LocationRepository that exposes the location updates via LiveData:

package com.commonsware.android.q.loc.fg

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

class LocationRepository(private val context: Context) {
  private val _locations = MutableLiveData<Location>()
  val locations: LiveData<Location> = _locations
  private var locationsRequested = false

  init {
    initRequest()
  }

  fun initRequest() {
    if (!locationsRequested) {
      val mgr = context.getSystemService(LocationManager::class.java)

      if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
        PackageManager.PERMISSION_GRANTED
      ) {
        locationsRequested = true
        mgr.requestLocationUpdates(
          LocationManager.GPS_PROVIDER,
          0,
          0.0f,
          object : LocationListener {
            override fun onLocationChanged(location: Location) {
              _locations.postValue(location)
            }

            override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) {
              // unused
            }

            override fun onProviderEnabled(p0: String?) {
              // unused
            }

            override fun onProviderDisabled(p0: String?) {
              // unused
            }
          })
      }
    }
  }
}

The activity has a viewmodel that gets the LocationRepository (via Koin-supplied dependency injection), and the activity gets the data to provide to data binding from that viewmodel.

This works great when the UI is in the foreground. However, we also want to ensure that LocationRepository can continue getting location data when the UI moves to the background.

For that, we have a ForegroundService. Not surprisingly, ForegroundService is a foreground service. It too gets the LocationRepository and dumps the latitude and longitude to Logcat:

package com.commonsware.android.q.loc.fg

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.Observer
import org.koin.android.ext.android.inject

private const val CHANNEL_WHATEVER = "channel_whatever"
private const val FOREGROUND_ID = 1338

class ForegroundService : LifecycleService() {
  private val repo: LocationRepository by inject()

  override fun onCreate() {
    super.onCreate()

    val mgr = getSystemService(NotificationManager::class.java)!!

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
      mgr.getNotificationChannel(CHANNEL_WHATEVER) == null
    ) {
      mgr.createNotificationChannel(
        NotificationChannel(
          CHANNEL_WHATEVER,
          "Whatever",
          NotificationManager.IMPORTANCE_DEFAULT
        )
      )
    }

    startForeground(FOREGROUND_ID, buildForegroundNotification())

    repo.locations.observe(this, Observer {
      Log.d(
        "LocationForeground",
        "Latitude: ${it.latitude} Longitude: ${it.longitude}"
      )
    })
  }

  private fun buildForegroundNotification(): Notification {
    val pi = PendingIntent.getBroadcast(
      this,
      1337,
      Intent(this, StopServiceReceiver::class.java),
      0
    )
    val b = NotificationCompat.Builder(this, CHANNEL_WHATEVER)

    b.setOngoing(true)
      .setContentTitle(getString(R.string.app_name))
      .setContentText(getString(R.string.notif_text))
      .setSmallIcon(R.drawable.ic_notification)
      .setContentIntent(pi)

    return b.build()
  }
}

class StopServiceReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context, intent: Intent) {
    context.stopService(Intent(context, ForegroundService::class.java))
  }
}

Its manifest entry has the new android:foregroundServiceType attribute:

    <service
      android:name=".ForegroundService"
      android:foregroundServiceType="location" />

This attribute is used to tell Android that the service:

Then, to start and stop the service, we leverage ProcessLifecycleOwner from the Architecture Components. This lets us know when our UI comes to the foreground or moves to the background overall. In this case, we have only one activity, but most apps have more than one activity, so ProcessLifecycleOwner will be a better choice, as it reports the overall foreground/background status, not just for a single activity. So, our custom Application subclass (KoinApp), in addition to setting up Koin dependency injection, also registers a DefaultLifecycleObserver to find out about the UI state changes:

package com.commonsware.android.q.loc.fg

import android.app.Application
import android.content.Intent
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import org.koin.android.ext.android.startKoin
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.ext.koin.viewModel
import org.koin.dsl.module.module

class KoinApp : Application() {
  private val koinModule = module {
    single { LocationRepository(androidContext()) }
    viewModel { MainMotor(get()) }
  }

  override fun onCreate() {
    super.onCreate()

    startKoin(this, listOf(koinModule))

    ProcessLifecycleOwner.get()
      .lifecycle
      .addObserver(object : DefaultLifecycleObserver {
        override fun onStart(owner: LifecycleOwner) {
          stopService(Intent(this@KoinApp, ForegroundService::class.java))
        }

        override fun onStop(owner: LifecycleOwner) {
          startForegroundService(Intent(this@KoinApp, ForegroundService::class.java))
        }
      })
  }
}

When the UI moves to the background, we start the ForegroundService. When the UI moves to the foreground, we stop the ForegroundService (even if the service was not necessarily started, as stopService() does not crash or anything if you do that).

This “run the service while the UI is in the background” approach works reasonably well… except that it always starts this service, which may include some times when the user does not really want it. For the purposes of the book sample, the notification itself will stop the service if the user clicks on it. A production-grade app may need greater sophistication here.

However, for the purposes of the Android 10 problem, we are able to continue receiving location updates, even when our UI is no longer in the foreground.

Requested from Background

Doing work purely in the background is difficult on modern versions of Android, owing to all the changes related to Doze mode and similar features. Getting location data purely in the background already was a pain, as some of the preferred background options — such as WorkManager — do not integrate well with asynchronous APIs like we have with the location APIs.

In light of that, Android 10’s changes are not a big deal.

There is a new permission, ACCESS_BACKGROUND_LOCATION, that you will need to request. This is a dangerous permission, so you not only need the <uses-permission> element for it in the manifest, but you need to request it at runtime. Since you are already requesting ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION, this additional permission just adds a bit of incremental code.

The user winds up with three basic options in terms of granting rights to your app:

The middle option will mean that your app will hold ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION (whichever you requested) but not ACCESS_BACKGROUND_LOCATION. And, if you do not hold ACCESS_BACKGROUND_LOCATION, you cannot obtain location data from the background, unless you originally were getting the locations in the foreground, as we saw in the preceding section.

However, this just means that your background code will need to check for this new permission and handle it the same as if the user revoked your ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION rights. Once again, this adds a bit of additional code, and it adds a new scenario (foreground location access but not background location access), but it should not cause much significant harm to your app’s functionality.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.