Chapters

Hide chapters

Android App Distribution

First Edition · Android 12 · Kotlin 1.5 · Android Studio Bumblebee

5. Permissions
Written by Fuad Kamal

In the previous section, you learned how to distribute and release your app. When you distribute your app, you may find one of your app’s critical functionalities requires explicitly asking the user for permission to access certain hardware features. Furthermore, when you release your app into production, you need to ensure your users’ data is secure. In this chapter, you’ll learn about different types of permissions, how to gain access to features that rely on those permissions and how to secure your users’ data.

Permissions

For security, you have to declare or request permission before using certain features on Android devices. Permissions protect access to restricted data and restricted actions. You declare permissions in the app manifest file. Certain types of permissions also require asking the user to actively grant them so your app can use them.

Permissions that only need to be declared in the app manifest and are granted automatically when your app is installed are called install-time permissions. The user sees a list of the install-time permissions your app requires on its app details page in the Play Store, but they won’t get UI prompts from the app about them.

Play Store permissions listing for Google Maps.
Play Store permissions listing for Google Maps.

Permissions that require authorization from the user at runtime are known as runtime permissions, or dangerous permissions. When you request a dangerous permission from the user, the system prompts them with a popup permission request.

In-app permissions popup for Google Meets.
In-app permissions popup for Google Meets.

You need to keep some best practices and user experience design, or UXD, in mind when working with dangerous permissions. Also, while there are many types of permissions, the permissions ecosystem changes from version to version of Android OS. So, be sure to check the official documentation regularly.

Dangerous permissions

Android OS presents the user with a UI prompt whenever your app requests a runtime permission. You can ruin user experience by requesting permissions that don’t make sense to the user or at the wrong time. So it’s critical to follow Permissions best practices. To learn how, you’ll update Podplay with some interesting features.

Until now, Podplay only searched for podcasts against the iTunes Store in the U.S. You review your app’s analytics in the Play Console and realize your user base covers the globe. Wouldn’t it be great if Podplay searched for podcasts in the user’s country?

The app needs location permission, which is considered a dangerous permission, to search based on the user’s location.

App manifest permissions

There are some common types of permissions your app might need to request. For a complete and current list of all Android app permissions, refer to the permissions API reference https://developer.android.com/guide/topics/permissions/overview.

Apps commonly need permissions such as access to the camera, microphone, location, networking information, Bluetooth access, various types of scoped storage and biometrics access. Also, be aware of permissions you need for any third-party libraries you use. For Podplay, you’ll only need to add the coarse location permission.

Location permissions

Apps that need to know a user’s location need location permissions. According to permissions best practices, you should minimize the data you collect from users.

You can request two levels of location accuracy: coarse location and fine location. If you don’t require the user’s precise location, you should request the course location permission. However, if you made an app that provided turn-by-turn directions, you’d need the fine location permission since you’d need to know the user’s precise location to alert them to the next turn.

To access background location in your app, you must first obtain a foreground location permission: either ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION. Otherwise, the system will throw an exception.

You’ll get into how to ask the user for permission in a moment. But first, you need to list each of the permissions your app requires in the app manifest file.

Open AndroidManifest.xml for Podplay and add the following permission before the application tag:

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

Since Android 11, you can’t get background location permission from an in-app dialogue. Instead, you must explain the feature which requires the background location using an in-context UI.

If the user chooses to permit access, they’re directed to the system settings to grant permission. The user can only choose the Allow all the time option for the location permission on this settings page, as shown below:

Allow background location permission
Allow background location permission

Build and run. Everything works as before. Declaring the permission in the manifest doesn’t make the app ask the user for permission. You’ll learn how to do that next.

Asking for permission

Here’s the recommended code block for checking for permissions and requesting them if necessary:

when {
	checkSelfPermission(...) == GRANTED -> {
		// Perform action.
	}
	shouldShowRequestPermissionRationale(...) -> {
		// Show in-context rationale.
	}
	else -> {
		requestPermissions(...)
	}
}

