Skip to main content

A marshmallow shaped problem in my screenshot empire

"Never without my permission" ~ Leeloo (translated)



android.permission.WRITE_EXTERNAL_STORAGE

As a tester, you probably have a favorite app, maybe a favorite feature, possibly even a favorite bug. But you probably don't have a favorite permission.

I've been taking advantage of it for years to do things like gather code coverage on non-rooted devices, logcat output from an entire test run, and probably my favorite usage is screenshots triggered when tests fail. Yep, that little permission has been a big pal of mine for a while. So it should be no surprise that I felt a little betrayed when I recently discovered a problem with it on devices running Android 6.0 aka "Marshmallow".

Among the many changes brought about by the 6.0 release was a long-overdue overhaul to Android's permission scheme, notably, from an all-or-nothing approval of permissions at install-time model from Android 1.0-5.1.1 to the more nuanced permissions requested when required during run-time model now. The idea is to give the user more control over what they want to allow an application to do and give app developer the ability to request permissions in a context the user understands inside their running application. In other words, everybody wins. Kinda. Okay not really.

See, I really like to use screenshots to quickly determine what went wrong when a UI test case of mine has failed. Like the saying goes, "a picture is worth a thousand lines of logcat". My test harnesses have had a means of capturing those screenshots and saving them locally to the device's /sdcard/TEST_OUTPUT_DIR which my framework would specify. Since that's not an internal directory to the application under test, I need to make sure it's manifest includes the permission: android.permission.WRITE_EXTERNAL_STORAGE. For some apps I've tested that was added at build-time for my specific builds as the public version of the app didn't require it. For the rest, it was already there and simply provided extra utility for my automation. And up until Marshmallow, that was enough.

What I discovered after adding test devices to my cluster that were running Android 6.0 was that when their UI tests experienced failures, no screenshots were recovered. When I checked the logs, I found that the files could not be written owing to a permission violation exception: a little like this:

E UiAutomatorBridge: java.io.FileNotFoundException: /sdcard/test_output_dir/com.example.app.test.DummyTestClass.testMethod.png: open failed: EACCES (Permission denied)


So after double-checking the manifest to make sure I wasn't taking crazy pills, it occurred to me to check the permissions in the Settings App to see if it wasn't giving me access to Storage. Sure enough, the permission was in the list but the toggle was off. I flipped the toggle on and re-ran the test without clearing app data or reinstalling it. Then lo and behold a wild screenshot appeared!

While discussing this problem with my friend, Joe, he suggested I try calling "pm grant com.example.app android.permission.WRITE_EXTERNAL_STORAGE" before the test and see if that worked. My hopes were high. I had a clever check in my script (also by Joe's suggestion) to only run that command if "getprop ro.build.version.sdk" evaluated greater than or equal to 23. I had tested the "pm grant" command and observed that it showed the Storage toggle as enabled in the Settings App's permissions for my app under test. Everything should have worked beautifully.

E UiAutomatorBridge: java.io.FileNotFoundException: /sdcard/test_output_dir/com.example.app.test.DummyTestClass.testMethod.png: open failed: EACCES (Permission denied)

ARE YOU KIDDING ME?!?!

So for the next hour or two or three, Joe and I speculated, I retraced my steps over and over again. Then I re-read that error message: "open failed". That sounded an awful lot like READ_EXTERNAL_STORAGE. To look further into it, I did something crazy: I read the documentation. Tucked away in there was this beautiful little nugget: 


However, if your app uses the WRITE_EXTERNAL_STORAGE permission, then it implicitly has permission to read the external storage as well.

How lovely. That made me think that perhaps the script needed to explicitly grant the READ_EXTERNAL_STORAGE permission even though it was implicitly included with the write permission. After a quick edit to the script, I re-ran my example test scenario and it finally worked again. After beating my head against my monitor for half the day thinking I'd been wrong, or high it came down to setting read access in order to use write access. Intuitive, right? Not if you ask me. I was so taken aback by the problem, I actually opened a bug on this. The issue no longer really seems to be a bug per se, but it is at least a subtle and possibly unforeseen consequence of the new permissions model.

Here's a quick Gist demonstrating the basic approach I use inside my larger test execution script:


Comments

  1. NOTE: If you're an Appium user, this applies to you too because Appium uses the same underlying APIs but isn't running the tests through the standalone uiautomator runtest approach. Which means I'm guessing you need to find a way to install the app, then apply the appropriate permissions, then start the tests.

    ReplyDelete
  2. Hi there, I'm using uiAutomator with Android Studio (JUnit4), ran into the same problem but unable to resolve it, any tips?

    ReplyDelete

Post a Comment

Popular posts from this blog

UiAutomator and Watchers: Adding Async Robustness to UI Automation

"I'm looking over your shoulder... only because I've got your back." ~ Stephen Colbert After my recent UiAutomator review a user brought up an important question about the use of UiWatcher. The watchers serve as async guardians of the test flow, making sure the odd dialog window doesn't completely frustrate your tests. Having a tool that automatically watches your back when you're focused on the functional flow of your tests is awesome. 100% pure awesomesauce. Since the API documentation on watchers is scant and the UI Testing tutorial on the Android dev guide doesn't cover their use in depth, I figured I should add a post here that goes over a simple scenario demonstrating how to use this fundamentally important UI automation tool. In my example code below, I'm using uiautomator to launch the API Demo app (meaning run this against an Emulator built in API level 17 - I used the Galaxy Nexus image included in the latest ADT and platform tools).

UiAutomator.jar: What happened when Android's JUnit and MonkeyRunner got drunk and hooked up

"Drunkenness does not create vice; it merely brings it into view" ~Seneca So Jelly Bean 4.2 landed with much fanfare and tucked in amongst the neat new OS and SDK features (hello, multi-user tablets!) was this little gem for testers: UiAutomator.jar. I have it on good authority that it snuck in amongst the updates in the preview tools and OS updates sometime around 4.1 with r3 of the platform. As a code-monkey of a tester, I was intrigued. One of the best ways Google can support developers struggling with platform fragmentation is to make their OS more testable so I hold high hopes with every release to see effort spent in that area. I have spent a couple days testing out the new UiAutomator API  and the best way I can think of describing it is that Android's JUnit and MonkeyRunner got drunk and had a code baby. Let me explain what I mean before that phrase sinks down into "mental image" territory. JUnit, for all its power and access to every interface, e

Run-As Like the Wind: Getting private app data off non-rooted devices using adb run-as and a debuggable app

"You're some kind of big, fat, smart-bug aren't you?" ~Johnny Rico, Starship Troopers (1997) One of the most important things about writing bugs is making them descriptive but concise. Screenshots, video, debug logging, and hardware snapshots are all fairly standard or available to Android testers these days and can save you enormously on text when communicating what constitutes the bug. Sometimes though, the app gets into a weird state due to some transient data issue where you may not own the API or the complexity with forcing the app data into a certain state is prohibitively high. In those cases it is very handy to directly inspect the data the app has in its own directories. Getting at this data is trivial on emulators and rooted devices but due to file system permissions, this data is otherwise completely private to the app itself. If you're like me, you tend to test using devices rather than emulators and you probably prefer not to root your devices sin