Search code examples
androidadbandroid-permissionsandroid-storagescoped-storage

Which storage API for automated screenshot saving and retrieval?


I'm automating the creation of screenshots using instrumented tests (androidTest) on a debug version of my app using UiAutomator and a shell script that uses adb pull.

Once each screenshot has been taken, I need to be given access to a directory on the Android device to write each image to, that:

  • can be written to without user input (eg requesting permissions, file picker)
  • can be accessed by ADB without any special tricks -- these may break in future (eg run-as)
  • doesn't require configuration steps on each device, which may break when someone else adds newer devices in future (eg rooting the device, installing special certificates).
  • doesn't require any third-party tools (eg better adb sync)
  • only has to work on Android devices with API 26 and later (8.0+).
  • preferably does not require any manifest permissions (but this is not a deal-breaker).

The Android docs for choosing a storage API are centred around normal usage by a user of the production app, and don't specify whether adb can access them.

There is so much outdated advice (thanks to the many, many breaking changes to the Android storage APIs over the last 10 years) that the signal-to-noise ratio for existing advice is very low, and checking whether each found solution still works is non-trivial.

Which storage API should/can I use that satisfies the above requirements?


Solution

  • In the end, the only modern API that was readable externally was MediaStore.

    Using code adapted from the Android docs:

        private fun saveWithMediaStore(context: Context, bm: Bitmap, fileName: String) {
            // Add a media item that other apps don't see until the item is
            // fully written to the media store.
            val resolver = context.contentResolver
    
            // Find all audio files on the primary external storage device.
            val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                MediaStore.Images.Media.getContentUri(
                    MediaStore.VOLUME_EXTERNAL_PRIMARY
                )
            } else {
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            }
    
            val imageDetails = ContentValues().apply {
                put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
                put(MediaStore.Images.Media.IS_PENDING, 1)
            }
    
            val songContentUri = resolver.insert(imageCollection, imageDetails)
    
            // "w" for write.
            resolver.openFileDescriptor(songContentUri!!, "w", null).use { pfd: ParcelFileDescriptor? ->
                // Write data into the pending image file.
                if(pfd !=null) {
                    try {
                        val fOut = FileOutputStream(pfd.fileDescriptor)
                        bm.compress(Bitmap.CompressFormat.PNG, 85, fOut)
                        fOut.flush()
                        fOut.close()
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
            // Now we're finished, release the "pending" status and let other apps view the image.
            imageDetails.clear()
            imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
            resolver.update(songContentUri, imageDetails, null, null)
    
        }
    
    

    On my emulated device, this wrote the screenshot to

    /storage/emulated/0/Pictures/<filename_you_specify>
    

    so you can then just do

    adb pull /storage/emulated/0/Pictures/<filename_you_specify>
    

    to get the file from the device.