This is the recommended flow to handle runtime permissions:

  1. Check whether the app holds a given permission before performing any action that requires it. Unlike iOS, in Android, the developer can always perform this check successfully regardless of the permission type. This check shouldn’t be cached or stored since it can get out of sync with the system state. A user can always go into the device settings and deny a permission they previously granted. Furthermore, in Android 11 and later, the system will automatically revoke all runtime permissions for apps the user hasn’t used for some time.

  2. Fail gracefully in cases where the user declines a permission.

Permissions best practices

Here are the best practices regarding permissions:

  1. Request minimum permissions.
  2. Only ask for access in context when the user starts to interact with the feature that requires it.
  3. Don’t block the user. Always provide the option to cancel an educational UI flow related to permissions.
  4. Plan for users to select deny.
  5. Don’t access data when the user doesn’t expect it.
  6. Pay attention to libraries.

Lets take a look at each of these best practices in a bit more detail.

Requesting minimum permissions

Only use the permissions necessary for your app to work. Every time you ask users for permission, you require them to make a decision, increasing the burden on the user.

Look for alternatives to common use cases that may help you limit the number of permissions you ask. For example, you might consider system intents, identifiers and background for phone calls.

Be mindful of your deny rate. If you’re distributing your app through Google Play, the Android Vitals section reports the percentage of users who deny permissions in your app. Use that to assess whether or not you should rethink your permissions strategy.

Asking for access in context

Ask for permissions only when your app needs to access it for the first time. Be transparent with your users about why you need the permission.

You want to design the UX of your app to feel natural and logical for your users to grant the requested permission, rather than abrupt, forced or accidental. If the user understands why they need to give a certain permission to carry out some task with your app, they’re much more likely to grant the permission.

Once the user clicks the feature that needs the permission, first check if they granted permission by passing the permission into the checkSelfPermissions as you saw earlier:

