blob: 774a8ceba47a3e92fbc344320f1e5dd30817b2b5 [file] [log] [blame]
/*
* Copyright 2020 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:Suppress("DEPRECATION")
package androidx.camera.camera2.pipe.testing
import android.Manifest
import android.app.Application
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.os.Handler
import android.os.Looper
import android.util.Size
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamConfig
import androidx.camera.camera2.pipe.StreamFormat
import androidx.camera.camera2.pipe.StreamType
import androidx.camera.camera2.pipe.impl.CameraGraphModules
import androidx.camera.camera2.pipe.impl.CameraMetadataImpl
import androidx.camera.camera2.pipe.impl.CameraPipeModules
import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
import androidx.test.core.app.ApplicationProvider
import dagger.Module
import dagger.Provides
import kotlinx.atomicfu.atomic
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowApplication
import org.robolectric.shadows.ShadowCameraCharacteristics
import org.robolectric.shadows.ShadowCameraManager
import java.lang.UnsupportedOperationException
import javax.inject.Singleton
/**
* Utility class for creating, configuring, and interacting with FakeCamera objects via Robolectric
*/
object FakeCameras {
private val cameraIds = atomic(0)
val application: Application
get() {
val app: Application = ApplicationProvider.getApplicationContext()
val shadowApp: ShadowApplication = shadowOf(app)
shadowApp.grantPermissions(Manifest.permission.CAMERA)
return app
}
private val cameraManager: CameraManager
get() = application.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private val initializedCameraIds = mutableSetOf<CameraId>()
/**
* This will create, configure, and add the specified CameraCharacteristics to the Robolectric
* CameraManager, which allows the camera characteristics to be queried for tests and for Fake
* CameraDevice objects to be created for tests.
*/
fun create(
metadata: Map<CameraCharacteristics.Key<*>, Any> = emptyMap()
): CameraId {
val shadowCameraManager = Shadow.extract<Any>(
cameraManager
) as ShadowCameraManager
val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
val shadowCharacteristics = Shadow.extract<ShadowCameraCharacteristics>(characteristics)
// Configure the camera characteristics
for (entry in metadata) {
shadowCharacteristics.set(entry.key, entry.value)
}
val cameraNumber = cameraIds.incrementAndGet()
// Note: It would be better if this was something like "Fake-Camera-1", but robolectric
// will not let you open a CameraDevice unless the id is numeric.
val cameraId = CameraId("FakeCamera-$cameraNumber")
// Add the camera to the camera service
shadowCameraManager.addCamera(cameraId.value, characteristics)
initializedCameraIds.add(cameraId)
return cameraId
}
operator fun get(fakeCameraId: CameraId): CameraCharacteristics {
check(initializedCameraIds.contains(fakeCameraId))
return cameraManager.getCameraCharacteristics(fakeCameraId.value)
}
fun open(cameraId: CameraId): FakeCamera {
check(initializedCameraIds.contains(cameraId))
val characteristics = cameraManager.getCameraCharacteristics(cameraId.value)
val metadata = CameraMetadataImpl(cameraId, false, characteristics, emptyMap())
val callback = CameraStateCallback(cameraId)
cameraManager.openCamera(
cameraId.value,
callback,
Handler()
)
shadowOf(Looper.myLooper()).idle()
val cameraDevice = callback.camera!!
val cameraDeviceWrapper = AndroidCameraDevice(metadata, cameraDevice, cameraId)
return FakeCamera(
cameraId,
characteristics,
metadata,
cameraDevice,
cameraDeviceWrapper
)
}
/** Remove all fake cameras */
fun removeAll() {
val shadowCameraManager = Shadow.extract<Any>(
cameraManager
) as ShadowCameraManager
for (cameraId in initializedCameraIds) {
try {
shadowCameraManager.removeCamera(cameraId.value)
} catch (e: Throwable) {
// Ignored - This is a cleanup task.
}
}
initializedCameraIds.clear()
}
/**
* The [FakeCamera] instance wraps up several useful objects for use in tests.
*/
data class FakeCamera(
val cameraId: CameraId,
val characteristics: CameraCharacteristics,
val metadata: CameraMetadata,
val cameraDevice: CameraDevice,
val cameraDeviceWrapper: CameraDeviceWrapper
)
private class CameraStateCallback(private val cameraId: CameraId) :
CameraDevice.StateCallback() {
var camera: CameraDevice? = null
override fun onOpened(cameraDevice: CameraDevice) {
check(cameraDevice.id == cameraId.value)
this.camera = cameraDevice
}
override fun onDisconnected(camera: CameraDevice) {
throw UnsupportedOperationException(
"onDisconnected is not expected for Robolectric Camera"
)
}
override fun onError(
camera: CameraDevice,
error: Int
) {
throw UnsupportedOperationException("onError is not expected for Robolectric Camera")
}
}
/**
* Utility module for testing the Dagger generated graph with a a reasonable default config.
*/
@Module(includes = [CameraPipeModules::class])
class FakeCameraPipeModule(
private val context: Context,
private val fakeCamera: FakeCamera
) {
@Provides
@Singleton
fun provideFakeCameraPipeConfig() = CameraPipe.Config(context)
@Provides
@Singleton
fun provideFakeGraphConfig() = CameraGraph.Config(
camera = fakeCamera.cameraId,
streams = listOf(
StreamConfig(
Size(640, 480),
StreamFormat.YUV_420_888,
fakeCamera.cameraId,
StreamType.SURFACE
)
),
template = RequestTemplate(0)
)
}
@Module(includes = [CameraGraphModules::class])
class FakeCameraGraphModule
}