Runtime Android Permissions were introduced in Android 6.0 (API 23), and starting from then, we could change the state of each permission for the application separately.

Android Runtime Permission dialog

I want to propose an analysis of a situation when permission is denied because it is often not tested with UI tests. The majority of UI tests check a case when required permissions are granted. However, it’s important to cover a scenario when permission is denied because some part of application functionality is unavailable, and we don't want to have a crash in this scenario.

Permission is dennied

We can have the following cases when we speak about runtime permissions:

  • all permissions are granted
  • some permissions are denied, and we provide information to the user about why we need them

Let's explore different ways of granting and revoking permission in UI test cases.

Interaction with Runtime permissions

We have different ways of interacting with Runtime permission in UI tests:

  • using a GrantPermissionRule
  • using an ABD command before installing the test application on a device
  • interacting with UI element of runtime permission dialog

All these approaches are helpful in different scenarios. Let's check out some other techniques and explore different scenarios when we can apply these approaches.

GrantPermissionRule

A GrantPermissionRule is a JUnit rule which simplifies granting permissions. When permissions are granted, it applies to all tests in the current instrumentation. If you try to revoke permissions, the instrumentation process will crash.

@get:Rule
val permissionRule = GrantPermissionRule.grant(
    android.Manifest.permission.ACCESS_FINE_LOCATION,
    android.Manifest.permission.ACCESS_NETWORK_STATE
)

Let's take a look at a practical example:

class MapNotesTests {

    @get:Rule
    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
        android.Manifest.permission.ACCESS_FINE_LOCATION,
        android.Manifest.permission.ACCESS_NETWORK_STATE
    )

    @Test
    fun should_displayMap_when_permissionsAreGranted() {
        ActivityScenario.launch(HomeActivity::class.java)

        onView(withId(R.id.map))
            .check(matches(isDisplayed()))
    }
}

We need to add the following dependency for using the GrantPermissionRule in test cases.

...

dependencies {
    ...
    androidTestImplementation "androidx.test:rules:1.3.0"
}

ADB Commands

Before running instrumentation tests, two applications will be installed on a device or emulator:

  • a debug version of an application
  • an application with instrumentation tests
Running instrumentation tests under the hood

We can grant and remove specific permissions via the ADB (Android Debug Bridge).

We can grant permission(s) with the adb shell pm grant PACKAGE PERMISSION command to grant permission for the package. To revoke permission(s), we can use the adb shell pm revoke PACKAGE PERMISSION command to revoke permission for the package.

So, let's add ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions to the application with the com.alexzh.mapnotes package.

adb shell pm grant com.alex.mapnotes android.permission.ACCESS_FINE_LOCATION
adb shell pm grant com.alex.mapnotes android.permission.ACCESS_COARSE_LOCATION

To revoke the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions, we can use the following command.

adb shell pm revoke com.alex.mapnotes android.permission.ACCESS_FINE_LOCATION
adb shell pm revoke com.alex.mapnotes android.permission.ACCESS_COARSE_LOCATION

Finally, we can check which permissions our application has:

adb shell dumpsys package com.alexzh.mapnotes | grep permission

This approach can be helpful when we want to run a set of tests with/without specific permissions.

UI Interaction

The last approach is to interact with the permission dialog.

Android Runtime Permission dialog

Note: We should remember that the permission dialog is not a part of an application; it’s a part of Android OS, meaning that we cannot interact with it using the Espresso framework. We should use a UiAutomator framework (or UiAutomator based framework) to interact with runtime dialog.

Let's analyze the UI of permission dialog in Android Marshmallow (API 23), Android 9 (API 28), Android 10 (API 29) and Android 11 (API 30) versions to understand changes in this dialog during recent releases of Android. We can use UiAutomatorViewer for checking text and index of the "ALLOW" buttons.