when {
	checkSelfPermission(...) == GRANTED -> {
		// Perform action.
	}...

This method will return permission granted, or permission denied. If the permission returned is denied, you call shouldShowRequestPermissionRationale to decide whethet to show the user a UI requesting the permission.

In this UI, you need to explain clearly why the app needs the permission. You also need to give the user the option to deny the permission request before seeing a runtime prompt. Never use the runtime prompt to capture the user’s decision to grant the permission. Instead, the runtime prompt is meant as a system confirmation that the user wants to use a specific feature in your app and therefore is willing to give you app access to their private data.

Planning for denied requests

There’s no guarantee the user will grant the permission you request. You should always write the logic of your permission request in a way that assumes the user may deny the request and act accordingly. If the user denies or revokes a permission that a feature needs, gracefully degrade your app so the user can continue using your app, possibly by disabling the feature that requires the permission.

Keep these best practices in mind:

  1. Expect that the user will deny the permission and gracefully degrade when that happens. Don’t block users from using the app unless the permission is critical to its functionality.
  2. Expect that the user might permanently deny the permission. To prevent this, make sure to ask for the permission in context and let the user deny the feature within your app’s UI.
  3. Use shouldShowRequestPermissionRationale properly, in case the user has selected “Only this time” or in case the system has automatically revoked the permission.

If the user denies a permission request, your app should help them understand the implications of denying the permission. In particular, your app should make the user aware of the features that won’t work because of the missing permission. When you do, keep the following best practices in mind:

  • Guide the user’s attention. Highlight a specific part of your app’s UI where there’s limited functionality because your app doesn’t have the necessary permission. For example, you could show a message where the feature’s results or data would have appeared. Or you could display a different button containing an error icon and color.
  • Be specific. Don’t display a generic message. Instead, mention which features are unavailable because your app doesn’t have the necessary permission.
  • Don’t block the user interface. In other words, don’t display a full-screen warning message that prevents users from continuing to use your app at all.

At the same time, your app should respect the user’s decision to deny a permission. Starting in Android 11, API level 30, if the user taps Deny for a specific permission more than once during your app’s lifetime of installation on a device, the user won’t see the system permissions dialog when your app requests that permission again. The user’s action implies “don’t ask again.” On previous versions, users would see the system permissions dialog each time your app requested a permission unless the user had previously selected a “don’t ask again” checkbox or option.

In certain situations, the permission might be denied automatically, without the user taking any action. Similarly, a permission might be granted automatically as well. It’s important not to assume anything about automatic behavior. Each time your app needs to access functionality that requires a permission, you should check that your app is still granted that permission.

One-time permissions

System dialog that appears when an app requests a one-time permission.
System dialog that appears when an app requests a one-time permission.

Starting in Android 11, API level 30, whenever your app requests a permission related to location, microphone or camera, the user-facing permissions dialog contains an option called Only this time, as shown above. If the user selects this option, they grant your app a temporary one-time permission. Your app can then access the related data for a period of time that depends on your app’s behavior and the user’s actions:

  • While its activity is visible, your app can access the data.
  • If the user sends your app to the background, it can continue to access the data for a short time.
  • If you launch a foreground service while the activity is visible, and the user moves your app to the background, your app can continue to access the data until that foreground service stops. You must note this condition. Later, you’ll see how to leverage this to get rid of background location services.
  • If the user revokes the one-time permission, such as in system settings, your app cannot access the data, regardless of whether you launched a foreground service. As with any permission, if the user revokes your app’s one-time permission, your app’s process terminates. When the user next opens your app, and it requests access to location, microphone or camera, the user is prompted for the permission again.

Don’t access private data unexpectedly

Pay attention to the time and frequency you’re accessing user data to ensure it aligns with what users expect. You must provide continuous indication for mic and camera whenever you access these sensitive capabilities.

Updating Podplay to search by location

Now that you understand the best practices and UX of requesting permissions, you’re ready to update Podplay to search for podcasts based on the user’s location.

iTunes Search API

Podplay uses the iTunes Search API to search for podcasts.

According to Apple’s developer documentation for this API, the fully-qualified URL for the API has the following format:

https://itunes.apple.com/search?parameterkeyvalue.

Here, parameterkeyvalue can be one or more parameter key and value pairs indicating your query’s details.

Under service, open ItunesService.kt. You can see a companion object, ItunesService, that defines the base URL for this API. There’s also a definition for searchPodcastByTerm, which takes the search term, typed in by the user, and appends it to the base URL:

@GET("/search?media=podcast")
fun searchPodcastByTerm(@Query("term") term: String): Call<PodcastResponse>

Podplay already sends parameter term. Also, note @GET above the function signature, which defines the media parameter key. Now, you need to add an additional parameter key, country, for the user’s country.

The value for this key is the country code, which you can derive directly from the location service you’ll implement. To keep with coding best practices, rename the function to keep it descriptive of what it does. Change the searchPodcastByTerm() function to:

@GET(value = "/search?media=podcast")
  fun searchPodcastByTermAndCountry(@Query("term") term: String, @Query("country") country: String): Call<PodcastResponse>

Alternatively, you could use @QueryMap instead of @Query. To keep things simple, adding another @Query is fine here.

Next, under repository, open ItunesRepo.kt. Again, you need to add the country parameter and change the function’s name to reflect this change in parameters. Change the function signature of searchByTerm and podcastCall to:

fun searchByTermAndCountry(term: String, country: String, callBack: (List<ItunesPodcast>?) -> Unit) {

    val podcastCall = itunesService.searchPodcastByTermAndCountry(term, country)

Finally, under viewmodel, open SearchViewModel.kt and update searchPodcasts() like this:

fun searchPodcasts(term: String, country: String, callback: (List<PodcastSummaryViewData>) -> Unit) {
    iTunesRepo?.searchByTermAndCountry(term, country) { results ->

You added the country code to the search functionality. Now, you need to determine which country the user is in to provide the right country code.

Implementing location service requests

As you know, you don’t want to access your user’s location until necessary. Hence, you don’t ask for location permissions as soon as the app starts. That would surprise the user and possibly lead them to deny the request outright.

When do you need to access their location? You’ll need the user’s country code to append your search parameter. So, wait until the user taps the search icon to search for a podcast. First, check if the user already granted permission. If they haven’t, then request the permission.

The search takes place in PodcastActivity.kt’s performSearch(). First, add the following two members to PodcastActivity:

private var searchTerm = ""
private val DEFAULT_COUNTRY = "US" // search United State, by default

Here, you move searchTerm to the class scope, so more than one function in the class can access it. DEFAULT_COUNTRY contains the default country code US. You provide a default value for the country code to avoid breaking the app’s existing functionality if the location can’t be determined or the user denies the permission.

Add the following localized string definitions to strings.xml:

<string name="permission_rationale">Select Allow Location in order to search for podcasts local to you. 🌎🌍🌏</string>
<string name="permission_denied_explanation">Location permission was denied, but is needed for searching in context of your country. Searches will use default country unless you grant the permission.</string>
<string name="ok">OK</string>
<string name="settings">Settings</string>

The code above defines strings you’ll use in the prompt shown to the user to request the location permission.

Next, head back to PodcastActivity.kt and update performSearch() to include a parameter for the search country, as shown below:

private fun performSearch(searchTerm: String, searchCountry: String) {
  showProgressBar()
  searchViewModel.searchPodcasts(searchTerm, country = searchCountry) { results ->
    hideProgressBar()
    toolbar.title = searchTerm
    podcastListAdapter.setSearchData(results)
  }
}

In the code above, you also update the method invocation of searchViewModel.searchPodcasts() to include the searchCountry parameter.

Next, add the following method to the same Activity:

private fun checkLocationPermissionAndSearch(term: String) {
  searchTerm = term
  when {
    // 1
    checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED -> {
      // Permission granted. Proceed to use the location.
      performSearch(term, DEFAULT_COUNTRY)
    }
    // 2
    // If the user denied a previous request, but didn't check "Don't ask again", provide
    // additional rationale.
    shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) -> {
      // In an educational UI, explain to the user why your app requires this
      // permission for a specific feature to behave as expected. In this UI,
      // if possible, include a "cancel" or "no thanks" button that allows the user to
      // continue using your app without granting the permission.
      Snackbar.make(
              podcastDetailsContainer,
              R.string.permission_rationale,
              Snackbar.LENGTH_LONG
      )
              .setAction(R.string.ok) {
                // Request permission
                ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
                        LOCATION_PERMISSION_REQUEST_CODE
                )
              }
              .show()
    }
    else -> {
      // 3
      // Display the system permissions dialog when necessary
      Log.d("PodcastActivity", "Request location permission")
      ActivityCompat.requestPermissions(
              this,
              arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
              LOCATION_PERMISSION_REQUEST_CODE
      )
    }
  }
}

Also, the following import for Manifest:

import android.Manifest

The code above does the following:

  1. First, if the user previously granted the permission, the search will execute as before. However, if they haven’t granted permission, for example, if this is the user’s first search, it won’t be performed yet.

  2. Next, you make the call to shouldShowRequestPermissionRationale, which returns a Boolean. If it returns true, you show the user an educational UI to explaining why you need the location permission. Note that typically, if this condition returns true, it’s because the user previously denied this permission request from this app. If you keep trying to request the permission, your permission request won’t be shown after the second time. Therefore, it’s better to explain to the user why you need this permission. Also, note that for simplicity’s sake, in these examples you’ll use snackbar notifications. However, if you need to provide a detailed explanation or additional buttons for a more graceful user experience, you should show a modal popup instead.

  3. If the user hasn’t already granted permission, and the system isn’t reporting that you need to display a rationale, then you make the permission request.

The IDE will warn you that LOCATION_PERMISSION_REQUEST_CODE hasn’t been defined. Create a new file named Constants.kt inside model. Add the following code to it:

const val LOCATION_PERMISSION_REQUEST_CODE = 100

Go back to PodcastActivity.kt and add the following import for LOCATION_PERMISSION_REQUEST_CODE :

import com.raywenderlich.podplay.model.LOCATION_PERMISSION_REQUEST_CODE

Since you added code to request permission, you also need to handle the premission request’s outcome.

Add the following method to the same Activity:

override fun onRequestPermissionsResult(
          requestCode: Int,
          permissions: Array<String>,
          grantResults: IntArray
  ) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
      LOCATION_PERMISSION_REQUEST_CODE -> when {
        grantResults.isEmpty() ->
          // If user interaction was interrupted, the permission request
          // is cancelled and you receive empty arrays.
          Log.d("PodcastActivity", "User interaction was cancelled.")

        grantResults[0] == PackageManager.PERMISSION_GRANTED ->
          // Permission was granted.
          performSearch(searchTerm, DEFAULT_COUNTRY)

        else -> {
          // Permission denied.
          Snackbar.make(
                  podcastDetailsContainer,
                  R.string.permission_denied_explanation,
                  Snackbar.LENGTH_LONG
          )
                  .setAction(R.string.settings) {
                    // Build intent that displays the App settings screen.
                    val intent = Intent()
                    intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                    val uri = Uri.fromParts(
                            "package",
                            BuildConfig.APPLICATION_ID,
                            null
                    )
                    intent.data = uri
                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    startActivity(intent)
                  }
                  .show()
          performSearch(searchTerm, DEFAULT_COUNTRY)
        }
      }
    }
  }

