无法在 Android 10 设备上存储 Espresso 失败屏幕截图

lov*_*219 3 automation screenshot kotlin android-espresso android-instrumentation

自从 Android 10 上改进的隐私更改Android 10 隐私更改以来,我注意到 Kotlin 中的屏幕截图失败测试观察程序规则(扩展了 Espresso BasicScreenCaptureProcessor)不再保存失败屏幕截图,因为我使用的是getExternalStoragePublicDirectoryAndroid 10 上已弃用的屏幕截图。

目前实现的概念非常类似于How to take Screenshot at the point where test failed in Espresso?

class TestScreenCaptureProcessor : BasicScreenCaptureProcessor() {
    init {
        this.mDefaultScreenshotPath = File(
            File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                "Failure_Screenshots"
            ).absolutePath
        )
    }
Run Code Online (Sandbox Code Playgroud)

正如在其他帖子中看到的,我可以使用getInstrumentation().getTargetContext().getApplicationContext().getExternalFilesDir(DIRECTORY_PICTURES)

这会将文件存储在 -/sdcard/Android/data/your.package.name/files/Pictures目录中,但connectedAndroidTestgradle 任务最后会删除该应用程序以及上面列出的文件夹。

我想知道是否有其他人遇到过类似的情况,并考虑过在 Android 10 上存储故障屏幕截图的方法,存储在测试运行完成后不会被删除的位置以及 Espresso Instrumentation 测试可以访问的位置。

我的 UI 测试在各种设备上运行,因此需要一种通用的文件存储方式来适应所有模型。

lov*_*219 7

经过大量研究,我找到了一种使用MediaStore在基于SDK版本的kotlin中保存屏幕截图的方法。


/**
 * 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")
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

与 TestWatcher 结合使用,对 UI 测试失败进行屏幕截图。然后按照规则将其添加到测试类中。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

文件存储在sdcard/Pictures/Failure_Screenshots名称为 en-testMethodName-Day-Month-Date-HH_MM_SS

规则调用使用:

val screenshotFailureRule = ScreenshotFailureRule()
Run Code Online (Sandbox Code Playgroud)