© Denys Zelenchuk 2019
Denys ZelenchukAndroid Espresso Revealedhttps://doi.org/10.1007/978-1-4842-4315-2_9

9. Dealing with Runtime System Actions and Permissions

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 

Nowadays, most Android applications support multiple locales, and many request different system permissions, for example permission to access the device camera, location permissions, and permission to write to external storage.

With the evolution of the Android platform, the approach to application permissions has changed in favor of user privacy. Starting from API level 23, application permissions are asked during application runtime and upon user requests. Such permissions are represented by a system popup or system dialog and are not the part of the application being tested. Moreover, these permissions can be revoked by the users any time from the application settings. Of course, the mentioned application states should be handled properly before or during UI tests are run.

This chapter explains the different ways we can deal with system actions like permission request dialogs and describes the possible solutions for changing the Android emulator system language programmatically.

Changing the Emulator System Language Programmatically

Up to API level 27, Android provided a possibility to set the system locale by sending the intent via an adb command or directly from the test code. This could be achieved because CustomLocale.apk was preinstalled on emulators and was able to handle the sent intent. An example of adb shell am command is the following:
adb shell am broadcast -a com.android.intent.action.SET_LOCALE --es
        com.android.intent.extra.LOCALE "en_US" com.android.customlocale2
However, starting with API level 28, the CustomLocale.apk application was removed from the emulator image, which required another solution. After a closer look at the Android emulator release notes ( https://developer.android.com/studio/releases/emulator ), the solution was clear. Starting with Android emulator version 27.2.9 (from May 2018), you can load a QuickBoot snapshot without restarting the emulator. The emulator release notes page explains how to do this manually with the help of the emulator Extended Controls window inside the Settings section. See Figure 9-1.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig1_HTML.jpg
Figure 9-1

Emulator extended controls

When the snapshot view appears, you put the device into the desired state and click the TAKE SNAPSHOT button. The new snapshot with an automatically generated name is taken and shown in the snapshot list. See Figure 9-2.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig2_HTML.jpg
Figure 9-2

Taking emulator snapshots

Note

Renaming the snapshot in the emulator Extended Controls window will not rename it globally, but just creates the alias. The actual snapshot name will remain unchanged.

The same can be done by communicating with the Android emulator via the telnet console command (more information about the emulator telnet command can be found at https://developer.android.com/studio/run/emulator-console ). In the following code examples, you can see how to establish the telnet session with a running emulator, list existing snapshots, take the snapshot, and load it during emulator runtime.

Sample Script to Save and Load the Emulator Snapshot.
telnet localhost 5554
avd snapshot list
avd snapshot save snap_de
emulator -avd Pixel2_API_28 -snapshot snap_de

Since the main topic of this book is test automation, the following Python script creates the telnet connection to the localhost port 5554 (which is the first port the Android emulator takes when it is created) and loads the previously saved snapshot.

Python Script to Establish the Emulator Telnet Connection and Load the Emulator Snapshot.
import telnetlib
HOST = "localhost"
PORT = "5554"
tn = telnetlib.Telnet(HOST, PORT)
tn.write(b"avd snapshot load name ")
tn.write(b"exit ")

Alternatively, you can do the same thing using the expect scripting utility (for MacOS and UNIX users only).

Expect Utility Script to Establish the Emulator Telnet Connection and Load the Emulator Snapshot.
#!/usr/bin/expect
set timeout 15
spawn telnet localhost 5554
expect "OK"
send "avd snapshot load snap_de "
expect "OK"
send "exit "
To install the expect utility on your computer, use these commands:
  • Mac: brew install expect

  • UNIX/Linux: yum install expect

In general, the current snapshot approach works not only for setting emulator language but also gives us the ability to have different snapshots for many use cases, which you can come up with by your own.

Exercise 21

Using the Emulator Snapshot Functionality
  1. 1.

    Launch an emulator and save a couple of emulator snapshots with the help of emulator Extended Controls window. Load the snapshots manually.

     
  2. 2.

    Launch an emulator using the console commands. Connect the telnet session to the emulator and save a couple of emulator snapshots. Load the snapshots from the console.

     
  3. 3.

    Create a Python script with emulator telnet commands and run it. Observe the result.

     
  4. 4.

    Install the expect utility on your computer, and then create and execute the script with the expect telnet commands. Observe the results.

     

Handling Runtime Permissions

Another burning topic in Android test automation related to system popups are runtime permissions. Appropriate permission should be requested by the Android application when it requires resources or information outside of its sandbox. The application declares permissions in the AndroidManifest.xml file and then requests that the user approve each permission at runtime (on Android 6.0 and higher).

When the user triggers a piece of code that requires additional permissions, the prompt shown by the system describes the permission group your app needs access to, not the specific permission.

In order to showcase this functionality, our sample application requests permission when we are adding an image to the TO-DO item. Try it out.

Enabling Permissions Using the GrantPermissionRule

Now let’s take a look at the RuntimePermissionTest.kt class , which contains the GrantPermissionRule sample. The GrantPermissionRule rule grants runtime permissions on Android M (API 23) and above. This rule is used when a test requires a runtime permission to do its work. When applied to a test class, this rule attempts to grant all requested runtime permissions. The requested permissions will then be granted on the device and will take immediate effect.

Clicking on the camera icon in the sample TO-DO application triggers the Camera permission prompt to be shown to the user. The prompt belongs to the different application package, called com.andriod.packageinstaller, which Espresso cannot interact with. So, in order to reduce external dependencies and keep our Espresso tests hermetic, GrantPermissionRule can be used to start the test with the already granted permission.

chapter9 . RuntimePermissionsTest.kt .
@RunWith(AndroidJUnit4::class)
class RuntimePermissionsTest {
    /**
     * Manifest.permission.CAMERA permission will be granted before the test run.
     */
    @get:Rule
    var mRuntimePermissionRule = GrantPermissionRule
            .grant(Manifest.permission.CAMERA)
    /**
     * Provided activity will be launched before each test.
     */
    @get:Rule
    var activityTestRule = ActivityTestRule(TasksActivity::class.java)
    @Test
    fun takesCameraPicture() {
        val toDoTitle = TestData.getToDoTitle()
        val toDoDescription = TestData.getToDoDescription()
        // Adding new TO-DO.
        onView(withId(R.id.fab_add_task)).perform(click())
        onView(withId(R.id.add_task_title))
                .perform(typeText(toDoTitle), closeSoftKeyboard())
        onView(withId(R.id.add_task_description))
                .perform(typeText(toDoDescription), closeSoftKeyboard())
        // Clicking on camera button to trigger the permission dialog.
        onView(withId(R.id.makePhoto)).perform(click())
        onView(withId(R.id.picture)).perform(click())
        waitForElement(onView(withId(R.id.fab_edit_task_done))).perform(click())
        // verifying new TO-DO with title is shown in the TO-DO list.
        onView(withText(toDoTitle)).check(matches(isDisplayed()))
    }
}
This current approach works well, but it has its pros and cons. The positive aspects are:
  • UI tests remain hermetic and do not require interactions with other system services.

  • Permission is granted for each test case inside the test class.

However, there are also some negative moments:
  • It is not possible to test different runtime permission use cases like getting permission after denial or trying to use the feature without permission granted.

  • There is no way to revoke a permission after it is granted. Attempting to do so will crash the instrumentation process.

In general, using GrantPermissionRule is a nice way to grant runtime permissions and avoid permission dialogs from showing up and blocking the application UI. From the other side, as was stated, it limits us in terms of covering multiple runtime permission requests use cases that are also part of the application that should be tested.

Handling Runtime Permissions Using UI Automator

Another way to handle runtime permissions is to use the UI Automator test framework functionality together with Espresso. Since it allows us to interact with any application, we are able to perform UI actions on permission dialogs as well.

First, let’s consider the possible use cases. In total, there can be at least three use cases where the runtime permission dialog is involved:
  • Camera permissions granted the first time the permission dialog is shown to the user.

  • Camera permission denied by the user during the first occurrence, but then the user realizes that she needs such functionality and enables it when the permission dialog is presented a second time.

  • Permission is denied two times. The second denial was made with the Don’t Ask Again checkbox checked. The user tries to use the Camera feature again, but now she must manually enable it from the application permission settings.

And just to be clear, there is an application code-behind all four use cases, which in the case of using GrantPermissionRule , are not covered by automated tests and require manual testing.

Second, we may face an issue using only AndroidJUnitRunner for permission tests. The thing is that each test requires a clean application state without granted permissions. Therefore, the option with the Android Test Orchestrator described in Chapter 1 should be used with testInstrumentationRunnerArguments clearPackageData: 'true' parameter (see app/build.gradle file for more details). It ensures that each test will be run within its own invocation, including the cleaned application permissions state.

Third, we have to inspect all areas we will navigate to with the Monitor tool, making the UI dump and collect identifiers from elements used in defined use cases.

Figure 9-3 shows the Grant Camera Permission dialog, used when a TO-DO item is created with an attached image.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig3_HTML.jpg
Figure 9-3

Dumping the TO-DO application UI with Camera Permission dialog

Figure 9-4 demonstrates the inspection of the TO-DO application settings page in the system’s settings application.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig4_HTML.jpg
Figure 9-4

UI dump of the TO-DO application on the settings page

Figure 9-5 shows the inspection of the TO-DO application permissions settings page inside the system’s settings application.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig5_HTML.jpg
Figure 9-5

UI dump of the TO-DO application in the settings app permissions page

This is the list of UI elements we should collect and which tests will operate on (referenced device Nexus 5X, operation system Android 8.1.0):
  • Allow button—com.android.packageinstaller:id/permission_allow_button

  • Deny button —com.android.packageinstaller:id/permission_deny_button

  • Don’t Ask Again checkbox—com.android.packageinstaller:id/do_not_ask_checkbox

  • Permissions list item—Fourth item in the recycler view com.android.settings:id/list

  • Camera permission list item—Zeroth element in the list view android:id/list

And finally, our tests. Take a look at RuntimePermissionsUiAutomatorTest.kt . For convenience, we declared some reusable instances:
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
private val todoAppPackageName = InstrumentationRegistry.getTargetContext().packageName
private val testContext = InstrumentationRegistry.getContext()

The first use case is represented by the takesCameraPicture() test case , where the user clicks just once on the permission dialog.

Test Method chapter9 .RuntimePermissionsUiAutomatorTest.takesCameraPicture().
@Test
fun takesCameraPicture() {
    val toDoTitle = TestData.getToDoTitle()
    // Adding new TO-DO.
    onView(withId(R.id.fab_add_task)).perform(click())
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard())
    // Clicking on camera button to trigger the permission dialog.
    onView(withId(R.id.makePhoto)).perform(click())
    // UI Automator - click permission dialog ALLOW button.
    uiDevice.findObject(By.res("com.android.packageinstaller:id/permission_allow_button")).click()
    onView(withId(R.id.picture)).perform(click())
    waitForElement(onView(withId(R.id.fab_edit_task_done))).perform(click())
    // verifying new TO-DO with title is shown in the TO-DO list.
    onView(withText(toDoTitle)).check(matches(isDisplayed()))
}

The second use case is covered by the deniesAndGrantsPermission() test .

Test Method chapter9 .RuntimePermissionsUiAutomatorTest.deniesAndGrantsPermission().
@Test
fun deniesAndGrantsPermission() {
    val toDoTitle = TestData.getToDoTitle()
    onView(withId(R.id.fab_add_task)).perform(click())
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard())
    onView(withId(R.id.makePhoto)).perform(click())
    // UI Automator - click permission dialog DENY button.
    uiDevice.findObject(By.res("com.android.packageinstaller:id/permission_deny_button")).click()
    onView(withId(R.id.makePhoto)).perform(click())
    onView(withId(R.id.snackbar_action)).perform(click())
    uiDevice.findObject(By.res("com.android.packageinstaller:id/permission_allow_button")).click()
    onView(withId(R.id.picture)).perform(click())
    waitForElement(onView(withId(R.id.fab_edit_task_done))).perform(click())
    onView(withText(toDoTitle)).check(matches(isDisplayed()))
}