Also, add the following import for Settings:

import android.provider.Settings

Note: If you are unable to find an import for BuildConfig, build the app once. It will generate the required BuildConfig.java and you’ll be able to import it.

In the code above:

  1. You check whether the user granted the location permission. If they did, you continue with the search. For now, you use the default country. You’ll change that shortly.
  2. If the user denied the permission, you show a SnackBar with an explanation and continue the search using the default country. This lets the user continue using the app even without granting the permission.

Finally, replace the invocation of performSearch inside handleIntent() with checkLocationPermissionAndSearch as shown in the code below:

private fun handleIntent(intent: Intent) {
  if (Intent.ACTION_SEARCH == intent.action) {
    val query = intent.getStringExtra(SearchManager.QUERY) ?: return
    checkLocationPermissionAndSearch(query)
  }
  //..
}

Build and run. Even though you aren’t fetching the user’s location yet, you ask for location permissions. Note: if you don’t see the prompt asking for permissions un-install and re-install the app on your device. Search for some podcasts. Experiment with allowing and denying the location permissions.

You can go into the device’s Settings to change the permissions for the app if you denied permission enough times that you no longer receive a permissions prompt. Additionally, you can set breakpoints in the code in the different permission scenarios to see how and when they’re called. Tap the links in the toast messages to test the functionality of opening Settings to the permissions settings for Podplay.

