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 aninit
method that takes in anandroid.app.ActivityThread
which is the main thread of the application (not the test application). -
SpoonRule.obtainDirectory()
callsandroid.os.Environment.getExternalStorageDirectory()
-
Environment
callsandroid.os.StorageManager.getVolumeList()
-
StorageManager.getVolumeList()
gets the package from the very sameActivityThread
: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.