Spoon and test apk WRITE_STORAGE_PERMISSION

I want to get this out quick without a lot of polish because something is better than nothing.

We use Spoon to aggregate our end-to-end tests and capture screenshots of our Android app. In order to save these screenshots, our app’s AndroidManifest.xml needs to declare the WRITE_STORAGE_PERMISSION permission.

However this is the only reason that our app needs this permission. We don’t write to external storage any other time in the actual app. So my goal was to remove this permission from our app manifest and see if I could instead declare it our test .apk.

The first step was to get Spoon to grant all permissions to the test .apk when it granted permissions to the app. I filed a ticket and eventually got a PR merged in.

I then moved the WRITE_STORAGE_PERMISSION line from our app’s AndroidManifest.xml to our test app’s AndroidManifest.xml. I then ran our end-to-end tests again and saw that we weren’t getting any screenshots in our Spoon report.

I checked both the installed apks. I saw that the test .apk has been granted the write storage permission. So what was the problem?

Down the rabbit hole

I won’t go into all the details. I’ll just share some breadcrumbs for you to follow along if you want.

com.squareup.spoon.SpoonRule.screenshot() takes in an Activity to obtain directory:

File screenshotDirectory =
        obtainDirectory(activity.getApplicationContext(),
        className, 
        methodName,
        SPOON_SCREENSHOTS);

Obviously this isn’t going to work since our Activity, and by extension activity.getApplicationContext(), isn’t the Context with the write storage permission. I altered the Spoon source to overload the screenshot() method to take in a separate Context. We still need to pass the Activity so that the screen capture can get access to the Window.

public File screenshot(Context permissionHoldingContext, Activity activity, String tag) {
    if (!TAG_VALIDATION.matcher(tag).matches()) {
        throw new IllegalArgumentException("Tag must match " + TAG_VALIDATION.pattern() + ".");
    }
    File screenshotDirectory =
            obtainDirectory(permissionHoldingContext, className, methodName, SPOON_SCREENSHOTS);
    String screenshotName = System.currentTimeMillis() + NAME_SEPARATOR + tag + EXTENSION;
    File screenshotFile = new File(screenshotDirectory, screenshotName);
    Bitmap bitmap = Screenshot.capture(tag, activity);
    ...
}

And then call that method with our test app’s Context from our test:

spoonRule.screenshot(InstrumentationRegistry.getContext(), currentActivity, tag);

Farther down the rabbit hole

I was certain that this would work but still no luck. Thats when I really went down the rabbit hole. I found that:

  • Whichever test runner you use, it subclasses android.app.Instrumentation.

  • Instrumentation has an init method that takes in an android.app.ActivityThread which is the main thread of the application (not the test application).

  • SpoonRule.obtainDirectory() calls android.os.Environment.getExternalStorageDirectory()

  • Environment calls android.os.StorageManager.getVolumeList()

  • StorageManager.getVolumeList() gets the package from the very same ActivityThread:

    String packageName = ActivityThread.currentOpPackageName();
    ...
    final int uid = ActivityThread.getPackageManager().getPackageUid(packageName,
                    PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId);
    ...
    return storageManager.getVolumeList(uid, packageName, flags);
    

So I was stuck. No matter what I did I won’t be able to force Environment or StorageManager to use my test app’s package.

Best Solution

I’d seen this for other test-only permissions. Basically you just put the needed permissions an AndroidManifest.xml in the debug/develop buildType and then rely on manifest merger to put this permission in debug/develop builds but not in release builds.

This isn’t my favorite solution but it’ll have to do.