Rationale toast message with link to settings app.
Rationale toast message with link to settings app.

Fetching location

There’s only one piece left to this puzzle: determine the user’s country. First you’ll ask the system for the user’s last known location. You’ll then use that location to perform a technique known as reverse geocoding.

Reverse geocoding is transforming a latitude, longitude coordinate into a partial address. The amount of detail in a reverse geocoded location description varies. For example, one might contain the closest building’s full street address, while another might contain only a city name and postal code.

You’ve already updated the manifest file for the coarse location permission. Now you also need to update the app Gradle file to use Google Play location services. Add the following to the dependencies block in the app buildgradle and sync the Gradle file:

// Location Services
implementation "com.google.android.gms:play-services-location:18.0.0"

Almost done! Add the following class level variabl to PodcastActivity.kt:

private lateinit var fusedLocationClient: FusedLocationProviderClient // Location Services Client

Then add the following method in PodcastActivity.kt:

private fun searchUsingLocation(searchTerm: String) {
  //Create Location Services Client
  fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

  // 1
  if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
    return
  }
  // 2
  fusedLocationClient.lastLocation
          .addOnSuccessListener { location : Location? ->
            // Got last known location. In some rare situations this can be null.
            location?.let {
              // 3
              val geoCoder = Geocoder(this)
              val addresses = geoCoder.getFromLocation(location.latitude,location.longitude, 1)
              val searchCountry = addresses[0].countryCode
              performSearch(searchTerm, searchCountry)
            } ?: run {
              performSearch(searchTerm, DEFAULT_COUNTRY)
            }
          }.addOnFailureListener {
              performSearch(searchTerm, DEFAULT_COUNTRY)
          }
}

