1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package android.virtualdevice.cts.camera
18
19 import android.Manifest
20 import android.companion.virtual.VirtualDeviceManager
21 import android.companion.virtual.VirtualDeviceParams
22 import android.companion.virtual.camera.VirtualCamera
23 import android.companion.virtual.camera.VirtualCameraCallback
24 import android.companion.virtual.camera.VirtualCameraConfig
25 import android.content.Context
26 import android.graphics.BitmapFactory
27 import android.graphics.Canvas
28 import android.graphics.ImageFormat
29 import android.hardware.camera2.CameraManager
30 import android.hardware.camera2.CameraMetadata
31 import android.view.Surface
32 import android.virtualdevice.cts.camera.util.VirtualCameraUtils
33 import android.virtualdevice.cts.camera.util.VirtualCameraUtils.BACK_CAMERA_ID
34 import android.virtualdevice.cts.camera.util.VirtualCameraUtils.INFO_DEVICE_ID
35 import android.virtualdevice.cts.camera.util.VirtualCameraUtils.assertImagesSimilar
36 import android.virtualdevice.cts.camera.util.VirtualCameraUtils.loadBitmapFromRaw
37 import android.virtualdevice.cts.common.VirtualDeviceRule
38 import androidx.appcompat.app.AppCompatActivity
39 import androidx.camera.camera2.Camera2Config
40 import androidx.camera.camera2.interop.Camera2CameraInfo
41 import androidx.camera.core.CameraSelector
42 import androidx.camera.core.CameraXConfig
43 import androidx.camera.core.ImageCapture
44 import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
45 import androidx.camera.core.ImageCapture.OutputFileOptions
46 import androidx.camera.core.ImageCaptureException
47 import androidx.camera.core.RetryPolicy
48 import androidx.camera.lifecycle.ProcessCameraProvider
49 import androidx.concurrent.futures.await
50 import androidx.core.content.ContextCompat
51 import androidx.test.ext.junit.runners.AndroidJUnit4
52 import androidx.test.platform.app.InstrumentationRegistry
53 import com.google.common.truth.Truth.assertThat
54 import java.io.File
55 import java.util.concurrent.TimeUnit
56 import java.util.concurrent.TimeoutException
57 import junit.framework.Assert.fail
58 import kotlin.coroutines.suspendCoroutine
59 import kotlinx.coroutines.CoroutineScope
60 import kotlinx.coroutines.Dispatchers
61 import kotlinx.coroutines.runBlocking
62 import kotlinx.coroutines.withContext
63 import kotlinx.coroutines.withTimeout
64 import org.junit.After
65 import org.junit.Assume
66 import org.junit.Before
67 import org.junit.Rule
68 import org.junit.Test
69 import org.junit.runner.RunWith
70
71 private const val VIRTUAL_CAMERA_WIDTH = 460
72 private const val VIRTUAL_CAMERA_HEIGHT = 260
73
74 @RunWith(AndroidJUnit4::class)
75 class VirtualCameraCameraXTest {
76
77 private var activity: AppCompatActivity? = null
78 private var cameraProvider: ProcessCameraProvider? = null
79 private var virtualDevice: VirtualDeviceManager.VirtualDevice? = null
80 private var vdContext: Context? = null
81
82 private val sameThreadExecutor: (Runnable) -> Unit = Runnable::run
83
84 @get:Rule
85 val virtualDeviceRule: VirtualDeviceRule = VirtualDeviceRule.withAdditionalPermissions(
86 Manifest.permission.GRANT_RUNTIME_PERMISSIONS
87 )
88
89 @Before
90 fun setUp() {
91 val deviceParams = VirtualDeviceParams.Builder()
92 .setDevicePolicy(
93 VirtualDeviceParams.POLICY_TYPE_CAMERA,
94 VirtualDeviceParams.DEVICE_POLICY_CUSTOM
95 )
96 .build()
97
98 val virtualDevice = virtualDeviceRule.createManagedVirtualDevice(deviceParams)
99 this.virtualDevice = virtualDevice
100 VirtualCameraUtils.grantCameraPermission(virtualDevice.deviceId)
101
102 val virtualDisplay = virtualDeviceRule.createManagedVirtualDisplay(
103 virtualDevice,
104 VirtualDeviceRule.createTrustedVirtualDisplayConfigBuilder()
105 )!!
106
107 val activity = virtualDeviceRule.startActivityOnDisplaySync(
108 virtualDisplay,
109 AppCompatActivity::class.java
110 )
111 this.activity = activity
112
113 val vdContext = activity.createDeviceContext(virtualDevice.deviceId)
114 this.vdContext = vdContext
115 }
116
117 private fun initCameraXProvider(context: Context) {
118 val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
119 .setCameraProviderInitRetryPolicy(RetryPolicy.NEVER)
120 .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
121 .build()
122 ProcessCameraProvider.configureInstance(cameraXConfig)
123 cameraProvider = ProcessCameraProvider.getInstance(context).get(10, TimeUnit.SECONDS)!!
124 }
125
126 @After
127 fun tearDown() {
128 runBlocking {
129 withContext(Dispatchers.Main) {
130 activity?.finish()
131 cameraProvider?.unbindAll()
132
133 // If we don't shutdown the camera provider, the metadata are
134 // cached and the device id is stall
135 cameraProvider?.shutdownAsync()?.await()
136 }
137 }
138 }
139
140 @Test
141 fun virtualDeviceContext_takePicture() {
142 val golden = loadBitmapFromRaw(R.raw.golden_camerax_virtual_camera)
143
144 createVirtualCamera(
145 lensFacing = CameraMetadata.LENS_FACING_BACK
146 ) { surface ->
147 val canvas: Canvas = surface.lockCanvas(null)
148 canvas.drawBitmap(golden, 0f, 0f, null)
149 surface.unlockCanvasAndPost(canvas)
150 }
151
152 initCameraXProvider(vdContext!!)
153
154 val imageCapture = ImageCapture.Builder()
155 .setFlashMode(FLASH_MODE_OFF)
156 .build()
157
158 val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
159
160 val imageFile = takeAndSavePicture(cameraSelector, imageCapture)
161 assertThat(imageFile.exists()).isTrue()
162 val bitmap = BitmapFactory.decodeFile(imageFile.path)
163
164 assertImagesSimilar(
165 bitmap,
166 golden,
167 "camerax_virtual_camera",
168 10.0
169 )
170 }
171
172 @Test
173 fun virtualDeviceContext_availableCameraInfos_returnsVirtualCameras() {
174 createVirtualCamera(
175 lensFacing = CameraMetadata.LENS_FACING_BACK
176 )
177 initCameraXProvider(vdContext!!)
178 runBlockingWithTimeout {
179 withContext(Dispatchers.Main) {
180 cameraProvider!!.bindToLifecycle(
181 activity!!,
182 CameraSelector.DEFAULT_BACK_CAMERA
183 )
184 }
185 }
186
187 val camera2Infos = cameraProvider!!.availableCameraInfos
188 .map(Camera2CameraInfo::from)
189
190 val ids: List<String> = camera2Infos
191 .map { it.cameraId }
192
193 val cameraManager = vdContext!!.getSystemService(CameraManager::class.java)
194 val cameraIdList: Array<String> =
195 cameraManager!!.cameraIdList
196 assertThat(ids).containsExactlyElementsIn(cameraIdList.asList())
197 assertThat(ids).containsExactly(BACK_CAMERA_ID)
198 assertThat(
199 cameraManager.getCameraCharacteristics(BACK_CAMERA_ID)
200 .get(INFO_DEVICE_ID)
201 ).isEqualTo(virtualDevice!!.deviceId)
202 assertThat(camera2Infos[0].getCameraCharacteristic(INFO_DEVICE_ID))
203 .isEqualTo(virtualDevice!!.deviceId)
204 }
205
206 private fun takeAndSavePicture(
207 cameraSelector: CameraSelector,
208 imageCapture: ImageCapture
209 ): File {
210 val imageFile = File(
211 InstrumentationRegistry.getInstrumentation().targetContext.filesDir,
212 "test_image.jpg"
213 )
214 runBlockingWithTimeout {
215 withContext(Dispatchers.Main) {
216 cameraProvider!!.bindToLifecycle(
217 activity!!,
218 cameraSelector,
219 imageCapture
220 )
221 }
222 suspendCoroutine { cont ->
223 imageCapture.takePicture(
224 OutputFileOptions.Builder(imageFile).build(),
225 ContextCompat.getMainExecutor(vdContext!!),
226 object : ImageCapture.OnImageSavedCallback {
227 override fun onImageSaved(
228 outputFileResults: ImageCapture.OutputFileResults
229 ) {
230 cont.resumeWith(Result.success(outputFileResults))
231 }
232
233 override fun onError(exception: ImageCaptureException) {
234 fail(exception.stackTrace.joinToString("\n") { it.toString() })
235 }
236 }
237 )
238 }
239 }
240 return imageFile
241 }
242
243 private fun createVirtualCamera(
244 inputWidth: Int = VIRTUAL_CAMERA_WIDTH,
245 inputHeight: Int = VIRTUAL_CAMERA_HEIGHT,
246 inputFormat: Int = ImageFormat.YUV_420_888,
247 lensFacing: Int = CameraMetadata.LENS_FACING_BACK,
248 surfaceWriter: (Surface) -> Unit = {}
249 ): VirtualCamera? {
250 val cameraCallBack = object : VirtualCameraCallback {
251
252 private var inputSurface: Surface? = null
253
254 override fun onStreamConfigured(
255 streamId: Int,
256 surface: Surface,
257 width: Int,
258 height: Int,
259 format: Int
260 ) {
261 inputSurface = surface
262 surfaceWriter(inputSurface!!)
263 }
264
265 override fun onStreamClosed(streamId: Int) = Unit
266 }
267 val config = VirtualCameraConfig.Builder("CameraXVirtualCamera")
268 .addStreamConfig(inputWidth, inputHeight, inputFormat, 30)
269 .setVirtualCameraCallback(sameThreadExecutor, cameraCallBack)
270 .setSensorOrientation(VirtualCameraConfig.SENSOR_ORIENTATION_0)
271 .setLensFacing(lensFacing)
272 .build()
273 try {
274 return virtualDevice!!.createVirtualCamera(config)
275 } catch (e: UnsupportedOperationException) {
276 Assume.assumeNoException("Virtual camera is not available on this device", e)
277 return null
278 }
279 }
280 }
281
runBlockingWithTimeoutnull282 private fun <T> runBlockingWithTimeout(block: suspend CoroutineScope.() -> T) {
283 var exception: Throwable? = null
284 runBlocking {
285 try {
286 withTimeout(2000) {
287 block()
288 }
289 } catch (ex: kotlinx.coroutines.TimeoutCancellationException) {
290 exception = ex
291 }
292 }
293 // Rethrow from outside the coroutine to get the stacktrace
294 exception?.let { throw TimeoutException() }
295 }
296