blob: 87cc58cbc008c1efaf097452364744ef177f79fe [file] [log] [blame]
/*
* Copyright 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.camera2.pipe.integration.impl
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
import android.os.Build
import android.util.Range
import android.util.Rational
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.compat.EvCompImpl
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeFrameInfo
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.core.CameraControl
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@RunWith(RobolectricCameraPipeTestRunner::class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class EvCompControlTest {
private val fakeUseCaseThreads by lazy {
val executor = Executors.newSingleThreadExecutor()
val dispatcher = executor.asCoroutineDispatcher()
val cameraScope = CoroutineScope(Job() + dispatcher)
UseCaseThreads(
cameraScope,
executor,
dispatcher,
)
}
private val metadata = FakeCameraMetadata(
mapOf(
CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE to Range.create(-4, 4),
CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP to Rational.parseRational("1/2"),
),
)
private val comboRequestListener = ComboRequestListener()
private lateinit var exposureControl: EvCompControl
@Before
fun setUp() {
exposureControl = EvCompControl(
EvCompImpl(
FakeCameraProperties(metadata),
fakeUseCaseThreads,
comboRequestListener
)
)
exposureControl.useCaseCamera = FakeUseCaseCamera()
}
@Test
fun setExposureTwice_theFirstCallShouldBeCancelled(): Unit = runBlocking {
val deferred: Deferred<Int> = exposureControl.updateAsync(1)
val deferred1: Deferred<Int> = exposureControl.updateAsync(2)
// Assert. The second call should keep working.
assertThat(deferred1.isCompleted).isFalse()
// Assert. The first call should be cancelled with a CameraControl.OperationCanceledException.
assertThrows<CameraControl.OperationCanceledException> {
deferred.awaitWithTimeout()
}
}
@Test
fun setExposureCancelled_theCompensationValueShouldKeepInControl() = runBlocking {
// Arrange.
val deferred = exposureControl.updateAsync(1)
// Act.
deferred.cancel()
// Assert. The new value should be set to the exposure control even when the task fails.
assertThat(exposureControl.exposureState.exposureCompensationIndex).isEqualTo(1)
}
@Test
fun exposureControlInactive_setExposureTaskShouldCancel(): Unit = runBlocking {
val deferred = exposureControl.updateAsync(1)
// Act. Simulate control inactive by set useCaseCamera to null & call reset().
exposureControl.useCaseCamera = null
exposureControl.reset()
// Assert. The exposure control has been set to inactive. It should throw an exception.
assertThrows<CameraControl.OperationCanceledException> {
deferred.awaitWithTimeout()
}
}
@Test
fun setExposureNotInRange_shouldCompleteTheTaskWithException(): Unit = runBlocking {
// Assert. The Exposure index 5 is not in the valid range. It should throw an exception.
assertThrows<IllegalArgumentException> {
exposureControl.updateAsync(5).awaitWithTimeout()
}
}
@Test
fun setExposureOnNotSupportedCamera_shouldCompleteTheTaskWithException(): Unit = runBlocking {
// Arrange.
val evCompCompat = EvCompImpl(
// Fake CameraProperties without CONTROL_AE_COMPENSATION related properties.
FakeCameraProperties(),
fakeUseCaseThreads,
comboRequestListener
)
exposureControl = EvCompControl(evCompCompat)
exposureControl.useCaseCamera = FakeUseCaseCamera()
// Act.
val deferred = exposureControl.updateAsync(1)
// Assert. This camera does not support the exposure compensation, the task should fail.
assertThrows<IllegalArgumentException> {
deferred.awaitWithTimeout()
}
}
@Test
fun useCaseCameraUpdated_setExposureResultShouldPropagate(): Unit = runBlocking {
val targetEv = 1
val deferred = exposureControl.updateAsync(targetEv)
// Act. Simulate the UseCaseCamera is recreated.
exposureControl.useCaseCamera = FakeUseCaseCamera()
comboRequestListener.simulateAeConverge(exposureValue = targetEv)
// Assert. The setEV task should be completed.
assertThat(deferred.awaitWithTimeout()).isEqualTo(targetEv)
}
@Test
fun useCaseCameraUpdated_onlyCompleteLatestRequest(): Unit = runBlocking {
val targetEv = 2
val deferred = exposureControl.updateAsync(1)
// Act. Simulate the UseCaseCamera is recreated,
exposureControl.useCaseCamera = FakeUseCaseCamera()
// Act. Submits a new EV value.
val deferred2 = exposureControl.updateAsync(targetEv)
comboRequestListener.simulateAeConverge(exposureValue = targetEv)
// Assert. The previous setEV task should be cancelled
assertThrows<CameraControl.OperationCanceledException> {
deferred.awaitWithTimeout()
}
// Assert. The latest setEV task should be completed.
assertThat(deferred2.awaitWithTimeout()).isEqualTo(targetEv)
}
private suspend fun Deferred<Int>.awaitWithTimeout(
timeMillis: Long = TimeUnit.SECONDS.toMillis(5)
) = withTimeout(timeMillis) {
await()
}
private fun ComboRequestListener.simulateAeConverge(
exposureValue: Int,
frameNumber: FrameNumber = FrameNumber(101L),
) {
val requestMetadata = FakeRequestMetadata(
requestParameters = mapOf(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION to exposureValue
),
requestNumber = RequestNumber(1)
)
val resultMetaData = FakeFrameMetadata(
resultMetadata = mapOf(
CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION to exposureValue,
CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
),
frameNumber = frameNumber,
)
fakeUseCaseThreads.sequentialExecutor.execute {
onComplete(
requestMetadata, frameNumber, FakeFrameInfo(
metadata = resultMetaData,
requestMetadata = requestMetadata,
)
)
}
}
}