In the third use case, it gets a bit more complicated since we have to interact with the settings application. Here goes the test.

Test Method chapter9 .RuntimePermissionsUiAutomatorTest. deniesAndGrantsPermissionFromSettings() .
@Test
fun deniesAndGrantsPermissionFromSettings() {
    val toDoTitle = TestData.getToDoTitle()
    onView(withId(R.id.fab_add_task)).perform(click())
    onView(withId(R.id.makePhoto)).perform(click())
    uiDevice
            .findObject(By.res("com.android.packageinstaller:id/permission_deny_button"))
            .click()
    onView(withId(R.id.makePhoto)).perform(click())
    onView(withId(R.id.snackbar_action)).perform(click())
    // UI Automator - click on permission dialog checkbox and DENY button
    uiDevice
            .findObject(By.res("com.android.packageinstaller:id/do_not_ask_checkbox"))
            .click()
    uiDevice
            .findObject(By.res("com.android.packageinstaller:id/permission_deny_button"))
            .click()
    // Clicking camera button to trigger permission dialog.
    onView(withId(R.id.makePhoto)).perform(click())
    onView(withId(R.id.snackbar_text))
            .check(matches(allOf(isDisplayed(), withText("Camera unavailable"))))
    sendApplicationSettingsIntent()
    enableCameraPermission()
    launchBackToDoApplication()
    onView(withId(R.id.fab_add_task)).perform(click())
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard())
    onView(withId(R.id.makePhoto)).perform(click())
    onView(withId(R.id.picture)).perform(click())
    waitForElement(onView(withId(R.id.fab_edit_task_done))).perform(click())
    onView(withText(toDoTitle)).check(matches(isDisplayed()))
}

