blob: e5d846e078513f15f08171882e99f0a517914ffd [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.os.Build
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.core.CameraControl
import androidx.camera.core.TorchState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.testutils.assertThrows
import com.google.common.truth.Truth
import com.google.common.util.concurrent.MoreExecutors
import java.util.Objects
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
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
import org.robolectric.shadows.StreamConfigurationMapBuilder
@RunWith(RobolectricCameraPipeTestRunner::class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@OptIn(ExperimentalCoroutinesApi::class)
class TorchControlTest {
companion object {
private val executor = MoreExecutors.directExecutor()
private val fakeUseCaseThreads by lazy {
val dispatcher = executor.asCoroutineDispatcher()
val cameraScope = CoroutineScope(Job() + dispatcher)
UseCaseThreads(
cameraScope,
executor,
dispatcher
)
}
}
private val metadata = FakeCameraMetadata(
mapOf(
CameraCharacteristics.FLASH_INFO_AVAILABLE to true,
),
)
private val neverCompleteTorchRequestControl = FakeUseCaseCameraRequestControl().apply {
// Set a CompletableDeferred without set it to completed.
setTorchResult = CompletableDeferred()
}
private val aeFpsRange = AeFpsRange(
CameraQuirks(
FakeCameraMetadata(),
StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
OutputSizesCorrector(
FakeCameraMetadata(),
StreamConfigurationMapBuilder.newBuilder().build()
)
)
)
)
private lateinit var torchControl: TorchControl
@Before
fun setUp() {
val fakeUseCaseCamera = FakeUseCaseCamera()
val fakeCameraProperties = FakeCameraProperties(metadata)
torchControl = TorchControl(
fakeCameraProperties,
State3AControl(
fakeCameraProperties,
NoOpAutoFlashAEModeDisabler,
aeFpsRange
).apply {
useCaseCamera = fakeUseCaseCamera
},
fakeUseCaseThreads,
)
torchControl.useCaseCamera = fakeUseCaseCamera
}
@Test
fun enableTorch_whenNoFlashUnit(): Unit = runBlocking {
assertThrows<IllegalStateException> {
val fakeUseCaseCamera = FakeUseCaseCamera()
val fakeCameraProperties = FakeCameraProperties()
// Without a flash unit, this Job will complete immediately with a IllegalStateException
TorchControl(
fakeCameraProperties,
State3AControl(
fakeCameraProperties,
NoOpAutoFlashAEModeDisabler,
aeFpsRange
).apply {
useCaseCamera = fakeUseCaseCamera
},
fakeUseCaseThreads,
).also {
it.useCaseCamera = fakeUseCaseCamera
}.setTorchAsync(true).await()
}
}
@Test
fun getTorchState_whenNoFlashUnit() {
val fakeUseCaseCamera = FakeUseCaseCamera()
val fakeCameraProperties = FakeCameraProperties()
val torchState = TorchControl(
fakeCameraProperties,
State3AControl(
fakeCameraProperties,
NoOpAutoFlashAEModeDisabler,
aeFpsRange
).apply {
useCaseCamera = fakeUseCaseCamera
},
fakeUseCaseThreads,
).also {
it.useCaseCamera = fakeUseCaseCamera
}.torchStateLiveData.value
Truth.assertThat(torchState).isEqualTo(TorchState.OFF)
}
@Test
fun enableTorch_whenInactive(): Unit = runBlocking {
assertThrows<CameraControl.OperationCanceledException> {
val fakeUseCaseCamera = FakeUseCaseCamera()
val fakeCameraProperties = FakeCameraProperties(metadata)
TorchControl(
fakeCameraProperties,
State3AControl(
fakeCameraProperties,
NoOpAutoFlashAEModeDisabler,
aeFpsRange
).apply {
useCaseCamera = fakeUseCaseCamera
},
fakeUseCaseThreads,
).setTorchAsync(true).await()
}
}
@Test
fun getTorchState_whenInactive() {
torchControl.useCaseCamera = null
Truth.assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.OFF)
}
@Test
fun enableTorch_torchStateOn(): Unit = runBlocking {
torchControl.setTorchAsync(true)
// LiveData is updated synchronously. Don't need to wait for the result of the setTorchAsync
Truth.assertThat(torchControl.torchStateLiveData.value).isEqualTo(TorchState.ON)
}
@Test
fun disableTorch_TorchStateOff() {
torchControl.setTorchAsync(true)
// LiveData is updated synchronously. Don't need to wait for the result of the setTorchAsync
val firstTorchState = Objects.requireNonNull<Int>(torchControl.torchStateLiveData.value)
torchControl.setTorchAsync(false)
// LiveData is updated synchronously. Don't need to wait for the result of the setTorchAsync
val secondTorchState = torchControl.torchStateLiveData.value
Truth.assertThat(firstTorchState).isEqualTo(TorchState.ON)
Truth.assertThat(secondTorchState).isEqualTo(TorchState.OFF)
}
@Test
fun enableDisableTorch_futureWillCompleteSuccessfully(): Unit = runBlocking {
// Job should be completed without exception
torchControl.setTorchAsync(true).await()
// Job should be completed without exception
torchControl.setTorchAsync(false).await()
}
@Test
fun enableTorchTwice_cancelPreviousFuture(): Unit = runBlocking {
val deferred = torchControl.also {
it.useCaseCamera = FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
}.setTorchAsync(true)
torchControl.setTorchAsync(true)
assertThrows<CameraControl.OperationCanceledException> {
deferred.await()
}
}
@Test
fun setInActive_cancelPreviousFuture(): Unit = runBlocking {
val deferred = torchControl.also {
it.useCaseCamera = FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
}.setTorchAsync(true)
// reset() will be called after all the UseCases are detached.
torchControl.reset()
assertThrows<CameraControl.OperationCanceledException> {
deferred.await()
}
}
@Test
fun setInActiveWhenTorchOn_changeToTorchOff() {
torchControl.setTorchAsync(true)
val initialTorchState = torchControl.torchStateLiveData.value
// reset() will be called after all the UseCases are detached.
torchControl.reset()
val torchStateAfterInactive = torchControl.torchStateLiveData.value
Truth.assertThat(initialTorchState).isEqualTo(TorchState.ON)
Truth.assertThat(torchStateAfterInactive).isEqualTo(TorchState.OFF)
}
@Test
fun enableDisableTorch_observeTorchStateLiveData() {
val receivedTorchState = mutableListOf<Int?>()
// The observer should be notified of initial state
torchControl.torchStateLiveData.observe(
TestLifecycleOwner(
Lifecycle.State.STARTED,
UnconfinedTestDispatcher()
), object : Observer<Int?> {
private var mValue: Int? = null
override fun onChanged(value: Int?) {
if (mValue != value) {
mValue = value
receivedTorchState.add(value)
}
}
})
torchControl.setTorchAsync(true)
torchControl.setTorchAsync(false)
Truth.assertThat(receivedTorchState[0]).isEqualTo(TorchState.OFF) // initial state
Truth.assertThat(receivedTorchState[1]).isEqualTo(TorchState.ON) // by setTorchAsync(true)
Truth.assertThat(receivedTorchState[2]).isEqualTo(TorchState.OFF) // by setTorchAsync(false)
}
@Test
fun useCaseCameraUpdated_setTorchResultShouldPropagate(): Unit = runBlocking {
// Arrange.
torchControl.useCaseCamera =
FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
val deferred = torchControl.setTorchAsync(true)
val fakeRequestControl = FakeUseCaseCameraRequestControl().apply {
setTorchResult = CompletableDeferred<Result3A>()
}
val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
torchControl.useCaseCamera = fakeUseCaseCamera
// Simulate setTorch is completed in the recreated UseCaseCamera
fakeRequestControl.setTorchResult.complete(Result3A(status = Result3A.Status.OK))
// Assert. The setTorch task should be completed.
Truth.assertThat(deferred.awaitWithTimeout()).isNotNull()
}
@Test
fun useCaseCameraUpdated_onlyCompleteLatestRequest(): Unit = runBlocking {
// Arrange.
torchControl.useCaseCamera =
FakeUseCaseCamera(requestControl = neverCompleteTorchRequestControl)
val deferred = torchControl.setTorchAsync(true)
val fakeRequestControl = FakeUseCaseCameraRequestControl().apply {
setTorchResult = CompletableDeferred()
}
val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
// Act. Simulate the UseCaseCamera is recreated.
torchControl.useCaseCamera = fakeUseCaseCamera
// Act. Set Torch mode again.
val deferred2 = torchControl.setTorchAsync(false)
// Simulate setTorch is completed in the recreated UseCaseCamera
fakeRequestControl.setTorchResult.complete(Result3A(status = Result3A.Status.OK))
// Assert. The previous setTorch task should be cancelled
assertThrows<CameraControl.OperationCanceledException> {
deferred.awaitWithTimeout()
}
// Assert. The latest setTorch task should be completed.
Truth.assertThat(deferred2.awaitWithTimeout()).isNotNull()
}
private suspend fun <T> Deferred<T>.awaitWithTimeout(
timeMillis: Long = TimeUnit.SECONDS.toMillis(5)
) = withTimeout(timeMillis) {
await()
}
}