After a lot of research I found a way to save screenshots in kotlin based on the SDK version using MediaStore.
/**
* storeFailureScreenshot will store the bitmap based on the SDK level of the
* device. Due to security improvements and changes to how data can be accessed in
* SDK levels >=29 Failure screenshots will be stored in
* sdcard/DIRECTORY_PICTURES/Failure_Screenshots.
*/
fun storeFailureScreenshot(bitmap: Bitmap, screenshotName: String) {
val contentResolver = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext.contentResolver
// Check SDK version of device to determine how to save the screenshot.
if (android.os.Build.VERSION.SDK_INT >= 29) {
useMediaStoreScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
} else {
usePublicExternalScreenshotStorage(
contentValues,
contentResolver,
screenshotName,
SCREENSHOT_FOLDER_LOCATION,
bitmap
)
}
}
/**
* This will be used by devices with SDK versions >=29. This is to overcome scoped
* storage considerations now in the SDK version listed to help limit file
* clutter. A Uniform resource identifier (Uri) is used to insert bitmap into
* the gallery using the contentValues previously specified. The contentResolver
* provides application access to content model to access and publish data in a
* secure manner, using MediaStore collections to do so. Files will
* be stored in sdcard/Pictures
*/
private fun useMediaStoreScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "$screenshotName.jpeg")
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + screenshotLocation)
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) }
contentResolver.update(uri, contentValues, null, null)
}
}
/**
* Method to access internal storage on a handset with SDK version below 29.
* Directory will be in sdcard/Pictures. Relevant sub directories will be created
* & screenshot will be stored as a .jpeg file.
*/
private fun usePublicExternalScreenshotStorage(
contentValues: ContentValues,
contentResolver: ContentResolver,
screenshotName: String,
screenshotLocation: String,
bitmap: Bitmap
) {
val directory = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES + screenshotLocation).toString())
if (!directory.exists()) {
directory.mkdirs()
}
val file = File(directory, "$screenshotName.jpeg")
saveScreenshotToStream(bitmap, FileOutputStream(file))
val values = contentValues
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
}
/**
* Assigns the assignments about the Image media including, image type & date
* taken. Content values are used so the contentResolver can interpret them. These
* are applied to the contentValues object.
*/
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
/**
* Compresses the bitmap object to a .jpeg image format using the specified
* OutputStream of bytes.
*/
private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) {
outputStream.use {
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, it)
} catch (e: IOException) {
Timber.e("Screenshot was not stored at this time")
}
}
}
Used in conjunction with a TestWatcher that takes a screenshot on a UI test failure. This is then added to the test class as a rule.
private val deviceLanguage = Locale.getDefault().language
/**
* Finds current date and time & is put into format of Wed-Mar-06-15:52:17.
*/
fun getDate(): String = SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())
/**
* ScreenshotFailureRule overrides TestWatcher failed rule and instead takes a
* screenshot using the UI Automation takeScreenshot method and the
* storeFailureScreenshot to decide where to store the bitmap when a failure
* occurs.
*/
class ScreenshotFailureRule : TestWatcher() {
override fun failed(e: Throwable?, description: Description) {
val screenShotName = "$deviceLanguage-${description.methodName}-${getDate()}"
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName)
}
}
file is stored in sdcard/Pictures/Failure_Screenshots
with a name of
en-testMethodName-Day-Month-Date-HH_MM_SS
Rule called using:
val screenshotFailureRule = ScreenshotFailureRule()