Where sendApplicationSettingsIntent() is responsible for creating and firing an intent to show the TO-DO application settings page.

Sends Intent to Open the TO-DO Application Settings chapter9 .RuntimePermissionsUiAutomatorTest. sendApplicationSettingsIntent() .
private fun sendApplicationSettingsIntent() {
    // Create intent to open To-Do application settings.
    val intent = Intent()
    intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
    val uri = Uri.fromParts("package", todoAppPackageName, null)
    intent.data = uri
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
    testContext.startActivity(intent)
}

Then enableCameraPermission() contains the code to open the application’s permission settings and click on the camera permission item (see Figures 9-4 and 9-5).

Enables Camera Permission in the TO-DO application settings chapter9 .RuntimePermissionsUiAutomatorTest.enableCameraPermission().
private fun enableCameraPermission() {
    // Wait for application Settings to appear
    uiDevice.wait(Until.hasObject(By.pkg("com.android.settings")), 5000)
    // Click on Permissions item.
    uiDevice.findObject(By.res("com.android.settings:id/list"))
            .children[3].clickAndWait(Until.newWindow(), 2000)
    // CLick on Camera item and wait for checked toggle state.
    uiDevice.findObject(By.res("android:id/list"))
            .children[0].click()
    uiDevice.findObject(By.res("android:id/list"))
            .children[0].wait(Until.checked(true), 1000)
}

