Chapters

Hide chapters

Android App Distribution

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

5. Permissions
Written by Fuad Kamal

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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.

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.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Allow background location permission
Ekqix kokpkweuzd xirejuov toncomgauz

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(...)
	}
}

Permissions best practices

Here are the best practices regarding permissions:

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.

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.

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

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.

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.

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.

@GET("/search?media=podcast")
fun searchPodcastByTerm(@Query("term") term: String): Call<PodcastResponse>
@GET(value = "/search?media=podcast")
  fun searchPodcastByTermAndCountry(@Query("term") term: String, @Query("country") country: String): Call<PodcastResponse>
fun searchByTermAndCountry(term: String, country: String, callBack: (List<ItunesPodcast>?) -> Unit) {

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

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.

private var searchTerm = ""
private val DEFAULT_COUNTRY = "US" // search United State, by default
<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>
private fun performSearch(searchTerm: String, searchCountry: String) {
  showProgressBar()
  searchViewModel.searchPodcasts(searchTerm, country = searchCountry) { results ->
    hideProgressBar()
    toolbar.title = searchTerm
    podcastListAdapter.setSearchData(results)
  }
}
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
      )
    }
  }
}
import android.Manifest
const val LOCATION_PERMISSION_REQUEST_CODE = 100
import com.raywenderlich.podplay.model.LOCATION_PERMISSION_REQUEST_CODE
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)
        }
      }
    }
  }
import android.provider.Settings
private fun handleIntent(intent: Intent) {
  if (Intent.ACTION_SEARCH == intent.action) {
    val query = intent.getStringExtra(SearchManager.QUERY) ?: return
    checkLocationPermissionAndSearch(query)
  }
  //..
}
Rationale toast message with link to settings app.
Suvaezeqe xuaxc rawgati qakc rolt fu qakfapvg utm.

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.

// Location Services
implementation "com.google.android.gms:play-services-location:18.0.0"
private lateinit var fusedLocationClient: FusedLocationProviderClient // Location Services Client
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)
          }
}
private fun checkLocationPermissionAndSearch(term: String) {
    when {
      checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED -> {
        searchUsingLocation(term)
      }
      //..
}
  override fun onRequestPermissionsResult(
      requestCode: Int,
      permissions: Array<String>,
      grantResults: IntArray
  ) {
    //..
    grantResults[0] == PackageManager.PERMISSION_GRANTED ->
      // Permission was granted.
      searchUsingLocation(searchTerm)
    //..
  }

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.

Updating your code

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

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.

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.

Key points

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

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now