blob: 26c6fd45c9cf259d05e45ba236f351bfba65a08b [file] [log] [blame]
/*
* Copyright 2023 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.content.Context
import android.graphics.Rect
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.camera2.pipe.integration.adapter.ZoomValue
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageCapture
import androidx.camera.core.ZoomState
import androidx.camera.integration.core.util.CameraPipeUtil
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.WakelockEmptyActivityRule
import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.concurrent.futures.await
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.abs
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert
import org.junit.Assume.assumeFalse
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.runner.RunWith
import org.junit.runners.Parameterized
@LargeTest
@RunWith(Parameterized::class)
@SdkSuppress(minSdkVersion = 21)
class ZoomControlDeviceTest(
private val selectorName: String,
private val cameraSelector: CameraSelector,
private val implName: String,
private val cameraConfig: CameraXConfig
) {
@get:Rule
val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
active = implName == CameraPipeConfig::class.simpleName,
)
@get:Rule
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
CameraUtil.PreTestCameraIdList(cameraConfig)
)
@get:Rule
val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var camera: Camera
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
private lateinit var cameraControl: CameraControl
private lateinit var cameraInfo: CameraInfo
private lateinit var captureCallback: CaptureCallback
@Before
fun setUp(): Unit = runBlocking {
assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
ProcessCameraProvider.configureInstance(cameraConfig)
cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
captureCallback = CaptureCallback(200)
withContext(Dispatchers.Main) {
fakeLifecycleOwner = FakeLifecycleOwner()
fakeLifecycleOwner.startAndResume()
camera = cameraProvider.bindToLifecycle(
fakeLifecycleOwner,
cameraSelector,
ImageCapture.Builder().also { builder ->
CameraPipeUtil.setCameraCaptureSessionCallback(
implName,
builder,
captureCallback
)
}.build()
)
}
cameraControl = camera.cameraControl
cameraInfo = camera.cameraInfo
}
@After
fun tearDown(): Unit = runBlocking {
if (::cameraProvider.isInitialized) {
withContext(Dispatchers.Main) {
cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
}
}
@Test
fun setZoomRatio_futuresCompletes() {
assumeTrue(cameraInfo.zoomState.value!!.maxZoomRatio + DELTA >= 2.0f)
// use ratio with fraction because it often causes unable-to-complete issue.
val result = cameraControl.setZoomRatio(1.3640054f)
assertFutureCompletes(result)
}
@Test
fun rebindAndSetZoomRatio_futureCompletes() = runBlocking {
withContext(Dispatchers.Main) {
cameraProvider.unbindAll()
val useCase = ImageCapture.Builder().build()
cameraProvider.bindToLifecycle(fakeLifecycleOwner, cameraSelector, useCase)
}
val result = cameraControl.setZoomRatio(1.0f)
assertFutureCompletes(result)
}
@Test
fun setZoomRatio_getValueIsCorrect() = runBlocking {
val newZoomRatio = 2.0f
assumeTrue(newZoomRatio <= cameraInfo.zoomState.value!!.maxZoomRatio + DELTA)
cameraControl.setZoomRatio(newZoomRatio).await()
assertThat(cameraInfo.zoomState.value?.zoomRatio)
.isEqualTo(newZoomRatio)
}
@Test
fun setZoomRatio_largerThanMax_zoomUnmodified() = runBlocking {
assumeTrue(2.0f <= cameraInfo.zoomState.value!!.maxZoomRatio + DELTA)
cameraControl.setZoomRatio(2.0f)[5, TimeUnit.SECONDS]
val maxZoomRatio = cameraInfo.zoomState.value!!.maxZoomRatio
/**
* The exception is caught but not handled here intentionally. Because in this test,
* we want to focus on the value of the zoomRatio after exception is thrown.
* The exception itself is tested with [setZoomRatio_largerThanMax_OutOfRangeException]
*/
try {
cameraControl.setZoomRatio(maxZoomRatio + 1.0f)[5, TimeUnit.SECONDS]
} catch (_: ExecutionException) {
}
assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
}
@Test
fun setZoomRatio_largerThanMax_OutOfRangeException() = runBlocking {
val maxZoomRatio = cameraInfo.zoomState.value!!.maxZoomRatio
val result = cameraControl.setZoomRatio(maxZoomRatio + 1.0f)
assertFutureThrowsIllegalArgumentException(result)
}
@Test
fun setZoomRatio_smallerThanMin_zoomUnmodified() = runBlocking {
assumeTrue(2.0f <= cameraInfo.zoomState.value!!.maxZoomRatio + DELTA)
cameraControl.setZoomRatio(2.0f)[5, TimeUnit.SECONDS]
val minZoomRatio = cameraInfo.zoomState.value!!.minZoomRatio
/**
* The exception is caught but not handled here intentionally. Because in this test,
* we want to focus on the value of the zoomRatio after exception is thrown.
* The exception itself is tested with [setZoomRatio_smallerThanMin_OutOfRangeException]
*/
try {
cameraControl.setZoomRatio(minZoomRatio - 1.0f)[5, TimeUnit.SECONDS]
} catch (_: ExecutionException) {
}
assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
}
@Test
fun setZoomRatio_smallerThanMin_OutOfRangeException() = runBlocking {
val minZoomRatio = cameraInfo.zoomState.value!!.minZoomRatio
val result = cameraControl.setZoomRatio(minZoomRatio - 1.0f)
assertFutureThrowsIllegalArgumentException(result)
}
@Test
fun setZoomRatioBy1_0_isEqualToSensorRect() = runBlocking {
checkTestPreconditions(isAndroidRZoom = false)
cameraControl.setZoomRatio(1.0f)[5, TimeUnit.SECONDS]
captureCallback.verify(
{ captureRequest, _ ->
captureRequest[CaptureRequest.SCALER_CROP_REGION] == getSensorRect()
},
5000
)
}
@Test
@SdkSuppress(minSdkVersion = 30)
fun setZoomRatioBy1_0_androidRZoomRatioIsUpdated() = runBlocking {
checkTestPreconditions(isAndroidRZoom = true)
cameraControl.setZoomRatio(1.0f)[5, TimeUnit.SECONDS]
captureCallback.verify(
{ captureRequest, _ ->
captureRequest[CaptureRequest.SCALER_CROP_REGION] == getSensorRect() &&
areFloatsEqual(captureRequest[CaptureRequest.CONTROL_ZOOM_RATIO], 1.0f)
},
5000
)
}
@Test
fun setZoomRatioBy2_0_cropRegionIsSetCorrectly() = runBlocking {
assumeTrue(getMaxDigitalZoom() != null && getMaxDigitalZoom()!! > 2.0f + DELTA)
checkTestPreconditions(isAndroidRZoom = false)
cameraControl.setZoomRatio(2.0f)[5, TimeUnit.SECONDS]
val sensorRect = getSensorRect()
val cropX = sensorRect.width() / 4
val cropY = sensorRect.height() / 4
val cropRect = Rect(
cropX, cropY, cropX + sensorRect.width() / 2,
cropY + sensorRect.height() / 2
)
captureCallback.verify(
{ captureRequest, _ ->
captureRequest[CaptureRequest.SCALER_CROP_REGION] == cropRect
},
5000
)
}
@Test
@SdkSuppress(minSdkVersion = 30)
fun setZoomRatioBy2_0_androidRZoomRatioIsUpdated() = runBlocking {
checkTestPreconditions(isAndroidRZoom = true)
cameraControl.setZoomRatio(2.0f)
captureCallback.verify(
{ captureRequest, _ ->
captureRequest[CaptureRequest.SCALER_CROP_REGION] == getSensorRect() &&
areFloatsEqual(captureRequest[CaptureRequest.CONTROL_ZOOM_RATIO], 2.0f)
},
5000
)
}
@Test
fun setLinearZoomBy0_isSameAsMinRatio() = runBlocking {
cameraControl.setLinearZoom(0f)
val ratioAtPercentage0 = cameraInfo.zoomState.waitForValue { value ->
areFloatsEqual(value.linearZoom, 0f)
}.zoomRatio
val ratioAtMinZoomRatio = cameraInfo.zoomState.value?.minZoomRatio
assertThat(ratioAtPercentage0).isEqualTo(ratioAtMinZoomRatio)
}
@Test
fun setLinearZoomBy1_isSameAsMaxRatio() = runBlocking {
cameraControl.setLinearZoom(1f)
val ratioAtPercentage1 = cameraInfo.zoomState.waitForValue { value ->
areFloatsEqual(value.linearZoom, 1f)
}.zoomRatio
val ratioAtMaxZoomRatio = cameraInfo.zoomState.value?.maxZoomRatio
assertThat(ratioAtPercentage1).isEqualTo(ratioAtMaxZoomRatio)
}
@Test
fun setLinearZoomBy0_5_isHalfCropWidth() = runBlocking {
checkTestPreconditions(isAndroidRZoom = false)
// crop region in percentage == 0 may be null, need to use sensor rect instead.
val cropRegionMinZoom = getSensorRect()
val cropRegionMaxZoom = getCropRegionFromCameraCapture(linearZoom = 1f)
val cropRegionHalfZoom = getCropRegionFromCameraCapture(linearZoom = 0.5f)
assertThat(cropRegionHalfZoom.width().toFloat())
.isWithin(TOLERANCE)
.of((cropRegionMinZoom.width() + cropRegionMaxZoom.width()) / 2.0f)
}
@Test
@SdkSuppress(minSdkVersion = 30)
fun setLinearZoomBy0_5_androidRZoomRatioUpdatedCorrectly() = runBlocking {
checkTestPreconditions(isAndroidRZoom = true)
val cropWidth = 10000f
val zoomRatioForLinearMax = getZoomRatioFromCameraCapture(1f)
val cropWidthForLinearMax = cropWidth / zoomRatioForLinearMax
val zoomRatioForLinearMin = getZoomRatioFromCameraCapture(0f)
val cropWidthForLinearMin = cropWidth / zoomRatioForLinearMin
val zoomRatioForLinearHalf = getZoomRatioFromCameraCapture(0.5f)
val cropWidthForLinearHalf = cropWidth / zoomRatioForLinearHalf
assertThat(cropWidthForLinearHalf)
.isWithin(TOLERANCE)
.of((cropWidthForLinearMin + cropWidthForLinearMax) / 2.0f)
}
@Test
fun setLinearZoom_cropWidthChangedLinearly() = runBlocking {
checkTestPreconditions(isAndroidRZoom = false)
// crop region in percentage == 0 may be null, need to use sensor rect instead.
var prevCropRegion = getSensorRect()
var prevWidthDelta = 0f
var percentage = 0.1f
while (percentage < 1.0f) {
val cropRegion = getCropRegionFromCameraCapture(linearZoom = percentage)
if (prevWidthDelta == 0f) {
prevWidthDelta = (prevCropRegion.width() - cropRegion.width()).toFloat()
} else {
val widthDelta = (prevCropRegion.width() - cropRegion.width()).toFloat()
assertThat(widthDelta)
.isWithin(TOLERANCE)
.of(prevWidthDelta)
}
prevCropRegion = cropRegion
percentage += 0.1f
}
}
@RequiresApi(Build.VERSION_CODES.R)
@Test
fun setLinearZoom_androidRZoomRatio_cropWidthChangedLinearly() = runBlocking {
checkTestPreconditions(isAndroidRZoom = true)
val cropWidth = 10000f
val zoomRatioForLinearMin = getZoomRatioFromCameraCapture(linearZoom = 0f)
var prevCropWidth = cropWidth / zoomRatioForLinearMin
var prevWidthDelta = 0f
var percentage = 0.1f
while (percentage < 1.0f) {
val zoomRatio = getZoomRatioFromCameraCapture(linearZoom = percentage)
val cropWidthForTheRatio = cropWidth / zoomRatio
if (prevWidthDelta == 0f) {
prevWidthDelta = prevCropWidth - cropWidthForTheRatio
} else {
val widthDelta = prevCropWidth - cropWidthForTheRatio
assertThat(widthDelta)
.isWithin(TOLERANCE)
.of(prevWidthDelta)
}
prevCropWidth = cropWidthForTheRatio
percentage += 0.1f
}
}
@Test
fun setLinearZoom_largerThan1_zoomUnmodified() = runBlocking {
cameraControl.setLinearZoom(0.5f)[5, TimeUnit.SECONDS]
/**
* The exception is caught but not handled here intentionally. Because in this test,
* we want to focus on the value of the zoomRatio after exception is thrown.
* The exception itself is tested with [setLinearZoom_largerThan1_outOfRangeException]
*/
try {
cameraControl.setLinearZoom(1.1f)[5, TimeUnit.SECONDS]
} catch (_: ExecutionException) {
}
assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
}
@Test
fun setLinearZoom_largerThan1_outOfRangeException() = runBlocking {
val result = cameraControl.setLinearZoom(1.1f)
assertFutureThrowsIllegalArgumentException(result)
}
@Test
fun setLinearZoom_smallerThan0_zoomUnmodified() = runBlocking {
cameraControl.setLinearZoom(0.5f)[5, TimeUnit.SECONDS]
/**
* The exception is caught but not handled here intentionally. Because in this test,
* we want to focus on the value of the zoomRatio after exception is thrown.
* The exception itself is tested with [setLinearZoom_smallerThan0_outOfRangeException]
*/
try {
cameraControl.setLinearZoom(-0.1f)[5, TimeUnit.SECONDS]
} catch (_: ExecutionException) {
}
assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
}
@Test
fun setLinearZoom_smallerThan0_outOfRangeException() = runBlocking {
val result = cameraControl.setLinearZoom(-0.1f)
assertFutureThrowsIllegalArgumentException(result)
}
@Test
fun getterLiveData_defaultValueIsNonNull() {
assertThat(cameraInfo.zoomState.value).isNotNull()
}
@Test
fun getZoomRatioLiveData_observerIsCalledWhenZoomRatioIsSet() = runBlocking {
assumeTrue(getMaxDigitalZoom() != null && getMaxDigitalZoom()!! > 2.0f + DELTA)
val latch1 = CountDownLatch(1)
val latch2 = CountDownLatch(1)
val latch3 = CountDownLatch(1)
withContext(Dispatchers.Main) {
val lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
cameraInfo.zoomState.observe(lifecycleOwner) { value: ZoomState ->
if (areFloatsEqual(value.zoomRatio, 1.2f)) {
latch1.countDown()
} else if (areFloatsEqual(value.zoomRatio, 1.5f)) {
latch2.countDown()
} else if (areFloatsEqual(value.zoomRatio, 2.0f)) {
latch3.countDown()
}
}
cameraControl.setZoomRatio(1.2f)
cameraControl.setZoomRatio(1.5f)
cameraControl.setZoomRatio(2.0f)
}
assertThat(latch1.await(3000, TimeUnit.MILLISECONDS)).isTrue()
assertThat(latch2.await(3000, TimeUnit.MILLISECONDS)).isTrue()
assertThat(latch3.await(3000, TimeUnit.MILLISECONDS)).isTrue()
}
@Test
fun getZoomRatioLiveData_observerIsCalledWhenZoomPercentageIsSet() = runBlocking {
// can not test properly as zoom ratio will always be 1.0f
assumeTrue(getMaxDigitalZoom() != null && abs(getMaxDigitalZoom()!! - 1.0f) > DELTA)
val latch = CountDownLatch(3)
withContext(Dispatchers.Main) {
val lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
cameraInfo.zoomState.observe(lifecycleOwner,
Observer { value: ZoomState ->
if (value.zoomRatio != getMaxDigitalZoom()) {
latch.countDown()
}
})
cameraControl.setLinearZoom(0.1f)
cameraControl.setLinearZoom(0.2f)
cameraControl.setLinearZoom(0.3f)
}
assertThat(latch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
}
@Test
fun getZoomPercentageLiveData_observerIsCalledWhenZoomPercentageIsSet() = runBlocking {
val latch1 = CountDownLatch(1)
val latch2 = CountDownLatch(1)
val latch3 = CountDownLatch(1)
withContext(Dispatchers.Main) {
val lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
cameraInfo.zoomState.observe(lifecycleOwner) { value: ZoomState ->
if (areFloatsEqual(value.linearZoom, 0.1f)) {
latch1.countDown()
} else if (areFloatsEqual(value.linearZoom, 0.2f)) {
latch2.countDown()
} else if (areFloatsEqual(value.linearZoom, 0.3f)) {
latch3.countDown()
}
}
cameraControl.setLinearZoom(0.1f)
cameraControl.setLinearZoom(0.2f)
cameraControl.setLinearZoom(0.3f)
}
assertThat(latch1.await(3000, TimeUnit.MILLISECONDS)).isTrue()
assertThat(latch2.await(3000, TimeUnit.MILLISECONDS)).isTrue()
assertThat(latch3.await(3000, TimeUnit.MILLISECONDS)).isTrue()
}
@Test
fun getZoomPercentageLiveData_observerIsCalledWhenZoomRatioIsSet() = runBlocking {
assumeTrue(getMaxDigitalZoom() != null && getMaxDigitalZoom()!! > 2.0f + DELTA)
val latch = CountDownLatch(3)
withContext(Dispatchers.Main) {
val lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
cameraInfo.zoomState.observe(lifecycleOwner,
Observer { value: ZoomState ->
if (value.linearZoom != 0f) {
latch.countDown()
}
})
cameraControl.setZoomRatio(1.2f)
cameraControl.setZoomRatio(1.5f)
cameraControl.setZoomRatio(2.0f)
}
assertThat(latch.await(1500, TimeUnit.MILLISECONDS)).isTrue()
}
@Test
fun getZoomRatioDefaultValue() {
assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(1.0f)
}
@Test
fun getZoomPercentageDefaultValue() {
checkTestPreconditions(isAndroidRZoom = false)
assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0)
}
@Test
fun getMaxZoomRatio_isMaxDigitalZoom() {
val maxZoom = cameraInfo.zoomState.value?.maxZoomRatio
assertThat(maxZoom).isEqualTo(getMaxDigitalZoom())
}
@Test
fun getMinZoomRatio_isOne() {
checkTestPreconditions(isAndroidRZoom = false)
val minZoom = cameraInfo.zoomState.value?.minZoomRatio
assertThat(minZoom).isEqualTo(1f)
}
private fun getMaxDigitalZoom(): Float? {
val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
assumeNotNull(cameraCharacteristics)
if (isAndroidRZoomEnabled(cameraCharacteristics!!)) {
return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)?.upper
}
return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
}
@Test
fun valueIsResetAfterUseCasesDetached() = runBlocking {
cameraControl.setLinearZoom(0.2f) // this will change ratio and percentage.
withContext(Dispatchers.Main) {
cameraProvider.unbindAll()
}
cameraInfo.zoomState.waitForValue { value ->
areFloatsEqual(value.zoomRatio, 1.0f)
}
return@runBlocking
}
@Test
fun maxZoomShouldBeLargerThanOrEqualToMinZoom() {
val zoomState = cameraInfo.zoomState.value
assertThat(zoomState!!.maxZoomRatio).isAtLeast(zoomState.minZoomRatio)
}
private suspend fun LiveData<ZoomState>.waitForValue(
waitCondition: (ZoomState) -> Boolean
): ZoomState {
var awaitedValue: ZoomState = ZoomValue(-1f, -1f, -1f)
val latch = CountDownLatch(1)
withContext(Dispatchers.Main) {
val lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
observe(lifecycleOwner,
Observer { value: ZoomState ->
if (waitCondition(value)) {
awaitedValue = value
latch.countDown()
}
})
}
latch.await(3, TimeUnit.SECONDS)
return awaitedValue
}
private fun checkTestPreconditions(isAndroidRZoom: Boolean) {
val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
assumeNotNull(cameraCharacteristics)
if (isAndroidRZoom) {
assumeTrue(isAndroidRZoomEnabled(cameraCharacteristics!!))
} else {
assumeNotNull(
cameraCharacteristics!!.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
)
assumeFalse(isAndroidRZoomEnabled(cameraCharacteristics))
}
}
private suspend fun getCropRegionFromCameraCapture(linearZoom: Float): Rect {
val cropRegionFromCameraCaptureRef = AtomicReference(Rect(0, 0, 0, 0))
val cropRegionCallbackCountRef = AtomicReference(10)
captureCallback.reset()
cameraControl.setLinearZoom(linearZoom)[5, TimeUnit.SECONDS]
captureCallback.verify(
{ captureRequest, _ ->
if (captureRequest[CaptureRequest.SCALER_CROP_REGION] == null) {
return@verify false
}
cropRegionFromCameraCaptureRef.set(
captureRequest[CaptureRequest.SCALER_CROP_REGION]!!
)
cropRegionCallbackCountRef.set(cropRegionCallbackCountRef.get() - 1)
return@verify cropRegionCallbackCountRef.get() == 0
},
5000
)
return cropRegionFromCameraCaptureRef.get()
}
@RequiresApi(Build.VERSION_CODES.R)
private suspend fun getZoomRatioFromCameraCapture(linearZoom: Float): Float {
val zoomRatioFromCameraCaptureRef = AtomicReference(Float.NaN)
val cropRegionCallbackCountRef = AtomicReference(10)
captureCallback.reset()
cameraControl.setLinearZoom(linearZoom)[5, TimeUnit.SECONDS]
captureCallback.verify(
{ captureRequest, _ ->
if (captureRequest[CaptureRequest.CONTROL_ZOOM_RATIO] == null) {
return@verify false
}
zoomRatioFromCameraCaptureRef.set(
captureRequest[CaptureRequest.CONTROL_ZOOM_RATIO]!!
)
cropRegionCallbackCountRef.set(cropRegionCallbackCountRef.get() - 1)
return@verify cropRegionCallbackCountRef.get() == 0
},
5000
)
return zoomRatioFromCameraCaptureRef.get()
}
private fun getSensorRect(): Rect {
val cameraCharacteristics = CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
val rect = cameraCharacteristics!!.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
// Some device like pixel 2 will have (0, 8) as the left-top corner.
return Rect(0, 0, rect!!.width(), rect.height())
}
// TODO: Use ZoomControl#isAndroidRZoomSupported for camera-camera2 config and
// ZoomCompat#Bindings#provideZoomRatio for camera-pipe config based on implName
private fun isAndroidRZoomEnabled(cameraCharacteristics: CameraCharacteristics) =
Build.VERSION.SDK_INT >= 30 && getZoomRatioRange(cameraCharacteristics) != null
@RequiresApi(30)
private fun getZoomRatioRange(cameraCharacteristics: CameraCharacteristics) = try {
cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
} catch (e: AssertionError) {
// Some devices may throw AssertionError when failed to get CameraCharacteristic.
// Catch the AssertionError and return null to workaround it. b/231701345
null
}
private fun <T> assertFutureCompletes(future: ListenableFuture<T>) {
try {
future[5, TimeUnit.SECONDS]
} catch (e: Exception) {
Assert.fail("future fail:$e")
}
}
private fun assertFutureThrowsIllegalArgumentException(result: ListenableFuture<Void>) {
try {
result[100, TimeUnit.MILLISECONDS]
} catch (e: ExecutionException) {
assertThat(e.cause).isInstanceOf(java.lang.IllegalArgumentException::class.java)
return
}
Assert.fail()
}
private fun areFloatsEqual(num1: Float?, num2: Float?): Boolean {
if (num1 == null && num2 == null) return true
if (num1 == null || num2 == null) return false
return abs(num1 - num2) < 2.0 * Math.ulp(abs(num1).coerceAtLeast(abs(num2)))
}
class CaptureCallback(private val captureCount: Int) : CameraCaptureSession.CaptureCallback() {
private var waitingCount = atomic(captureCount)
private val failureException =
TimeoutException("Test doesn't complete after waiting for $captureCount frames.")
@Volatile
private var startReceiving = false
@Volatile
private var _verifyBlock: (
captureRequest: CaptureRequest,
captureResult: TotalCaptureResult
) -> Boolean = { _, _ -> false }
private var signal = CompletableDeferred<Unit>()
fun reset() {
_verifyBlock = { _, _ -> false }
startReceiving = false
waitingCount = atomic(captureCount)
signal = CompletableDeferred()
}
suspend fun verify(
verifyBlock: (
captureRequest: CaptureRequest,
captureResult: TotalCaptureResult
) -> Boolean = { _, _ -> false },
timeout: Long = TimeUnit.SECONDS.toMillis(5),
) {
withTimeout(timeout) {
_verifyBlock = verifyBlock
startReceiving = true
signal.await()
}
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
if (!startReceiving) {
return
}
if (waitingCount.decrementAndGet() < 0) {
signal.completeExceptionally(failureException)
return
}
if (_verifyBlock(request, result)) {
signal.complete(Unit)
}
}
}
companion object {
private const val DELTA = 1e-9
private const val TOLERANCE = 5f
@JvmStatic
@Parameterized.Parameters(name = "selector={0},config={2}")
fun data() = listOf(
arrayOf(
"front",
CameraSelector.DEFAULT_FRONT_CAMERA,
Camera2Config::class.simpleName,
Camera2Config.defaultConfig()
),
arrayOf(
"front",
CameraSelector.DEFAULT_FRONT_CAMERA,
CameraPipeConfig::class.simpleName,
CameraPipeConfig.defaultConfig()
),
arrayOf(
"back",
CameraSelector.DEFAULT_BACK_CAMERA,
Camera2Config::class.simpleName,
Camera2Config.defaultConfig()
),
arrayOf(
"back",
CameraSelector.DEFAULT_BACK_CAMERA,
CameraPipeConfig::class.simpleName,
CameraPipeConfig.defaultConfig()
),
)
}
}