[go: nahoru, domu]

blob: c444306915e9a1d43156c2878ccd5cc189f4342e [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.
*/
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
package androidx.camera.integration.core
import android.content.Context
import android.content.Context.CAMERA_SERVICE
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.TotalCaptureResult
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.interop.Camera2CameraControl
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.camera2.interop.CaptureRequestOptions
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.concurrent.futures.await
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class Camera2InteropIntegrationTest {
@get:Rule
val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
PreTestCameraIdList(Camera2Config.defaultConfig())
)
private var processCameraProvider: ProcessCameraProvider? = null
@Before
fun setUp() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
processCameraProvider = ProcessCameraProvider.getInstance(context).await()
Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
}
@After
fun tearDown(): Unit = runBlocking {
processCameraProvider?.apply {
withContext(Dispatchers.Main) {
unbindAll()
}
shutdown().await()
}
}
@Test
fun cameraDeviceListener_receivesClose_afterUnbindAll(): Unit = runBlocking {
val previewBuilder = Preview.Builder()
val deviceStateFlow = Camera2Interop.Extender(previewBuilder).createDeviceStateFlow()
withContext(Dispatchers.Main) {
processCameraProvider!!.bindToLifecycle(
TestLifecycleOwner(Lifecycle.State.RESUMED),
CameraSelector.DEFAULT_BACK_CAMERA, previewBuilder.build()
)
}
// Call unbindAll() once device state is [DeviceState.Opened]. If another state occurs
// first, the StateFlow subscriber will unsubscribe with the last state. Once
// [DeviceState.Opened] occurs, we will call unbindAll(), and the next state should be
// [DeviceState.Closed].
var unbindAllCalled = false
val lastState = deviceStateFlow.dropWhile { state ->
when (state) {
is DeviceState.Unknown -> true // Filter out this state from the downstream flow
is DeviceState.Opened -> {
withContext(Dispatchers.Main) { processCameraProvider!!.unbindAll() }
unbindAllCalled = true
true // Filter out this state from the downstream flow
}
else -> false // Forward to the downstream flow
}
}.first()
assertThat(unbindAllCalled).isTrue()
assertThat(lastState).isEqualTo(DeviceState.Closed)
}
@Test
fun canUseCameraSelector_fromCamera2CameraIdAndCameraFilter(): Unit = runBlocking {
val camera2CameraManager = ApplicationProvider.getApplicationContext<Context>()
.getSystemService(CAMERA_SERVICE) as CameraManager
camera2CameraManager.cameraIdList.forEach continuing@{ id ->
if (!isBackwardCompatible(camera2CameraManager, id)) {
return@continuing
}
val cameraSelector = CameraSelector.Builder()
.addCameraFilter {
it.filter { Camera2CameraInfo.from(it).cameraId == id }
}.build()
withContext(Dispatchers.Main) {
val camera = processCameraProvider!!.bindToLifecycle(
TestLifecycleOwner(Lifecycle.State.CREATED),
cameraSelector
)
assertThat(Camera2CameraInfo.from(camera.cameraInfo).cameraId).isEqualTo(id)
}
}
}
@Test
fun canUseCameraSelector_fromCamera2CameraIdAndAvailableCameraInfos(): Unit = runBlocking {
val camera2CameraManager = ApplicationProvider.getApplicationContext<Context>()
.getSystemService(CAMERA_SERVICE) as CameraManager
camera2CameraManager.cameraIdList.forEach continuing@{ id ->
if (!isBackwardCompatible(camera2CameraManager, id)) {
return@continuing
}
withContext(Dispatchers.Main) {
val cameraSelector =
processCameraProvider!!.availableCameraInfos.find {
Camera2CameraInfo.from(it).cameraId == id
}?.cameraSelector
val camera = processCameraProvider!!.bindToLifecycle(
TestLifecycleOwner(Lifecycle.State.CREATED),
cameraSelector!!
)
assertThat(Camera2CameraInfo.from(camera.cameraInfo).cameraId).isEqualTo(id)
}
}
}
@Test
fun requestOptionsShouldExist_afterLifeCycleStopStart(): Unit = runBlocking {
// Arrange.
val testKey = CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION
val testValue = 1
val testLifecycle = TestLifecycleOwner(Lifecycle.State.RESUMED)
withContext(Dispatchers.Main) {
processCameraProvider!!.bindAnalysis(testLifecycle).setInteropOptions(
mapOf(testKey to testValue)
)
}
captureCallback.waitFor(numOfCaptures = 10) {}
// Act.
withContext(Dispatchers.Main) {
testLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
testLifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
// Assert.
captureCallback.waitFor(numOfCaptures = 20) {
assertThat(it.last().get(testKey)).isEqualTo(testValue)
}
}
private fun ProcessCameraProvider.bindAnalysis(lifecycleOwner: LifecycleOwner): Camera {
val imageAnalysis = ImageAnalysis.Builder().apply {
Camera2Interop.Extender(this).setSessionCaptureCallback(
captureCallback
)
}.build().apply {
// set analyzer to make it active.
setAnalyzer(
CameraXExecutors.highPriorityExecutor()
) {
// Analyzer nothing to to
}
}
unbindAll()
return bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
imageAnalysis
)
}
private fun Camera.setInteropOptions(parameter: Map<CaptureRequest.Key<Int>, Int>) {
Camera2CameraControl.from(cameraControl).captureRequestOptions =
CaptureRequestOptions.Builder().apply {
parameter.forEach { (key, value) ->
setCaptureRequestOption(key, value)
}
}.build()
}
private val captureCallback = object : CameraCaptureSession.CaptureCallback() {
val waitingList = mutableListOf<CaptureContainer>()
fun waitFor(
timeout: Long = TimeUnit.SECONDS.toMillis(5),
numOfCaptures: Int = 1,
verifyResults: (captureRequests: List<CaptureRequest>) -> Unit
) {
val resultContainer = CaptureContainer(CountDownLatch(numOfCaptures))
waitingList.add(resultContainer)
assertTrue(resultContainer.countDownLatch.await(timeout, TimeUnit.MILLISECONDS))
verifyResults(resultContainer.captureRequests)
waitingList.remove(resultContainer)
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
waitingList.toList().forEach {
it.captureRequests.add(request)
it.countDownLatch.countDown()
}
}
}
data class CaptureContainer(
val countDownLatch: CountDownLatch,
val captureRequests: MutableList<CaptureRequest> = mutableListOf()
)
// Sealed class for converting CameraDevice.StateCallback into a StateFlow
sealed class DeviceState {
object Unknown : DeviceState()
object Opened : DeviceState()
object Closed : DeviceState()
object Disconnected : DeviceState()
data class Error(val errorCode: Int) : DeviceState()
}
/**
* Returns a [StateFlow] which will signal the states of the camera defined in [DeviceState].
*/
private fun <T> Camera2Interop.Extender<T>.createDeviceStateFlow(): StateFlow<DeviceState> =
MutableStateFlow<DeviceState>(DeviceState.Unknown).apply {
val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
// tryEmit should always succeed for [MutableStateFlow]
value = DeviceState.Opened
}
override fun onClosed(camera: CameraDevice) {
tryEmit(DeviceState.Closed)
}
override fun onDisconnected(camera: CameraDevice) {
tryEmit(DeviceState.Disconnected)
}
override fun onError(camera: CameraDevice, errorCode: Int) {
tryEmit(DeviceState.Error(errorCode))
}
}
setDeviceStateCallback(stateCallback)
}.asStateFlow()
private fun isBackwardCompatible(cameraManager: CameraManager, cameraId: String): Boolean {
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
val capabilities =
cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
capabilities?.let {
return it.contains(REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE)
}
return false
}
}