Runtime permission dialog: Android Marshmallow vs Android 9 vs Android 10
ALLOW button text ALLOW button index
Android Marshmallow (API 23) Allow 2
Android 9 (API 28) ALLOW 1
Android 10 (API 29) Allow only while using the app 0
Android 11 (API 30) While using the app 0

As you can see, the index and text of the "ALLOW" are different between devices with API 23 and 28. However, the devices with Android 10+ has the "Allow only while using the app" button. We need to make some adjustments depending on the Android version if we want to make our code workable on different devices/emulators.

Let's create a function for enabling permissions using the UiAutomator framework.

fun grantPermission() {
    val instrumentation = InstrumentationRegistry.getInstrumentation()
    if (Build.VERSION.SDK_INT >= 23) {
    val allowPermission = UiDevice.getInstance(instrumentation).findObject(UiSelector().text(
        when {
            Build.VERSION.SDK_INT == 23 -> "Allow"
            Build.VERSION.SDK_INT <= 28 -> "ALLOW"
            Build.VERSION.SDK_INT == 29 - > "Allow only while using the app"
            else -> "While using the app"
        }
    ))
    if (allowPermission.exists()) {
        allowPermission.click()
    }
}

We can use a similar approach to deny specific permissions when it's needed.

fun denyPermission() {
    val instrumentation = InstrumentationRegistry.getInstrumentation()
    if (Build.VERSION.SDK_INT >= 23) {
        val denyPermission = UiDevice.getInstance(instrumentation).findObject(UiSelector().text(
            when {
                Build.VERSION.SDK_INT in 24..28 -> "DENY"
                else -> "Deny"
            }
        ))
        if (denyPermission.exists()) {
            denyPermission.click()
        }
    }
}

Clean permissions

We explored different approaches to granting permissions, but sometimes we want to deny permission for a specific test even if previous test cases’ permissions were already granted. We can do this with the "Android Test Orchestrator".

We need to add changes to the build.gradle file:

android {
  defaultConfig {
   ...
   testInstrumentationRunnerArguments clearPackageData: 'true'
 }

  testOptions {
    execution 'ANDROIDX_TEST_ORCHESTRATOR'
  }
}

dependencies {
  androidTestUtil 'androidx.test:orchestrator:1.1.0'
}

The clearPackageData: 'true' helps us clear memory and CPU data for our application. This means that we can clear permission before each test case.

Combine approaches

Let's check out two different scenarios:

  • ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions were granted, and map is displayed
  • ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permission were denied, and the "No Location permission" screen is displayed
App screens

The MapNotesTests class has two cover both of these test cases:

@RunWith(AndroidJUnit4::class)
class MapNotesTests {

    @Test
    fun should_displayMap_when_permissionsAreGranted() {
        ActivityScenario.launch(HomeActivity::class.java)

        grantPermission()
        
        onView(withId(R.id.map))
            .check(matches(isDisplayed()))
    }

    @Test
    fun should_displayNoPermission_when_permissionsAreDenied() {
        ActivityScenario.launch(HomeActivity::class.java)

        denyPermission()
        
        onView(withId(R.id.permissionExplanation))
                .check(matches(withText(R.string.permission_explanation)))
    }
}

In this case, we can grant and deny permission by interacting with UI elements of "Permission Dialog."

Note: We need to remember to configure "Android Test Orchestrator" for cleaning application data. However, in this case, the execution time will increase.

Summary

As a result, we can check how our application works with/without permission(s). In addition to this, we can verify what a user sees when permission isn't granted and how a user can grant it.

The Android Test Orchestrator allows us to clear all permissions before each test case.

The UiAutomator or UiAutomator based framework allows us to interact with the Runtime Permission dialog via UI.

The GrantPermissionRule allows us to grant permission to instrumentation.

The ADB commands allow us to grant and revoke permission(s) before executing UI test cases.

We can verify interaction with permission dialogs if we combine UI interacting with the Runtime Permission Dialog, GrantPermissionRule, and Android Test Orchestrator library.

Resources