The code above does a few things:

  1. It might seem redundant to check for location permissions again. After all, you never get to this function without first doing a permissions check. However, if you don’t make the check here, you’ll get a rather annoying compiler warning from Android Studio. You could also argue that always checking permissions before trying to use a dangerous permission is a best practice.

  2. Both in the Android and iOS ecosystems, hardware devices are extremely well engineered when it comes to location services. Hence, location services on both platforms provide an API that relies on the combined smarts of the OS, processor and all the various sensors including magnetometer, GPS, accelerometer, WiFi, and cell phone antennae to provide what is known as “fused sensors”. Fused sensors are a virtual construct that’s more dynamic and versatile than any of the sensors on their own. It’s more reliable for the programmer to simply use the fused sensors rather than try to access raw data from individual sensors.

    Furthermore, with “dangerous” location permissions on Android, if you don’t require the user’s exact location, you’re only supposed to access “coarse location”, which is the permission you implemented previously. Where and how the location is determined will vary depending on the device’s sensors, what data is available from each and so on. As a programmer, you don’t need to care about those details. The FusedLocationProviderClient provides a convenient callback that will return a Location object when it’s available. This, in turn, provides an array of addresses.

  3. You then rely on yet another convenient built-in class on Android, Geocoder, which provides geocoding and reverse geocoding for you. You provide it with a latitude and longitude from the Location object, and it provides the country code.

Next, change the first invocation of performSearch in checkLocationPermissionAndSearch to searchUsingLocation as shown below:

