blob: ef15d27b423947eafd519688f965eabdfce0830d [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.integration.core
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.graphics.ImageFormat
import android.graphics.Rect
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCaptureSession.CaptureCallback
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Pair
import android.util.Rational
import android.util.Size
import android.view.Surface
import androidx.annotation.OptIn
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraFilter
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
import androidx.camera.core.impl.Identifier
import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.Exif
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionFilter
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.integration.core.util.CameraPipeUtil
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
import androidx.camera.testing.impl.CoreAppTestUtil
import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.testing.impl.fakes.FakeSessionProcessor
import androidx.camera.testing.impl.mocks.MockScreenFlashUiControl
import androidx.camera.video.Recorder
import androidx.camera.video.VideoCapture
import androidx.core.content.ContextCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.abs
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeNoException
import org.junit.Assume.assumeNotNull
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
private val DEFAULT_RESOLUTION = Size(640, 480)
private val BACK_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
private val FRONT_SELECTOR = CameraSelector.DEFAULT_FRONT_CAMERA
private const val BACK_LENS_FACING = CameraSelector.LENS_FACING_BACK
private const val CAPTURE_TIMEOUT = 15_000.toLong() // 15 seconds
private const val TOLERANCE = 1e-3f
@LargeTest
@RunWith(Parameterized::class)
class ImageCaptureTest(private val implName: String, private val cameraXConfig: CameraXConfig) {
@get:Rule
val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
active = implName == CameraPipeConfig::class.simpleName,
)
@get:Rule
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
CameraUtil.PreTestCameraIdList(cameraXConfig)
)
@get:Rule
val externalStorageRule: GrantPermissionRule =
GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
val temporaryFolder =
TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
@get:Rule
val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
companion object {
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data() = listOf(
arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
)
}
private val context = ApplicationProvider.getApplicationContext<Context>()
private val mainExecutor = ContextCompat.getMainExecutor(context)
private val defaultBuilder = ImageCapture.Builder()
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
@Before
fun setUp(): Unit = runBlocking {
CoreAppTestUtil.assumeCompatibleDevice()
assumeTrue(CameraUtil.hasCameraWithLensFacing(BACK_LENS_FACING))
createDefaultPictureFolderIfNotExist()
ProcessCameraProvider.configureInstance(cameraXConfig)
cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
withContext(Dispatchers.Main) {
fakeLifecycleOwner = FakeLifecycleOwner()
fakeLifecycleOwner.startAndResume()
}
}
@After
fun tearDown(): Unit = runBlocking {
if (::cameraProvider.isInitialized) {
withContext(Dispatchers.Main) {
cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
}
}
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun capturedImageHasCorrectSize() = runBlocking {
val useCase = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.setTargetRotation(Surface.ROTATION_0)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
var sizeEnvelope = imageProperties.size
// Some devices may not be able to fit the requested resolution. In this case, the returned
// size should be able to enclose 640 x 480.
if (sizeEnvelope != DEFAULT_RESOLUTION) {
val rotationDegrees = imageProperties.rotationDegrees
// If the image data is rotated by 90 or 270, we need to ensure our desired width fits
// within the height of this image and our desired height fits in the width.
if (rotationDegrees == 270 || rotationDegrees == 90) {
sizeEnvelope = Size(sizeEnvelope!!.height, sizeEnvelope.width)
}
// Ensure the width and height can be cropped from the source image
assertThat(sizeEnvelope!!.width).isAtLeast(DEFAULT_RESOLUTION.width)
assertThat(sizeEnvelope.height).isAtLeast(DEFAULT_RESOLUTION.height)
}
}
@Test
fun canCaptureMultipleImages() {
canTakeImages(defaultBuilder, numImages = 5)
}
@Test
fun canCaptureMultipleImagesWithMaxQuality() {
canTakeImages(
ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY),
numImages = 5
)
}
@Test
fun canCaptureMultipleImagesWithZsl() = runBlocking {
val useCase = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build()
var camera: Camera
withContext(Dispatchers.Main) {
camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
if (camera.cameraInfo.isZslSupported) {
val numImages = 5
val callback = FakeImageCaptureCallback(capturesCount = numImages)
for (i in 0 until numImages) {
useCase.takePicture(mainExecutor, callback)
}
callback.awaitCapturesAndAssert(
timeout = numImages * CAPTURE_TIMEOUT,
capturedImagesCount = numImages
)
}
}
@Test
fun canCaptureImageWithFlashModeOn() {
canTakeImages(defaultBuilder.setFlashMode(ImageCapture.FLASH_MODE_ON))
}
@Test
fun canCaptureImageWithFlashModeOn_frontCamera() {
// This test also wants to ensure that the image can be captured without the flash unit.
// Front camera usually doesn't have a flash unit.
canTakeImages(
defaultBuilder.setFlashMode(ImageCapture.FLASH_MODE_ON),
cameraSelector = FRONT_SELECTOR
)
}
@Test
fun canCaptureImageWithFlashModeOnAndUseTorch() {
canTakeImages(
defaultBuilder.setFlashType(ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH)
.setFlashMode(ImageCapture.FLASH_MODE_ON),
)
}
@Test
fun canCaptureImageWithFlashModeOnAndUseTorch_frontCamera() {
// This test also wants to ensure that the image can be captured without the flash unit.
// Front camera usually doesn't have a flash unit.
canTakeImages(
defaultBuilder.setFlashType(ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH)
.setFlashMode(ImageCapture.FLASH_MODE_ON),
cameraSelector = FRONT_SELECTOR
)
}
@Test
fun canCaptureImageWithFlashModeScreen_frontCamera() {
// Front camera usually doesn't have a flash unit. Screen flash will be used in such case.
// Otherwise, physical flash will be used. But capture should be successful either way.
canTakeImages(
defaultBuilder.apply {
setScreenFlashUiControl(MockScreenFlashUiControl())
setFlashMode(ImageCapture.FLASH_MODE_SCREEN)
},
cameraSelector = FRONT_SELECTOR
)
}
@Test
fun canCaptureImageWithFlashModeScreenAndUseTorch_frontCamera() {
// Front camera usually doesn't have a flash unit. Screen flash will be used in such case.
// Otherwise, physical flash will be used as torch. Either way, capture should be successful
canTakeImages(
defaultBuilder.apply {
setFlashType(ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH)
setScreenFlashUiControl(MockScreenFlashUiControl())
setFlashMode(ImageCapture.FLASH_MODE_SCREEN)
},
cameraSelector = FRONT_SELECTOR
)
}
private fun canTakeImages(
builder: ImageCapture.Builder,
cameraSelector: CameraSelector = BACK_SELECTOR,
numImages: Int = 1,
): Unit = runBlocking {
val useCase = builder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = numImages)
repeat(numImages) {
useCase.takePicture(mainExecutor, callback)
}
callback.awaitCapturesAndAssert(
timeout = numImages * CAPTURE_TIMEOUT,
capturedImagesCount = numImages
)
}
@Test
fun saveCanSucceed_withNonExistingFile() {
val saveLocation = temporaryFolder.newFile("test${System.currentTimeMillis()}.jpg")
// make sure file does not exist
if (saveLocation.exists()) {
saveLocation.delete()
}
assertThat(!saveLocation.exists())
canSaveToFile(saveLocation)
}
@Test
fun saveCanSucceed_withExistingFile() {
val saveLocation = temporaryFolder.newFile("test.jpg")
assertThat(saveLocation.exists())
canSaveToFile(saveLocation)
}
@Test
fun saveCanSucceed_toExternalStoragePublicFolderFile() {
val pictureFolder = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
assumeTrue(pictureFolder.exists())
val saveLocation = File(pictureFolder, "test.jpg")
canSaveToFile(saveLocation)
saveLocation.delete()
}
private fun canSaveToFile(saveLocation: File) = runBlocking {
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
mainExecutor,
callback
)
// Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
}
@Test
fun saveToUri(): Unit = runBlocking {
// Arrange.
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build()
val callback = FakeImageSavedCallback(capturesCount = 1)
// Act.
useCase.takePicture(outputFileOptions, mainExecutor, callback)
// Assert: Wait for the signal that the image has been saved
callback.awaitCapturesAndAssert(savedImagesCount = 1)
// Verify save location Uri is available.
val saveLocationUri = callback.results.first().savedUri
assertThat(saveLocationUri).isNotNull()
// Clean up.
context.contentResolver.delete(saveLocationUri!!, null, null)
}
@Test
fun saveToOutputStream() = runBlocking {
// Arrange.
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation = temporaryFolder.newFile("test.jpg")
val callback = FakeImageSavedCallback(capturesCount = 1)
FileOutputStream(saveLocation).use { outputStream ->
// Act.
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(outputStream).build(),
mainExecutor,
callback
)
// Assert: Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
}
}
@Test
fun canSaveFile_withRotation() = runBlocking {
// TODO(b/147448711) Add back in once cuttlefish has correct user cropping functionality.
assumeFalse(
"Cuttlefish does not correctly handle crops. Unable to test.",
Build.MODEL.contains("Cuttlefish")
)
val useCase = ImageCapture.Builder()
.setTargetRotation(Surface.ROTATION_0)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation = temporaryFolder.newFile("test.jpg")
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
mainExecutor,
callback
)
// Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
// Retrieve the exif from the image
val exif = Exif.createFromFile(saveLocation)
val saveLocationRotated90 = temporaryFolder.newFile("testRotated90.jpg")
val callbackRotated90 = FakeImageSavedCallback(capturesCount = 1)
useCase.targetRotation = Surface.ROTATION_90
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocationRotated90).build(),
mainExecutor,
callbackRotated90
)
// Wait for the signal that the image has been saved.
callbackRotated90.awaitCapturesAndAssert(savedImagesCount = 1)
// Retrieve the exif from the image
val exifRotated90 = Exif.createFromFile(saveLocationRotated90)
// Compare aspect ratio with a threshold due to floating point rounding. Can't do direct
// comparison of height and width, because the rotated capture is scaled to fit within
// the sensor region
val aspectRatioThreshold = 0.01
// If rotation is equal then buffers were rotated by HAL so the aspect ratio should be
// rotated by 90 degrees. Otherwise the aspect ratio should be the same.
if (exif.rotation == exifRotated90.rotation) {
val aspectRatio = exif.height.toDouble() / exif.width
val aspectRatioRotated90 = exifRotated90.width.toDouble() / exifRotated90.height
assertThat(abs(aspectRatio - aspectRatioRotated90)).isLessThan(aspectRatioThreshold)
} else {
val aspectRatio = exif.width.toDouble() / exif.height
val aspectRatioRotated90 = exifRotated90.width.toDouble() / exifRotated90.height
assertThat(abs(aspectRatio - aspectRatioRotated90)).isLessThan(aspectRatioThreshold)
}
}
@Test
fun canSaveFile_flippedHorizontal() = runBlocking {
// Use a non-rotated configuration since some combinations of rotation + flipping vertically
// can be equivalent to flipping horizontally
val configBuilder = ImageCapture.Builder.fromConfig(createNonRotatedConfiguration())
val metadata = ImageCapture.Metadata()
metadata.isReversedHorizontal = true
canSaveFileWithMetadata(
configBuilder = configBuilder,
metadata = metadata,
verifyExif = { exif ->
assertThat(exif.isFlippedHorizontally).isTrue()
}
)
}
@Test
fun canSaveFile_flippedVertical() = runBlocking {
// Use a non-rotated configuration since some combinations of rotation + flipping
// horizontally can be equivalent to flipping vertically
val configBuilder = ImageCapture.Builder.fromConfig(createNonRotatedConfiguration())
val metadata = ImageCapture.Metadata()
metadata.isReversedVertical = true
canSaveFileWithMetadata(
configBuilder = configBuilder,
metadata = metadata,
verifyExif = { exif ->
assertThat(exif.isFlippedVertically).isTrue()
}
)
}
// See b/263289024, writing location data might cause the output JPEG image corruption on some
// specific Android 12 devices. This issue happens if:
// 1. The image is not cropped from the original captured image (unnecessary Exif copy is done)
// 2. The inserted location provider is FUSED_PROVIDER
@Test
fun canSaveFile_withFusedProviderLocation() {
val latitudeValue = 50.0
val longitudeValue = -100.0
val metadata = ImageCapture.Metadata().apply {
location = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = latitudeValue
longitude = longitudeValue
}
}
canSaveFileWithMetadata(
defaultBuilder,
metadata,
verifyExif = { exif ->
assertThat(exif.location).isNotNull()
assertThat(exif.location!!.provider).isEqualTo(metadata.location!!.provider)
assertThat(exif.location!!.latitude).isEqualTo(latitudeValue)
assertThat(exif.location!!.longitude).isEqualTo(longitudeValue)
}
)
}
private fun canSaveFileWithMetadata(
configBuilder: ImageCapture.Builder,
metadata: ImageCapture.Metadata,
verifyExif: (Exif) -> Unit
) = runBlocking {
val useCase = configBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation = temporaryFolder.newFile("test.jpg")
val outputFileOptions = ImageCapture.OutputFileOptions
.Builder(saveLocation)
.setMetadata(metadata)
.build()
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(outputFileOptions, mainExecutor, callback)
// Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
// Retrieve the exif from the image
val exif = Exif.createFromFile(saveLocation)
verifyExif(exif)
}
@Test
fun canSaveMultipleFiles() = runBlocking {
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val numImages = 5
val callback = FakeImageSavedCallback(capturesCount = numImages)
for (i in 0 until numImages) {
val saveLocation = temporaryFolder.newFile("test$i.jpg")
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
mainExecutor,
callback
)
}
// Wait for the signal that all the images have been saved.
callback.awaitCapturesAndAssert(
timeout = numImages * CAPTURE_TIMEOUT,
savedImagesCount = numImages
)
}
@Test
fun saveWillFail_whenInvalidFilePathIsUsed() = runBlocking {
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
// Note the invalid path
val saveLocation = File("/not/a/real/path.jpg")
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(saveLocation).build()
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(outputFileOptions, mainExecutor, callback)
// Wait for the signal that saving the image has failed
callback.awaitCapturesAndAssert(errorsCount = 1)
val error = callback.errors.first().imageCaptureError
assertThat(error).isEqualTo(ImageCapture.ERROR_FILE_IO)
}
@kotlin.OptIn(
androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
)
@Test
@OptIn(
markerClass = [ExperimentalCamera2Interop::class]
)
fun camera2InteropCaptureSessionCallbacks() = runBlocking {
val stillCaptureCount = AtomicInteger(0)
val captureCallback = object : CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
if (request.get(CaptureRequest.CONTROL_CAPTURE_INTENT) ==
CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE
) {
stillCaptureCount.incrementAndGet()
}
}
}
val builder = ImageCapture.Builder()
CameraPipeUtil.setCameraCaptureSessionCallback(implName, builder, captureCallback)
val useCase = builder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
// Because interop listener will get both image capture and preview callbacks, ensure
// that there is one CAPTURE_INTENT_STILL_CAPTURE from all onCaptureCompleted() callbacks.
assertThat(stillCaptureCount.get()).isEqualTo(1)
}
@Test
fun takePicture_withBufferFormatRaw10() = runBlocking {
// RAW10 does not work in redmi 8
assumeFalse(Build.DEVICE.equals("olive", ignoreCase = true)) // Redmi 8
val cameraCharacteristics = CameraUtil.getCameraCharacteristics(BACK_LENS_FACING)
val map = cameraCharacteristics!!.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val resolutions = map!!.getOutputSizes(ImageFormat.RAW10)
// Ignore this tests on devices that do not support RAW10 image format.
assumeNotNull(resolutions)
assumeTrue(resolutions!!.isNotEmpty())
assumeTrue(isRawSupported(cameraCharacteristics))
val useCase = ImageCapture.Builder()
.setBufferFormat(ImageFormat.RAW10)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
assertThat(imageProperties.format).isEqualTo(ImageFormat.RAW10)
}
private fun isRawSupported(cameraCharacteristics: CameraCharacteristics): Boolean {
val capabilities =
cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
?: IntArray(0)
return capabilities.any { capability ->
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW == capability
}
}
@SdkSuppress(minSdkVersion = 28)
@Test(expected = IllegalArgumentException::class)
fun constructor_withBufferFormatAndSessionProcessorIsSet_throwsException(): Unit = runBlocking {
val sessionProcessor = FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
inputFormatCapture = ImageFormat.YUV_420_888
)
val imageCapture = ImageCapture.Builder()
.setBufferFormat(ImageFormat.RAW_SENSOR)
.build()
val preview = Preview.Builder().build()
withContext(Dispatchers.Main) {
val cameraSelector =
getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
cameraProvider.bindToLifecycle(
fakeLifecycleOwner, cameraSelector, imageCapture, preview)
}
}
@Test
fun onStateOffline_abortAllCaptureRequests() = runBlocking {
val imageCapture = ImageCapture.Builder().build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
// After the use case can be reused, the capture requests can only be cancelled after the
// onStateAttached() callback has been received. In the normal code flow, the
// onStateDetached() should also come after onStateAttached(). There is no API to
// directly know onStateAttached() callback has been received. Therefore, taking a
// picture and waiting for the capture success callback to know the use case's
// onStateAttached() callback has been received.
val callback = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val callback2 = FakeImageCaptureCallback(capturesCount = 3)
imageCapture.takePicture(mainExecutor, callback2)
imageCapture.takePicture(mainExecutor, callback2)
imageCapture.takePicture(mainExecutor, callback2)
withContext(Dispatchers.Main) {
imageCapture.onStateDetached()
}
callback2.awaitCaptures()
assertThat(callback2.results.size + callback2.errors.size).isEqualTo(3)
assertThat(callback2.errors.size).isAtLeast(1)
for (error in callback2.errors) {
assertThat(error.imageCaptureError).isEqualTo(ImageCapture.ERROR_CAMERA_CLOSED)
}
}
@Test
fun unbind_abortAllCaptureRequests() = runBlocking {
val imageCapture = ImageCapture.Builder().build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
val callback = FakeImageCaptureCallback(capturesCount = 3)
imageCapture.takePicture(mainExecutor, callback)
imageCapture.takePicture(mainExecutor, callback)
imageCapture.takePicture(mainExecutor, callback)
// Needs to run on main thread because takePicture gets posted on main thread if it isn't
// running on the main thread. Which means the internal ImageRequests likely get issued
// after ImageCapture is removed so errors out with a different error from
// ERROR_CAMERA_CLOSED
withContext(Dispatchers.Main) {
cameraProvider.unbind(imageCapture)
}
// Wait for the signal that the image capture has failed.
callback.awaitCapturesAndAssert(errorsCount = 3)
assertThat(callback.results.size + callback.errors.size).isEqualTo(3)
for (error in callback.errors) {
assertThat(error.imageCaptureError).isEqualTo(ImageCapture.ERROR_CAMERA_CLOSED)
}
}
@Test
fun takePictureReturnsErrorNO_CAMERA_whenNotBound() = runBlocking {
val imageCapture = ImageCapture.Builder().build()
val callback = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image capture has failed.
callback.awaitCapturesAndAssert(errorsCount = 1)
val error = callback.errors.first()
assertThat(error.imageCaptureError).isEqualTo(ImageCapture.ERROR_INVALID_CAMERA)
}
@Test
fun defaultAspectRatioWillBeSet_whenTargetResolutionIsNotSet() = runBlocking {
val useCase = ImageCapture.Builder().build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val config = useCase.currentConfig as ImageOutputConfig
assertThat(config.targetAspectRatio).isEqualTo(AspectRatio.RATIO_4_3)
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun defaultAspectRatioWillBeSet_whenRatioDefaultIsSet() = runBlocking {
val useCase = ImageCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_DEFAULT).build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val config = useCase.currentConfig as ImageOutputConfig
assertThat(config.targetAspectRatio).isEqualTo(AspectRatio.RATIO_4_3)
}
@Suppress("DEPRECATION") // legacy resolution API
@Test
fun defaultAspectRatioWontBeSet_whenTargetResolutionIsSet() = runBlocking {
val useCase = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.build()
assertThat(
useCase.currentConfig.containsOption(
ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO
)
).isFalse()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
assertThat(
useCase.currentConfig.containsOption(
ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO
)
).isFalse()
}
@Test
fun targetRotationCanBeUpdatedAfterUseCaseIsCreated() {
val imageCapture = ImageCapture.Builder()
.setTargetRotation(Surface.ROTATION_0)
.build()
imageCapture.targetRotation = Surface.ROTATION_90
assertThat(imageCapture.targetRotation).isEqualTo(Surface.ROTATION_90)
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun targetResolutionIsUpdatedAfterTargetRotationIsUpdated() = runBlocking {
val imageCapture = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.setTargetRotation(Surface.ROTATION_0)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
// Updates target rotation from ROTATION_0 to ROTATION_90.
imageCapture.targetRotation = Surface.ROTATION_90
val newConfig = imageCapture.currentConfig as ImageOutputConfig
val expectedTargetResolution = Size(DEFAULT_RESOLUTION.height, DEFAULT_RESOLUTION.width)
// Expected targetResolution will be reversed from original target resolution.
assertThat(newConfig.targetResolution).isEqualTo(expectedTargetResolution)
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun capturedImageHasCorrectCroppingSizeWithoutSettingRotation() {
val useCase = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.build()
capturedImageHasCorrectCroppingSize(
useCase,
rotateCropRect = { capturedImageRotationDegrees ->
capturedImageRotationDegrees % 180 != 0
}
)
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun capturedImageHasCorrectCroppingSizeSetRotationBuilder() {
// Checks camera device sensor degrees to set correct target rotation value to make sure
// that the initial set target cropping aspect ratio matches the sensor orientation.
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_LENS_FACING)
val isRotateNeeded = sensorOrientation!! % 180 != 0
val useCase = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.setTargetRotation(if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0)
.build()
capturedImageHasCorrectCroppingSize(
useCase,
rotateCropRect = { capturedImageRotationDegrees ->
capturedImageRotationDegrees % 180 != 0
}
)
}
@Suppress("DEPRECATION") // test for legacy resolution API
@Test
fun capturedImageHasCorrectCroppingSize_setUseCaseRotation90FromRotationInBuilder() {
// Checks camera device sensor degrees to set correct target rotation value to make sure
// that the initial set target cropping aspect ratio matches the sensor orientation.
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_LENS_FACING)
val isRotateNeeded = sensorOrientation!! % 180 != 0
val useCase = ImageCapture.Builder()
.setTargetResolution(DEFAULT_RESOLUTION)
.setTargetRotation(if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0)
.build()
// Updates target rotation to opposite one.
useCase.targetRotation = if (isRotateNeeded) Surface.ROTATION_0 else Surface.ROTATION_90
capturedImageHasCorrectCroppingSize(
useCase,
rotateCropRect = { capturedImageRotationDegrees ->
capturedImageRotationDegrees % 180 == 0
}
)
}
private fun capturedImageHasCorrectCroppingSize(
useCase: ImageCapture,
rotateCropRect: (Int) -> Boolean
) = runBlocking {
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
// After target rotation is updated, the result cropping aspect ratio should still the
// same as original one.
val expectedCroppingRatio = Rational(DEFAULT_RESOLUTION.width, DEFAULT_RESOLUTION.height)
val imageProperties = callback.results.first()
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
// displayed image
val resultCroppingRatio: Rational = if (rotateCropRect(imageProperties.rotationDegrees)) {
Rational(cropRect!!.height(), cropRect.width())
} else {
Rational(cropRect!!.width(), cropRect.height())
}
assertThat(resultCroppingRatio.toFloat()).isWithin(TOLERANCE)
.of(expectedCroppingRatio.toFloat())
if (imageProperties.format == ImageFormat.JPEG && isRotationOptionSupportedDevice()) {
assertThat(imageProperties.rotationDegrees).isEqualTo(imageProperties.exif!!.rotation)
}
}
@Test
fun capturedImageHasCorrectCroppingSize_setCropAspectRatioAfterBindToLifecycle() = runBlocking {
val useCase = ImageCapture.Builder().build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
// Checks camera device sensor degrees to set target cropping aspect ratio match the
// sensor orientation.
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_LENS_FACING)
val isRotateNeeded = sensorOrientation!! % 180 != 0
// Set the default aspect ratio of ImageCapture to the target cropping aspect ratio.
val targetCroppingAspectRatio = if (isRotateNeeded) Rational(3, 4) else Rational(4, 3)
useCase.setCropAspectRatio(targetCroppingAspectRatio)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
// After target rotation is updated, the result cropping aspect ratio should still the
// same as original one.
val imageProperties = callback.results.first()
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
// displayed image
val resultCroppingRatio: Rational = if (imageProperties.rotationDegrees % 180 != 0) {
Rational(cropRect!!.height(), cropRect.width())
} else {
Rational(cropRect!!.width(), cropRect.height())
}
if (imageProperties.format == ImageFormat.JPEG && isRotationOptionSupportedDevice()) {
assertThat(imageProperties.rotationDegrees).isEqualTo(
imageProperties.exif!!.rotation
)
}
// Compare aspect ratio with a threshold due to floating point rounding. Can't do direct
// comparison of height and width, because the target aspect ratio of ImageCapture will
// be corrected in API 21 Legacy devices and the captured image will be scaled to fit
// within the cropping aspect ratio.
val aspectRatioThreshold = 0.01
assertThat(
abs(resultCroppingRatio.toDouble() - targetCroppingAspectRatio.toDouble())
).isLessThan(aspectRatioThreshold)
}
@Test
fun capturedImageHasCorrectCroppingSize_viewPortOverwriteCropAspectRatio() = runBlocking {
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_LENS_FACING)
val isRotateNeeded = sensorOrientation!! % 180 != 0
val useCase = ImageCapture.Builder()
.setTargetRotation(if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0)
.build()
// Sets a crop aspect ratio to the use case. This will be overwritten by the view port
// setting.
val useCaseCroppingAspectRatio = Rational(4, 3)
useCase.setCropAspectRatio(useCaseCroppingAspectRatio)
// Sets view port with different aspect ratio and then attach the use case
val viewPortAspectRatio = Rational(2, 1)
val viewPort = ViewPort.Builder(
viewPortAspectRatio,
if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0
).build()
val useCaseGroup = UseCaseGroup.Builder().setViewPort(viewPort).addUseCase(useCase).build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCaseGroup)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
// After target rotation is updated, the result cropping aspect ratio should still the
// same as original one.
val imageProperties = callback.results.first()
val cropRect = imageProperties.cropRect
// Rotate the captured ImageProxy's crop rect into the coordinate space of the final
// displayed image
val resultCroppingRatio: Rational = if (imageProperties.rotationDegrees % 180 != 0) {
Rational(cropRect!!.height(), cropRect.width())
} else {
Rational(cropRect!!.width(), cropRect.height())
}
if (imageProperties.format == ImageFormat.JPEG && isRotationOptionSupportedDevice()) {
assertThat(imageProperties.rotationDegrees).isEqualTo(
imageProperties.exif!!.rotation
)
}
// Compare aspect ratio with a threshold due to floating point rounding. Can't do direct
// comparison of height and width, because the target aspect ratio of ImageCapture will
// be corrected in API 21 Legacy devices and the captured image will be scaled to fit
// within the cropping aspect ratio.
val aspectRatioThreshold = 0.01
assertThat(
abs(resultCroppingRatio.toDouble() - viewPortAspectRatio.toDouble())
).isLessThan(aspectRatioThreshold)
}
@Test
fun useCaseConfigCanBeReset_afterUnbind() = runBlocking {
val useCase = defaultBuilder.build()
val initialConfig = useCase.currentConfig
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
withContext(Dispatchers.Main) {
cameraProvider.unbind(useCase)
}
val configAfterUnbinding = useCase.currentConfig
assertThat(initialConfig == configAfterUnbinding).isTrue()
}
@Test
fun targetRotationIsRetained_whenUseCaseIsReused() = runBlocking {
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
// Generally, the device can't be rotated to Surface.ROTATION_180. Therefore,
// use it to do the test.
useCase.targetRotation = Surface.ROTATION_180
withContext(Dispatchers.Main) {
// Unbind the use case.
cameraProvider.unbind(useCase)
}
// Check the target rotation is kept when the use case is unbound.
assertThat(useCase.targetRotation).isEqualTo(Surface.ROTATION_180)
// Check the target rotation is kept when the use case is rebound to the
// lifecycle.
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
assertThat(useCase.targetRotation).isEqualTo(Surface.ROTATION_180)
}
@Test
fun cropAspectRatioIsRetained_whenUseCaseIsReused() = runBlocking {
val useCase = defaultBuilder.build()
val cropAspectRatio = Rational(1, 1)
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
useCase.setCropAspectRatio(cropAspectRatio)
withContext(Dispatchers.Main) {
// Unbind the use case.
cameraProvider.unbind(useCase)
}
// Rebind the use case.
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
val cropRect = imageProperties.cropRect
val cropRectAspectRatio = Rational(cropRect!!.height(), cropRect.width())
// The crop aspect ratio could be kept after the use case is reused. So that the aspect
// of the result cropRect is 1:1.
assertThat(cropRectAspectRatio).isEqualTo(cropAspectRatio)
}
@Test
fun useCaseCanBeReusedInSameCamera() = runBlocking {
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation1 = temporaryFolder.newFile("test1.jpg")
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation1).build(),
mainExecutor,
callback
)
// Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
withContext(Dispatchers.Main) {
// Unbind the use case.
cameraProvider.unbind(useCase)
}
// Rebind the use case to the same camera.
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation2 = temporaryFolder.newFile("test2.jpg")
val callback2 = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation2).build(),
mainExecutor,
callback2
)
// Wait for the signal that the image has been saved.
callback2.awaitCapturesAndAssert(savedImagesCount = 1)
}
@Test
fun useCaseCanBeReusedInDifferentCamera() = runBlocking {
assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
val useCase = defaultBuilder.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val saveLocation1 = temporaryFolder.newFile("test1.jpg")
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation1).build(),
mainExecutor,
callback
)
// Wait for the signal that the image has been saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
withContext(Dispatchers.Main) {
// Unbind the use case.
cameraProvider.unbind(useCase)
}
// Rebind the use case to different camera.
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
CameraSelector.DEFAULT_FRONT_CAMERA,
useCase
)
}
val saveLocation2 = temporaryFolder.newFile("test2.jpg")
val callback2 = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation2).build(),
mainExecutor,
callback2
)
// Wait for the signal that the image has been saved.
callback2.awaitCapturesAndAssert(savedImagesCount = 1)
}
@Test
fun returnValidTargetRotation_afterUseCaseIsCreated() {
val imageCapture = ImageCapture.Builder().build()
assertThat(imageCapture.targetRotation).isNotEqualTo(ImageOutputConfig.INVALID_ROTATION)
}
@Test
fun returnCorrectTargetRotation_afterUseCaseIsAttached() = runBlocking {
val imageCapture = ImageCapture.Builder()
.setTargetRotation(Surface.ROTATION_180)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
assertThat(imageCapture.targetRotation).isEqualTo(Surface.ROTATION_180)
}
@Test
fun returnDefaultFlashMode_beforeUseCaseIsAttached() {
val imageCapture = ImageCapture.Builder().build()
assertThat(imageCapture.flashMode).isEqualTo(ImageCapture.FLASH_MODE_OFF)
}
@Test
fun returnCorrectFlashMode_afterUseCaseIsAttached() = runBlocking {
val imageCapture = ImageCapture.Builder()
.setFlashMode(ImageCapture.FLASH_MODE_ON)
.build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
assertThat(imageCapture.flashMode).isEqualTo(ImageCapture.FLASH_MODE_ON)
}
@Test
fun returnJpegImage_whenSoftwareJpegIsEnabled() = runBlocking {
val builder = ImageCapture.Builder()
// Enables software Jpeg
builder.mutableConfig.insertOption(
ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
true
)
val useCase = builder.build()
var camera: Camera
withContext(Dispatchers.Main) {
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
BACK_SELECTOR,
useCase,
Preview.Builder().build().apply {
setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
}
)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees).isEqualTo(
camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
)
// Check the output format is correct.
assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
}
@Test
fun canSaveJpegFileWithRotation_whenSoftwareJpegIsEnabled() = runBlocking {
val builder = ImageCapture.Builder()
// Enables software Jpeg
builder.mutableConfig.insertOption(
ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
true
)
val useCase = builder.build()
var camera: Camera
withContext(Dispatchers.Main) {
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
BACK_SELECTOR,
useCase,
Preview.Builder().build().apply {
setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
}
)
}
val saveLocation = temporaryFolder.newFile("test.jpg")
val callback = FakeImageSavedCallback(capturesCount = 1)
useCase.takePicture(
ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
mainExecutor, callback)
// Wait for the signal that the image has been captured and saved.
callback.awaitCapturesAndAssert(savedImagesCount = 1)
// For YUV to JPEG case, the rotation will only be in Exif.
val exif = Exif.createFromFile(saveLocation)
assertThat(exif.rotation).isEqualTo(
camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation))
}
@Test
fun returnYuvImage_withYuvBufferFormat() = runBlocking {
val builder = ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888)
val useCase = builder.build()
var camera: Camera
withContext(Dispatchers.Main) {
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
BACK_SELECTOR,
useCase,
Preview.Builder().build().apply {
setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
}
)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees).isEqualTo(
camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
)
// Check the output format is correct.
assertThat(imageProperties.format).isEqualTo(ImageFormat.YUV_420_888)
}
@Test
fun returnYuvImage_whenSoftwareJpegIsEnabledWithYuvBufferFormat() = runBlocking {
val builder = ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888)
// Enables software Jpeg
builder.mutableConfig.insertOption(
ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
true
)
val useCase = builder.build()
var camera: Camera
withContext(Dispatchers.Main) {
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
BACK_SELECTOR,
useCase,
Preview.Builder().build().apply {
setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
}
)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
useCase.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees).isEqualTo(
camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
)
// Check the output format is correct.
assertThat(imageProperties.format).isEqualTo(ImageFormat.YUV_420_888)
}
@Test
@SdkSuppress(minSdkVersion = 28)
fun returnJpegImage_whenSessionProcessorIsSet() = runBlocking {
assumeTrue(
"TODO(b/275493663): Enable when camera-pipe has extensions support",
implName != CameraPipeConfig::class.simpleName
)
val builder = ImageCapture.Builder()
val sessionProcessor = FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
inputFormatCapture = ImageFormat.YUV_420_888
)
val imageCapture = builder.build()
val preview = Preview.Builder().build()
var camera: Camera
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
val cameraSelector =
getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner, cameraSelector, imageCapture, preview
)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
// Check the output image rotation degrees value is correct.
assertThat(imageProperties.rotationDegrees).isEqualTo(
camera.cameraInfo.getSensorRotationDegrees(imageCapture.targetRotation)
)
// Check the output format is correct.
assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
}
@Test
@SdkSuppress(minSdkVersion = 28)
fun returnJpegImage_whenSessionProcessorIsSet_outputFormatJpeg() = runBlocking {
assumeFalse(
"Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
Build.MODEL.contains("Cuttlefish")
)
val sessionProcessor = FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
inputFormatCapture = null
)
val imageCapture = ImageCapture.Builder().build()
val preview = Preview.Builder().build()
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
val cameraSelector =
getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
cameraProvider.bindToLifecycle(
fakeLifecycleOwner, cameraSelector, imageCapture, preview
)
}
val callback = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
// Check the output image rotation degrees value is correct.
if (isRotationOptionSupportedDevice()) {
assertThat(imageProperties.rotationDegrees).isEqualTo(imageProperties.exif!!.rotation)
}
// Check the output format is correct.
assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
}
@Test
fun canCaptureImage_whenOnlyImageCaptureBound_withYuvBufferFormat() {
val cameraHwLevel = CameraUtil.getCameraCharacteristics(CameraSelector.LENS_FACING_BACK)
?.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
assumeTrue(
"TODO(b/298138582): Check if MeteringRepeating will need to be added while" +
" choosing resolution for ImageCapture",
cameraHwLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
cameraHwLevel != CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
)
canTakeImages(ImageCapture.Builder().apply {
setBufferFormat(ImageFormat.YUV_420_888)
})
}
private fun getCameraSelectorWithSessionProcessor(
cameraSelector: CameraSelector,
sessionProcessor: SessionProcessor
): CameraSelector {
val identifier = Identifier.create("idStr")
ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
object : CameraConfig {
override fun getConfig(): Config {
return MutableOptionsBundle.create()
}
override fun getCompatibilityId(): Identifier {
return Identifier.create(0)
}
override fun getSessionProcessor(
valueIfMissing: SessionProcessor?
): SessionProcessor? {
return sessionProcessor
}
override fun getSessionProcessor(): SessionProcessor {
return sessionProcessor
}
}
}
val builder = CameraSelector.Builder.fromSelector(cameraSelector)
builder.addCameraFilter(object : CameraFilter {
override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
val newCameraInfos = mutableListOf<CameraInfo>()
newCameraInfos.addAll(cameraInfos)
return newCameraInfos
}
override fun getIdentifier(): Identifier {
return identifier
}
})
return builder.build()
}
@kotlin.OptIn(
androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
)
@Test
fun unbindPreview_imageCapturingShouldSuccess() = runBlocking {
// Arrange.
val imageCapture = ImageCapture.Builder().build()
val previewStreamReceived = CompletableDeferred<Boolean>()
val preview = Preview.Builder().also {
CameraPipeUtil.setCameraCaptureSessionCallback(
implName,
it,
object : CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
previewStreamReceived.complete(true)
}
})
}.build()
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
cameraProvider.bindToLifecycle(
fakeLifecycleOwner, BACK_SELECTOR, imageCapture, preview
)
}
assertWithMessage("Preview doesn't start").that(
previewStreamReceived.awaitWithTimeoutOrNull()
).isTrue()
// Act.
val callback = FakeImageCaptureCallback(capturesCount = 1)
withContext(Dispatchers.Main) {
// Test the reproduce step in b/235119898
cameraProvider.unbind(preview)
imageCapture.takePicture(mainExecutor, callback)
}
// Assert.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
}
@Test
fun unbindVideoCaptureWithoutStartingRecorder_imageCapturingShouldSuccess() = runBlocking {
// Arrange.
val imageCapture = ImageCapture.Builder().build()
val videoCapture = VideoCapture.Builder<Recorder>(Recorder.Builder().build()).build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(
fakeLifecycleOwner, BACK_SELECTOR, imageCapture, videoCapture
)
}
// wait for camera to start by taking a picture
val callback1 = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback1)
try {
callback1.awaitCapturesAndAssert(capturedImagesCount = 1)
} catch (e: AssertionError) {
assumeNoException("image capture failed, camera might not have started yet", e)
}
// Act.
val callback2 = FakeImageCaptureCallback(capturesCount = 1)
withContext(Dispatchers.Main) {
cameraProvider.unbind(videoCapture)
imageCapture.takePicture(mainExecutor, callback2)
}
// Assert.
callback2.awaitCapturesAndAssert(capturedImagesCount = 1)
}
@Test
fun capturedImage_withHighResolutionEnabled_imageCaptureOnly() = runBlocking {
capturedImage_withHighResolutionEnabled()
}
@Test
fun capturedImage_withHighResolutionEnabled_imageCapturePreviewImageAnalysis() = runBlocking {
val preview = Preview.Builder().build().also {
withContext(Dispatchers.Main) {
it.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
}
}
val imageAnalysis = ImageAnalysis.Builder().build().also { imageAnalysis ->
imageAnalysis.setAnalyzer(Dispatchers.Default.asExecutor()) { imageProxy ->
imageProxy.close()
}
}
capturedImage_withHighResolutionEnabled(preview, imageAnalysis)
}
@Test
@SdkSuppress(minSdkVersion = 28)
fun getRealtimeCaptureLatencyEstimate_whenSessionProcessorSupportsRealtimeLatencyEstimate() =
runBlocking {
val expectedCaptureLatencyMillis = 1000L
val expectedProcessingLatencyMillis = 100L
val sessionProcessor = object : SessionProcessor by FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
inputFormatCapture = null
) {
override fun getRealtimeCaptureLatency(): Pair<Long, Long> =
Pair(expectedCaptureLatencyMillis, expectedProcessingLatencyMillis)
}
val imageCapture = ImageCapture.Builder().build()
val preview = Preview.Builder().build()
withContext(Dispatchers.Main) {
preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
val cameraSelector =
getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
cameraProvider.bindToLifecycle(
fakeLifecycleOwner, cameraSelector, imageCapture, preview
)
}
val latencyEstimate = imageCapture.realtimeCaptureLatencyEstimate
// Check the realtime latency estimate is correct.
assertThat(latencyEstimate.captureLatencyMillis).isEqualTo(expectedCaptureLatencyMillis)
assertThat(latencyEstimate.processingLatencyMillis).isEqualTo(
expectedProcessingLatencyMillis
)
}
@Test
fun resolutionSelectorConfigCorrectlyMerged_afterBindToLifecycle() = runBlocking {
val resolutionFilter = ResolutionFilter { supportedSizes, _ -> supportedSizes }
val useCase = ImageCapture.Builder().setResolutionSelector(
ResolutionSelector.Builder().setResolutionFilter(resolutionFilter)
.setAllowedResolutionMode(PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE).build()
).build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
}
val resolutionSelector = useCase.currentConfig.retrieveOption(OPTION_RESOLUTION_SELECTOR)
// The default 4:3 AspectRatioStrategy is kept
assertThat(resolutionSelector!!.aspectRatioStrategy).isEqualTo(
AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY
)
// The default highest available ResolutionStrategy is kept
assertThat(resolutionSelector.resolutionStrategy).isEqualTo(
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
)
// The set resolutionFilter is kept
assertThat(resolutionSelector.resolutionFilter).isEqualTo(resolutionFilter)
// The set allowedResolutionMode is kept
assertThat(resolutionSelector.allowedResolutionMode).isEqualTo(
PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
)
}
private fun capturedImage_withHighResolutionEnabled(
preview: Preview? = null,
imageAnalysis: ImageAnalysis? = null
) = runBlocking {
// TODO(b/247492645) Remove camera-pipe-integration restriction after porting
// ResolutionSelector logic
assumeTrue(implName != CameraPipeConfig::class.simpleName)
val maxHighResolutionOutputSize = CameraUtil.getMaxHighResolutionOutputSizeWithLensFacing(
BACK_SELECTOR.lensFacing!!,
ImageFormat.JPEG
)
// Only runs the test when the device has high resolution output sizes
assumeTrue(maxHighResolutionOutputSize != null)
val resolutionSelector = ResolutionSelector.Builder()
.setAllowedResolutionMode(PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE)
.setResolutionFilter { _, _ ->
listOf(maxHighResolutionOutputSize)
}
.build()
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_SELECTOR.lensFacing!!)
// Sets the target rotation to the camera sensor orientation to avoid the captured image
// buffer data rotated by the HAL and impact the final image resolution check
val targetRotation = if (sensorOrientation!! % 180 == 0) {
Surface.ROTATION_0
} else {
Surface.ROTATION_90
}
val imageCapture = ImageCapture.Builder()
.setResolutionSelector(resolutionSelector)
.setTargetRotation(targetRotation)
.build()
val useCases = arrayListOf<UseCase>(imageCapture)
preview?.let { useCases.add(it) }
imageAnalysis?.let { useCases.add(it) }
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
BACK_SELECTOR,
*useCases.toTypedArray()
)
}
assertThat(imageCapture.resolutionInfo!!.resolution).isEqualTo(maxHighResolutionOutputSize)
val callback = FakeImageCaptureCallback(capturesCount = 1)
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
val imageProperties = callback.results.first()
assertThat(imageProperties.size).isEqualTo(maxHighResolutionOutputSize)
}
/**
* See b/288828159 for the detailed info of the issue
*/
@Test
fun jpegImageZeroPaddingDataDetectionTest(): Unit = runBlocking {
val imageCapture = ImageCapture.Builder().build()
withContext(Dispatchers.Main) {
cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
}
val latch = CountdownDeferred(1)
var errors: Exception? = null
val callback = object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val planes = image.planes
val buffer = planes[0].buffer
val data = ByteArray(buffer.capacity())
buffer.rewind()
buffer[data]
image.close()
val invalidJpegDataParser = InvalidJpegDataParser()
// Only checks the unnecessary zero padding data when the device is not included in
// the LargeJpegImageQuirk device list. InvalidJpegDataParser#getValidDataLength()
// should have returned the valid data length to avoid the extremely large JPEG
// file issue.
if (invalidJpegDataParser.getValidDataLength(data) == data.size &&
containsZeroPaddingDataAfterEoi(data)
) {
errors = Exception("UNNECESSARY_JPEG_ZERO_PADDING_DATA_DETECTED!")
}
latch.countDown()
}
override fun onError(exception: ImageCaptureException) {
errors = exception
latch.countDown()
}
}
imageCapture.takePicture(mainExecutor, callback)
// Wait for the signal that the image has been captured.
assertThat(withTimeoutOrNull(CAPTURE_TIMEOUT) {
latch.await()
}).isNotNull()
assertThat(errors).isNull()
}
/**
* This util function is only used to detect the unnecessary zero padding data after EOI. It
* will directly return false when it fails to parse the JPEG byte array data.
*/
private fun containsZeroPaddingDataAfterEoi(bytes: ByteArray): Boolean {
val jfifEoiMarkEndPosition = InvalidJpegDataParser.getJfifEoiMarkEndPosition(bytes)
// Directly returns false when EOI mark can't be found.
if (jfifEoiMarkEndPosition == -1) {
return false
}
// Will check 1mb data to know whether unnecessary zero padding data exists or not.
// Directly returns false when the data length is long enough
val dataLengthToDetect = 1_000_000
if (jfifEoiMarkEndPosition + dataLengthToDetect > bytes.size) {
return false
}
// Checks that there are at least continuous 1mb of unnecessary zero padding data after EOI
for (position in jfifEoiMarkEndPosition..jfifEoiMarkEndPosition + dataLengthToDetect) {
if (bytes[position] != 0x00.toByte()) {
return false
}
}
return true
}
private fun createNonRotatedConfiguration(): ImageCaptureConfig {
// Create a configuration with target rotation that matches the sensor rotation.
// This assumes a back-facing camera (facing away from screen)
val sensorRotation = CameraUtil.getSensorOrientation(BACK_LENS_FACING)
val surfaceRotation = CameraOrientationUtil.degreesToSurfaceRotation(
sensorRotation!!
)
return ImageCapture.Builder()
.setTargetRotation(surfaceRotation)
.useCaseConfig
}
@Suppress("DEPRECATION")
private fun createDefaultPictureFolderIfNotExist() {
val pictureFolder = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
if (!pictureFolder.exists()) {
pictureFolder.mkdir()
}
}
/**
* See ImageCaptureRotationOptionQuirk. Some real devices or emulator do not support the
* capture rotation option correctly. The capture rotation option setting can't be correctly
* applied to the exif metadata of the captured images. Therefore, the exif rotation related
* verification in the tests needs to be ignored on these devices or emulator.
*/
private fun isRotationOptionSupportedDevice() =
ExifRotationAvailability().isRotationOptionSupported
private class ImageProperties(
val size: Size? = null,
val format: Int = -1,
val rotationDegrees: Int = -1,
val cropRect: Rect? = null,
val exif: Exif? = null,
)
private class FakeImageCaptureCallback(capturesCount: Int) :
ImageCapture.OnImageCapturedCallback() {
private val latch = CountdownDeferred(capturesCount)
val results = mutableListOf<ImageProperties>()
val errors = mutableListOf<ImageCaptureException>()
override fun onCaptureSuccess(image: ImageProxy) {
results.add(
ImageProperties(
size = Size(image.width, image.height),
format = image.format,
rotationDegrees = image.imageInfo.rotationDegrees,
cropRect = image.cropRect,
exif = getExif(image),
)
)
image.close()
latch.countDown()
}
override fun onError(exception: ImageCaptureException) {
errors.add(exception)
latch.countDown()
}
private fun getExif(image: ImageProxy): Exif? {
if (image.format == ImageFormat.JPEG) {
val planes = image.planes
val buffer = planes[0].buffer
val data = ByteArray(buffer.capacity())
buffer[data]
return Exif.createFromInputStream(ByteArrayInputStream(data))
}
return null
}
suspend fun awaitCaptures(timeout: Long = CAPTURE_TIMEOUT) {
assertThat(withTimeoutOrNull(timeout) {
latch.await()
}).isNotNull()
}
suspend fun awaitCapturesAndAssert(
timeout: Long = CAPTURE_TIMEOUT,
capturedImagesCount: Int = 0,
errorsCount: Int = 0
) {
assertThat(withTimeoutOrNull(timeout) {
latch.await()
}).isNotNull()
assertThat(results.size).isEqualTo(capturedImagesCount)
assertThat(errors.size).isEqualTo(errorsCount)
}
}
private class FakeImageSavedCallback(capturesCount: Int) :
ImageCapture.OnImageSavedCallback {
private val latch = CountdownDeferred(capturesCount)
val results = mutableListOf<ImageCapture.OutputFileResults>()
val errors = mutableListOf<ImageCaptureException>()
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
results.add(outputFileResults)
latch.countDown()
}
override fun onError(exception: ImageCaptureException) {
errors.add(exception)
latch.countDown()
}
suspend fun awaitCapturesAndAssert(
timeout: Long = CAPTURE_TIMEOUT,
savedImagesCount: Int = 0,
errorsCount: Int = 0
) {
assertThat(withTimeoutOrNull(timeout) {
latch.await()
}).isNotNull()
assertThat(results.size).isEqualTo(savedImagesCount)
assertThat(errors.size).isEqualTo(errorsCount)
}
}
private class CountdownDeferred(count: Int) {
private val deferredItems = mutableListOf<CompletableDeferred<Unit>>().apply {
repeat(count) { add(CompletableDeferred()) }
}
private var index = 0
fun countDown() {
deferredItems[index++].complete(Unit)
}
suspend fun await() {
deferredItems.forEach { it.await() }
}
}
private suspend fun <T> Deferred<T>.awaitWithTimeoutOrNull(
timeMillis: Long = TimeUnit.SECONDS.toMillis(5)
): T? {
return withTimeoutOrNull(timeMillis) {
await()
}
}
}