blob: 5c7d1561976b35113afad22fb4a2624d20bc76d4 [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.compat
import android.hardware.camera2.CameraDevice
import android.os.Build
import android.os.Looper.getMainLooper
import androidx.camera.camera2.pipe.CameraError
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.core.SystemTimeSource
import androidx.camera.camera2.pipe.core.TimeSource
import androidx.camera.camera2.pipe.core.Timestamps
import androidx.camera.camera2.pipe.core.Token
import androidx.camera.camera2.pipe.graph.GraphListener
import androidx.camera.camera2.pipe.internal.CameraErrorListener
import androidx.camera.camera2.pipe.testing.FakeCamera2DeviceCloser
import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.testing.RobolectricCameras
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@OptIn(ExperimentalCoroutinesApi::class)
internal class VirtualCameraStateTest {
private val mainLooper = shadowOf(getMainLooper())
private val cameraId = RobolectricCameras.create()
private val testCamera = RobolectricCameras.open(cameraId)
private val graphListener: GraphListener = mock()
private val cameraErrorListener: CameraErrorListener = mock()
@After
fun teardown() {
mainLooper.idle()
RobolectricCameras.clear()
}
@Test
fun virtualCameraStateCanBeDisconnected() = runTest {
// This test asserts that the virtual camera starts in an unopened state and is changed to
// "Closed" when disconnect is invoked on the VirtualCamera.
val virtualCamera = VirtualCameraState(cameraId, graphListener)
assertThat(virtualCamera.value).isInstanceOf(CameraStateUnopened::class.java)
virtualCamera.disconnect()
assertThat(virtualCamera.value).isInstanceOf(CameraStateClosed::class.java)
val closedState = virtualCamera.value as CameraStateClosed
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_DISCONNECTED)
// Disconnecting a virtual camera does not propagate statistics.
assertThat(closedState.cameraErrorCode).isNull()
assertThat(closedState.cameraException).isNull()
assertThat(closedState.cameraRetryCount).isNull()
assertThat(closedState.cameraRetryDurationNs).isNull()
assertThat(closedState.cameraOpenDurationNs).isNull()
assertThat(closedState.cameraActiveDurationNs).isNull()
assertThat(closedState.cameraClosingDurationNs).isNull()
}
@Test
fun virtualCameraStateConnectsToFlow() = runTest {
// This test asserts that when a virtual camera is connected to a flow of CameraState
// changes that it receives those changes and can be subsequently disconnected, which stops
// additional events from being passed to the virtual camera instance.
val virtualCamera = VirtualCameraState(cameraId, graphListener)
val cameraState =
flowOf(
CameraStateOpen(
AndroidCameraDevice(
testCamera.metadata,
testCamera.cameraDevice,
testCamera.cameraId,
cameraErrorListener,
)
)
)
virtualCamera.connect(
cameraState,
object : Token {
override fun release(): Boolean {
return true
}
})
virtualCamera.state.first { it !is CameraStateUnopened }
assertThat(virtualCamera.value).isInstanceOf(CameraStateOpen::class.java)
virtualCamera.disconnect()
assertThat(virtualCamera.value).isInstanceOf(CameraStateClosed::class.java)
val closedState = virtualCamera.value as CameraStateClosed
assertThat(closedState.cameraId).isEqualTo(cameraId)
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_DISCONNECTED)
}
@Test
fun virtualCameraStateRespondsToClose() = runTest {
// This tests that a listener attached to the virtualCamera.state property will receive all
// of the events, starting from CameraStateUnopened.
val virtualCamera = VirtualCameraState(cameraId, graphListener)
val states =
listOf(
CameraStateOpen(
AndroidCameraDevice(
testCamera.metadata,
testCamera.cameraDevice,
testCamera.cameraId,
cameraErrorListener,
)
),
CameraStateClosing(),
CameraStateClosed(
cameraId,
ClosedReason.CAMERA2_ERROR,
cameraErrorCode = CameraError.ERROR_CAMERA_SERVICE
)
)
val events = mutableListOf<CameraState>()
val job = launch { virtualCamera.state.collect { events.add(it) } }
virtualCamera.connect(
states.asFlow(),
object : Token {
override fun release(): Boolean {
return true
}
})
advanceUntilIdle()
job.cancelAndJoin()
val expectedStates = listOf(CameraStateUnopened).plus(states)
assertThat(events).containsExactlyElementsIn(expectedStates)
}
}
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class AndroidCameraDeviceTest {
private val mainLooper = shadowOf(getMainLooper())
private val cameraId = RobolectricCameras.create()
private val testCamera = RobolectricCameras.open(cameraId)
private val timeSource: TimeSource = SystemTimeSource()
private val cameraDeviceCloser = FakeCamera2DeviceCloser()
private val now = Timestamps.now(timeSource)
private val cameraErrorListener = object : CameraErrorListener {
var lastCameraId: CameraId? = null
var lastCameraError: CameraError? = null
override fun onCameraError(
cameraId: CameraId,
cameraError: CameraError,
willAttemptRetry: Boolean
) {
lastCameraId = cameraId
lastCameraError = cameraError
}
}
@After
fun teardown() {
RobolectricCameras.clear()
}
@Test
fun cameraOpensAndGeneratesStats() {
mainLooper.idleFor(200, TimeUnit.MILLISECONDS)
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
assertThat(listener.state.value).isInstanceOf(CameraStateUnopened.javaClass)
// Advance the system clocks.
mainLooper.idleFor(200, TimeUnit.MILLISECONDS)
listener.onOpened(testCamera.cameraDevice)
assertThat(listener.state.value).isInstanceOf(CameraStateOpen::class.java)
assertThat(
(listener.state.value as CameraStateOpen)
.cameraDevice
.unwrapAs(CameraDevice::class)
)
.isSameInstanceAs(testCamera.cameraDevice)
mainLooper.idleFor(1000, TimeUnit.MILLISECONDS)
listener.onClosed(testCamera.cameraDevice)
mainLooper.idle()
assertThat(listener.state.value).isInstanceOf(CameraStateClosed::class.java)
val closedState = listener.state.value as CameraStateClosed
assertThat(closedState.cameraId).isEqualTo(cameraId)
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_CLOSED)
assertThat(closedState.cameraRetryCount).isEqualTo(0)
assertThat(closedState.cameraException).isNull()
assertThat(closedState.cameraRetryDurationNs?.value).isAtLeast(1)
assertThat(closedState.cameraOpenDurationNs?.value).isAtLeast(1)
assertThat(closedState.cameraActiveDurationNs?.value).isAtLeast(1)
// Closing duration measures how long "close()" takes to invoke on the camera device.
// However, shimming the clocks is difficult.
assertThat(closedState.cameraClosingDurationNs).isNotNull()
}
@Test
fun multipleCloseEventsReportFirstEvent() {
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
listener.onDisconnected(testCamera.cameraDevice)
listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
listener.onClosed(testCamera.cameraDevice)
mainLooper.idle()
val closedState = listener.state.value as CameraStateClosed
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_DISCONNECTED)
}
@Test
fun closingStateReportsAppClose() {
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
listener.close()
mainLooper.idle()
val closedState = listener.state.value as CameraStateClosed
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.APP_CLOSED)
}
@Test
fun closingWithExceptionIsReported() {
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
listener.closeWith(IllegalArgumentException("Test Exception"))
mainLooper.idle()
val closedState = listener.state.value as CameraStateClosed
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_EXCEPTION)
}
@Test
fun errorCodesAreReported() {
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
mainLooper.idle()
val closedState = listener.state.value as CameraStateClosed
assertThat(closedState.cameraClosedReason).isEqualTo(ClosedReason.CAMERA2_ERROR)
assertThat(closedState.cameraErrorCode).isEqualTo(CameraError.ERROR_CAMERA_SERVICE)
assertThat(closedState.cameraException).isNull()
}
@Test
fun errorCodesAreReportedToGraphListener() {
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser,
)
listener.onOpened(testCamera.cameraDevice)
listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
mainLooper.idle()
assertThat(cameraErrorListener.lastCameraId).isEqualTo(testCamera.cameraId)
assertThat(cameraErrorListener.lastCameraError).isEqualTo(CameraError.ERROR_CAMERA_SERVICE)
}
@Test
fun errorCodesAreReportedToGraphListenerWhenCameraIsNotOpened() {
// Unless this is a camera open exception, all errors should be reported even if camera is
// not opened. The main reason is CameraAccessException.CAMERA_ERROR, where under which, we
// only know the nature of the error true onError(), and we should and would report that.
val listener =
AndroidCameraState(
testCamera.cameraId,
testCamera.metadata,
attemptNumber = 1,
attemptTimestampNanos = now,
timeSource,
cameraErrorListener,
cameraDeviceCloser
)
listener.onError(testCamera.cameraDevice, CameraDevice.StateCallback.ERROR_CAMERA_SERVICE)
mainLooper.idle()
assertThat(cameraErrorListener.lastCameraId).isEqualTo(testCamera.cameraId)
assertThat(cameraErrorListener.lastCameraError).isEqualTo(CameraError.ERROR_CAMERA_SERVICE)
}
}