private fun checkLocationPermissionAndSearch(term: String) {
    when {
      checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED -> {
        searchUsingLocation(term)
      }
      //..
}

Finally, change the invocation of performSearch in onRequestPermissionsResult() in the condition where permission is granted, as follows:

  override fun onRequestPermissionsResult(
      requestCode: Int,
      permissions: Array<String>,
      grantResults: IntArray
  ) {
    //..
    grantResults[0] == PackageManager.PERMISSION_GRANTED ->
      // Permission was granted.
      searchUsingLocation(searchTerm)
    //..
  }

Voila! Your app now provides the search query with the proper country code. Build and run. Search for some podcasts!

Finding background location use

Android 10 and 11 give users better privacy controls for fine-grain location permissions. One of the biggest changes is the separation of foreground location access, or while-in-use access, from background location access, or all-the-time access.

In Android 11, you can’t request both permissions at the same time. Whenever possible, Google recommends you only rely on foreground access for your location data.

If your app or the libraries it uses require background location, determine if you can switch to foreground location instead.

Updating your own code is straightforward, but you need to be mindful of permissions required by third-party libraries you’re using.

Updating your code

Find any location APIs in your code to determine if they’re used in the background.

  1. Search through your code and look at your use of location services. This part is pretty straight forward. You’re just reviewing your own code and figuring things out.
  2. Next, examine how your app uses location. Specifically, do you really need background location? You should evaluate whether background location access is critical to your app’s core functionality. You can realize many use cases with only foreground location. If you don’t need location access in the background, either migrate to foreground location access or remove it altogether. In both cases, it’ll simplify your app and codebase. Keep in mind, for Android 10 and later, you need to remove the ACCESS_BACKGROUND_LOCATION permission from the manifest.
  3. If you determine background location is critical, make sure to follow the best practices and review Google’s current policies on location services.

Suppose you want to migrate some background location service code to foreground location instead. What are your options?

  1. You could only retrieve location while your activity is viewable. This is the most common approach. So, don’t request location data, like “get last location” when your app isn’t visible. Update your code so it stops listening to location updates whenever the UI goes out of view.
  2. There’s another option for foreground location. Your app can retrieve location via a foreground service without the background location permission. This is a little more difficult to implement and debug. For example, you can have a navigation app that only gets updates using the foreground location service. Here, the trick is to transition the service to a foreground service and tie it to a notification. That way, your app is still in use, and you can continue to get location updates without background location access. However, it’s important to note that you shouldn’t replace all code retrieving location in the background with this approach.

When your app is paused, the trick is to transition the location service being used in the app as a foreground service to a foreground service that’s instead tied to a notification. That way, your app is still “in use” and you can continue to get location updates without background location access.

  1. First, determine if you should retrieve location data in that instance. In most cases, you should only request location data in the foreground if the user-initiated an action that requires location, like navigating to another place.
  2. If you take the time to architect things with proper separation of concerns, all your location code should be separated. That way, you’ll know that when the subscriber unbinds from the class using location services, that the activity is going away since it’s no longer visible. So if they actively subscribe to location changes and you get an unbind request, you know you need to transition the service to a foreground service. In that case, you create a notification and then transition it to a foreground service. Then, if the activity comes back and re-binds to this class, you transition it back to a foreground service.

You’re using a service to retrieve the activity’s location via binding. Then you promote that service to a foreground service for the notification when the activity is no longer visible. That way, you still get location changes, even when the app isn’t technically visible. It’s within the notification, but you don’t have an activity visible, and you don’t need background permissions.

For an example of how to implement this type of switching between a foreground location service and notifications, see this Google Codelab: https://codelabs.developers.google.com/codelabs/while-in-use-location/?hl=ko#2

Finding permissions required by third-party libraries

You inherit permissions every time you include a third-party library in your code. Users will generally attribute those permissions to your app, not to the library.

If you need to figure out which library requires a specific permission, you can use two APIs Google introduced in Android 11.

The first API is a callback on data access. This API tells the system to backtrace an app’s specified callback each time the app accesses sensitive data. The callback provides various information, including the type of data being accessed, a stack trace, the frequency and time of data access. For more information on this API, see the official documentation https://developer.android.com/guide/topics/data/audit-access.

The second API is feature tagging. It lets developers attribute access to logical features within their app by tagging certain parts of the app. You can learn more about this API at https://developer.android.com/guide/topics/data/audit-access#audit-by-attribution-tag .

Simplifying multiple permission requests with Android Jetpack

The code and techniques you used so far are fine when you only need to request one or two permissions. But some apps need access to multiple permission types. Using requestPermissions() and then checking the request code in onRequestPermissionsResult() for many different types of permissions can start to make your code complex.

Alternatively, with Jetpack and AndroidX, you can request permission using an activity result registry with the request permission result contract. This removes the need for request codes and overwriting activity APIs. However, it does introduce a bit of its own complexity, and requires you to add yet another Gradle dependency to your app. But, if your app requires many permissions, or if you’re using ActivityResult API for some of the many other features it offers, it might be something to look into.

Key points

You covered quite a lot in this chapter! Here’s a quick recap of some of the salient points:

  • Never ask your users for permissions out of context. Wait until you need to access the feature which requires the permission, and then clearly explain why you need it.
  • Try to fail gracefully or work around lack of permissions, if you can.
  • Architect your apps in a way that plans for the user to deny you permissions.
  • Third-party APIs can introduce their own permissions dependencies, but your users will assume this is coming from your app. They don’t know or care about third-party libraries. It’s good to debug both your app and your library dependencies to see where your user’s data is being accessed.

Where to go from here?

In this chapter, you got hands-on experience with some location services and the permissions around them. However, your app many need access to many other kinds. See Saving Data on Android for all the many ways you can access data on a device, including disk and database access, network access and SharedPreferences, which you’ll touch on from the security standpoint in the next chapter. You can find the book here: https://www.raywenderlich.com/books/saving-data-on-android/.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.