Finally, launchBackToDoApplication() sends an intent to launch the sample application.

Sends Intent to Open the TO-DO Application chapter9 .RuntimePermissionsUiAutomatorTest.launchBackToDoApplication().
private fun launchBackToDoApplication() {
    // Create intent to open To-Do application.
    val intent = testContext.packageManager.getLaunchIntentForPackage(todoAppPackageName)
    InstrumentationRegistry.getContext().startActivity(intent)
}
After running tests from RuntimePermissionsUiAutomatorTest, we can see in Figure 9-6 that the runtime looks good—36 seconds for three tests that interact with third-party applications.
../images/469090_1_En_9_Chapter/469090_1_En_9_Fig6_HTML.jpg
Figure 9-6

RuntimePermissionsUiAutomatorTest.kt tests runtime

Exercise 22

Using Runtime Permissions
  1. 1.

    Delete GrantPermissionRule from RuntimePermissionsTest.kt and run a test. Observe the results. Revert the GrantPermissionRule deletion and run the test again. Observe the results.

     
  2. 2.

    Run all tests implemented in the RuntimePermissionsUiAutomatorTest.kt class. Remove any Android Test Orchestrator dependencies from the application build.gradle file and run the test again. Observe the results. Revert to the original file.

     
  3. 3.

    Write a test that opens the TO-DO application settings and enables camera permission. Then open the TO-DO application and proceed with task creation.

     

Summary

This chapter showed how to change the Android emulator system language at runtime and described different ways of handling runtime permissions in UI tests. It should be clear that multi-language support is a must-have for modern Android applications. This requires thorough testing and enabling the test environment to switch emulator system languages easily is an essential part of this test infrastructure.

Having an easy and reliable way to set runtime permissions is a crucial and sensitive topic for the end users. It impacts user satisfaction and should be thoroughly tested. Applications should handle different permission flows properly and without mistakes. Of course, it is up to you to select which testing approach works best for your specific case—using the GrantPermissionRule or fully automating the permission granting with the UI Automator framework. With the knowledge from this chapter, you can do that easily.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.232.189