Невозможно сохранить скриншоты отказа эспрессо на устройствах Android 10

Поскольку в Android 10 улучшены изменения конфиденциальности Android 10, я заметил, что мое правило наблюдателя за тестом на сбой скриншота в Kotlin, которое расширяет Espresso BasicScreenCaptureProcessor, больше не сохраняет скриншоты сбоя, потому что я использую устаревшее getExternalStoragePublicDirectory на Android 10.

Концепция, реализуемая в настоящее время, очень похожа на Как сделать снимок экрана в момент сбоя теста в эспрессо?

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

Как видно из других сообщений, я мог бы использовать getInstrumentation().getTargetContext().getApplicationContext().getExternalFilesDir(DIRECTORY_PICTURES)

это сохранит файл в каталоге - /sdcard/Android/data/your.package.name/files/Pictures, но задача connectedAndroidTest gradle удалит приложение в конце вместе с папками, перечисленными выше.

Мне интересно, сталкивался ли кто-нибудь еще с чем-то подобным и рассматривал ли способ хранения скриншотов сбоев на Android 10 в месте, которое не будет удалено после завершения тестов, и где-то, к чему могут получить доступ тесты Espresso Instrumentation.

Мои тесты пользовательского интерфейса выполняются на различных устройствах, поэтому для всех моделей требуется общий способ хранения файлов.


person lovelylauz219    schedule 21.04.2020    source источник


Ответы (1)


После долгих исследований я нашел способ сохранять скриншоты в kotlin на основе версии SDK с помощью 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")
        }
    }
}

Используется вместе с TestWatcher, который делает снимок экрана при сбое теста пользовательского интерфейса. Затем, как правило, это добавляется в тестовый класс.

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)
    }
}

файл хранится в sdcard/Pictures/Failure_Screenshots с именем en-testMethodName-Day-Month-Date-HH_MM_SS

Правило вызывается с помощью:

val screenshotFailureRule = ScreenshotFailureRule()
person lovelylauz219    schedule 17.08.2020
comment
Это потрясающе, и спасибо, что поделились этим. Это, вероятно, прекрасно работает на CI, но на моем локальном компьютере любые изображения, созданные во время запуска № 1, не могут быть перезаписаны при запуске № 2, потому что приложение больше не владеет этими файлами. - person Afzal N; 10.02.2021