Testing request permissions in an Android application

15/7/2019

While working on an Android application I wanted to test permission requests. This application needed the location permission, typically this is code that is written once and when it is marked as done never looked at again. I wanted to create some UI tests to ensure that everything works and will still work in the future as expected.

Let's start this off with creating a sample application. It's written in Kotlin with API 23 as minimum API level and it also uses the androidx.* artifacts.

Following permissions are added in the AndroidManifest.xml file:

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

When the activity is created, the app will check if the location permissions have been granted. If they haven't been granted yet, the app will request them. When the permissions are denied, the app will show Permissions denied to the user. When the permissions are granted, it will show Permissions granted. Pretty straightforward.

This is the overridden onCreate method:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) 
               != PackageManager.PERMISSION_GRANTED && 
            ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) 
               != PackageManager.PERMISSION_GRANTED) {

            requestPermissions(
                arrayOf(
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                ), LocationPermissionRequestCode)
        }
    }

This code checks if the ACCESS_FINE_LOCATION and the ACCESS_COARSE_LOCATION permissions have been granted. If not they will be requested from the user. The LocationPermissionRequestCode variable is a constant to identify the request. In the sample application it is set to 1.

Next we need to handle the response of the user. To do this, there is a method which the activity provides that can be overridden. The onRequestPermissionsResult method is overridden and a switch statement determines for which request the response is received. Note that the same LocationPermissionRequestCode variable is used as above. According to the result of the request the text of the feedbackLabel is changed.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {

        when(requestCode) {
            LocationPermissionRequestCode -> {

                if((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                    feedbackLabel.text = "Permissions granted"
                } else {
                    feedbackLabel.text = "Permissions denied"
                }
                return
            }
            else -> {

            }
        }
    }

Note: to access the feedbackLabel I'm using the Kotlin Android Extensions.

Now the sample app is working let's get to the testing part. Code like this is often written once, tested once and then never touched again. A developer will grant the permissions and go on with his life. If a bug creeps in it will be noticed too late or not at all. So let's write some UI tests to verify that the app behaves as expected. UI testing on Android is often done using Espresso and it works quite well. There even is an Espresso test recorder built into Android Studio to quickly record tests.

Let's record our first Espresso test, if the user denies the permission the feedbackLabel should read Permissions denied. In Android Studio select Run > Record Espresso Test from the menu.

The test recorder will open and a log of all actions that have happened on the device will appear. After clicking the Deny button assert that the feedbackLabel has the expected value.

If Espresso has not been added to the gradle file of the application, Android Studio will propose to do it for you.

Unfortunately Espresso can't be used for this kind of test. Espresso only works on the current package and the Allow and Deny buttons from the Permissions dialog are from another package. As you can see in the test record, the click on the Deny button is not recorded to the list of actions.

There is however a solution, next to Espresso there is also the UI Automator testing framework. This framework has no problem with accessing elements from another package. We can't use the test recorder so the test will have to be coded but this is pretty straightforward. This line in the gradle file will add UI Automator:

androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'

And the test becomes:

@RunWith(AndroidJUnit4::class)
class MainActivityTests {

    private var device : UiDevice? = null

    @get:Rule
    var mainActivityTestRule = ActivityTestRule(MainActivity::class.java)

    @Before
    fun setUp() {
        this.device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    }

    @Test
    fun testFeedbackLocationPermissionDenied() {
        val denyButton = this.device?.findObject(UiSelector().text("DENY"))
        val permissionDeniedMessage = this.device?.findObject(UiSelector().text("Permission denied"))

        denyButton!!.click()

        assert(permissionDeniedMessage!!.exists())
    }

    @Test
    fun testFeedbackLocationPermissionAllowed() {
        val allowButton = this.device?.findObject(UiSelector().text("ALLOW"))
        var permissionAllowedMessage = this.device?.findObject(UiSelector().text("Permission allowed"))
        allowButton!!.click()
        assert(permissionAllowedMessage!!.exists())
    }
}

The testFeedbackLocationPermissionAllowed test will check the message when the permission is allowed, the testFeedbackLocationPermissionDenied will check the message when the permission is denied.

There is however a problem with these tests. Once the permission has been allowed, the tests will fail when they are run again. If you think about it, it's not surprising. Because the permission is already granted, the permission popup will no longer be shown the second, third, fourth, ... time the tests are run.

To fix this the granted permissions need to be cleared every time the tests are run. Clearing the permission can be done using pm revoke. These commands are added to the After method to clean up the permissions.

@After
fun tearDown() {
   InstrumentationRegistry.getInstrumentation().uiAutomation.
       executeShellCommand("pm revoke ${InstrumentationRegistry.getInstrumentation().targetContext.packageName} android.permission.ACCESS_COARSE_LOCATION")

   InstrumentationRegistry.getInstrumentation().uiAutomation.
       executeShellCommand("pm revoke ${InstrumentationRegistry.getInstrumentation().targetContext.packageName} android.permission.ACCESS_FINE_LOCATION")
    }
}

When the tearDown executes logcat logs the following:

2019-07-15 11:09:43.639 21138-21154/com.eysermans.permissionuitesting W/UiAutomation: UiAutomation.revokeRuntimePermission() is more robust and should be used instead of 'pm revoke'

So instead of using pm revoke we should use revokeRuntimePermission.

@After
fun tearDown() {
    InstrumentationRegistry.getInstrumentation().uiAutomation.revokeRuntimePermission(
        InstrumentationRegistry.getInstrumentation().targetContext.packageName,
        Manifest.permission.ACCESS_COARSE_LOCATION)

    InstrumentationRegistry.getInstrumentation().uiAutomation.revokeRuntimePermission(
        InstrumentationRegistry.getInstrumentation().targetContext.packageName,
        Manifest.permission.ACCESS_FINE_LOCATION)
}

Unfortunately this didn't work either. Every time the testFeedbackLocationPermissionAllowed test ran it failed stating that more information can be found in logcat. However I did not find any additional error logging in logcat. It just did not work.

Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.''. Check device logcat for details

Let's explore another option, using the Android Test Orchestrator. Add the dependency to the gradle file:

androidTestUtil 'androidx.test:orchestrator:1.2.0'

Then add the following line to the defaultConfig section of the gradle file:

testInstrumentationRunnerArguments clearPackageData: 'true

And add the testOptions section to the android section:

testOptions {
    execution 'ANDROIDX_TEST_ORCHESTRATOR'

    unitTests {
        includeAndroidResources = true
    }
}

The gradle file now looks like this:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.eysermans.permissionuitesting"
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    testOptions {
        execution 'ANDROIDX_TEST_ORCHESTRATOR'

        unitTests {
            includeAndroidResources = true
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test:rules:1.3.0-alpha01'
    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'

    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestUtil 'androidx.test:orchestrator:1.2.0'
}

The tearDown method can now completely be removed and the testFeedbackLocationPermissionAllowed method can now be run over and over again.

It took some work but this solution works. The permissions are cleared before every test. I preferred using a tearDown method because it is more straightforward. But unfortunately it did not work as expected.

The sample application is available on GitHub.

More links on the subject: