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.platform.test.annotations.RequiresFlagsEnabled
32 import android.view.Surface
33 import android.virtualdevice.cts.camera.VirtualCameraUtils.BACK_CAMERA_ID
34 import android.virtualdevice.cts.camera.VirtualCameraUtils.INFO_DEVICE_ID
35 import android.virtualdevice.cts.camera.VirtualCameraUtils.assertImagesSimilar
36 import android.virtualdevice.cts.camera.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 @RequiresFlagsEnabled(
75 android.companion.virtual.flags.Flags.FLAG_VIRTUAL_CAMERA,
76 android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_CAMERA_SERVICE_DISCOVERY,
77 android.companion.virtualdevice.flags.Flags.FLAG_CAMERA_DEVICE_AWARENESS
78 )
79 @RunWith(AndroidJUnit4::class)
80 class VirtualCameraCameraXTest {
81
82 private var activity: AppCompatActivity? = null
83 private var cameraProvider: ProcessCameraProvider? = null
84 private var virtualDevice: VirtualDeviceManager.VirtualDevice? = null
85 private var vdContext: Context? = null
86
87 private val sameThreadExecutor: (Runnable) -> Unit = Runnable::run
88
89 @get:Rule
90 val virtualDeviceRule: VirtualDeviceRule = VirtualDeviceRule.withAdditionalPermissions(
91 Manifest.permission.GRANT_RUNTIME_PERMISSIONS
92 ).withVirtualCameraSupportCheck()
93
94 @Before
95 fun setUp() {
96 val deviceParams = VirtualDeviceParams.Builder()
97 .setDevicePolicy(
98 VirtualDeviceParams.POLICY_TYPE_CAMERA,
99 VirtualDeviceParams.DEVICE_POLICY_CUSTOM
100 )
101 .build()
102
103 val virtualDevice = virtualDeviceRule.createManagedVirtualDevice(deviceParams)
104 this.virtualDevice = virtualDevice
105 VirtualCameraUtils.grantCameraPermission(virtualDevice.deviceId)
106
107 val virtualDisplay = virtualDeviceRule.createManagedVirtualDisplay(
108 virtualDevice,
109 VirtualDeviceRule.TRUSTED_VIRTUAL_DISPLAY_CONFIG
110 )!!
111
112 val activity = virtualDeviceRule.startActivityOnDisplaySync(
113 virtualDisplay,
114 AppCompatActivity::class.java
115 )
116 this.activity = activity
117
118 val vdContext = activity.createDeviceContext(virtualDevice.deviceId)
119 this.vdContext = vdContext
120 }
121
122 private fun initCameraXProvider(context: Context) {
123 val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
124 .setCameraProviderInitRetryPolicy(RetryPolicy.NEVER)
125 .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
126 .build()
127 ProcessCameraProvider.configureInstance(cameraXConfig)
128 cameraProvider = ProcessCameraProvider.getInstance(context).get(10, TimeUnit.SECONDS)!!
129 }
130
131 @After
132 fun tearDown() {
133 runBlocking {
134 withContext(Dispatchers.Main) {
135 activity?.finish()
136 cameraProvider?.unbindAll()
137
138 // If we don't shutdown the camera provider, the metadata are
139 // cached and the device id is stall
140 cameraProvider?.shutdownAsync()?.await()
141 }
142 }
143 }
144
145 @Test
146 fun virtualDeviceContext_takePicture() {
147 val golden = loadBitmapFromRaw(R.raw.golden_camerax_virtual_camera)
148
149 createVirtualCamera(
150 lensFacing = CameraMetadata.LENS_FACING_BACK
151 ) { surface ->
152 val canvas: Canvas = surface.lockCanvas(null)
153 canvas.drawBitmap(golden, 0f, 0f, null)
154 surface.unlockCanvasAndPost(canvas)
155 }
156
157 initCameraXProvider(vdContext!!)
158
159 val imageCapture = ImageCapture.Builder()
160 .setFlashMode(FLASH_MODE_OFF)
161 .build()
162
163 val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
164
165 val imageFile = takeAndSavePicture(cameraSelector, imageCapture)
166 assertThat(imageFile.exists()).isTrue()
167 val bitmap = BitmapFactory.decodeFile(imageFile.path)
168
169 assertImagesSimilar(
170 bitmap,
171 golden,
172 "camerax_virtual_camera",
173 5.0
174 )
175 }
176
177 @Test
178 fun virtualDeviceContext_availableCameraInfos_returnsVirtualCameras() {
179 createVirtualCamera(
180 lensFacing = CameraMetadata.LENS_FACING_BACK
181 )
182 initCameraXProvider(vdContext!!)
183 runBlockingWithTimeout {
184 withContext(Dispatchers.Main) {
185 cameraProvider!!.bindToLifecycle(
186 activity!!,
187 CameraSelector.DEFAULT_BACK_CAMERA
188 )
189 }
190 }
191
192 val camera2Infos = cameraProvider!!.availableCameraInfos
193 .map(Camera2CameraInfo::from)
194
195 val ids: List<String> = camera2Infos
196 .map { it.cameraId }
197
198 val cameraManager = vdContext!!.getSystemService(CameraManager::class.java)
199 val cameraIdList: Array<String> =
200 cameraManager!!.cameraIdList
201 assertThat(ids).containsExactlyElementsIn(cameraIdList.asList())
202 assertThat(ids).containsExactly(BACK_CAMERA_ID)
203 assertThat(
204 cameraManager.getCameraCharacteristics(BACK_CAMERA_ID)
205 .get(INFO_DEVICE_ID)
206 ).isEqualTo(virtualDevice!!.deviceId)
207 assertThat(camera2Infos[0].getCameraCharacteristic(INFO_DEVICE_ID))
208 .isEqualTo(virtualDevice!!.deviceId)
209 }
210
211 private fun takeAndSavePicture(
212 cameraSelector: CameraSelector,
213 imageCapture: ImageCapture
214 ): File {
215 val imageFile = File(
216 InstrumentationRegistry.getInstrumentation().targetContext.filesDir,
217 "test_image.jpg"
218 )
219 runBlockingWithTimeout {
220 withContext(Dispatchers.Main) {
221 cameraProvider!!.bindToLifecycle(
222 activity!!,
223 cameraSelector,
224 imageCapture
225 )
226 }
227 suspendCoroutine { cont ->
228 imageCapture.takePicture(
229 OutputFileOptions.Builder(imageFile).build(),
230 ContextCompat.getMainExecutor(vdContext!!),
231 object : ImageCapture.OnImageSavedCallback {
232 override fun onImageSaved(
233 outputFileResults: ImageCapture.OutputFileResults
234 ) {
235 cont.resumeWith(Result.success(outputFileResults))
236 }
237
238 override fun onError(exception: ImageCaptureException) {
239 fail(exception.stackTrace.joinToString("\n") { it.toString() })
240 }
241 }
242 )
243 }
244 }
245 return imageFile
246 }
247
248 private fun createVirtualCamera(
249 inputWidth: Int = VIRTUAL_CAMERA_WIDTH,
250 inputHeight: Int = VIRTUAL_CAMERA_HEIGHT,
251 inputFormat: Int = ImageFormat.YUV_420_888,
252 lensFacing: Int = CameraMetadata.LENS_FACING_BACK,
253 surfaceWriter: (Surface) -> Unit = {}
254 ): VirtualCamera? {
255 val cameraCallBack = object : VirtualCameraCallback {
256
257 private var inputSurface: Surface? = null
258
259 override fun onStreamConfigured(
260 streamId: Int,
261 surface: Surface,
262 width: Int,
263 height: Int,
264 format: Int
265 ) {
266 inputSurface = surface
267 surfaceWriter(inputSurface!!)
268 }
269
270 override fun onStreamClosed(streamId: Int) = Unit
271 }
272 val config = VirtualCameraConfig.Builder("CameraXVirtualCamera")
273 .addStreamConfig(inputWidth, inputHeight, inputFormat, 30)
274 .setVirtualCameraCallback(sameThreadExecutor, cameraCallBack)
275 .setSensorOrientation(VirtualCameraConfig.SENSOR_ORIENTATION_0)
276 .setLensFacing(lensFacing)
277 .build()
278 try {
279 return virtualDevice!!.createVirtualCamera(config)
280 } catch (e: UnsupportedOperationException) {
281 Assume.assumeNoException("Virtual camera is not available on this device", e)
282 return null
283 }
284 }
285 }
286
runBlockingWithTimeoutnull287 private fun <T> runBlockingWithTimeout(block: suspend CoroutineScope.() -> T) {
288 var exception: Throwable? = null
289 runBlocking {
290 try {
291 withTimeout(2000) {
292 block()
293 }
294 } catch (ex: kotlinx.coroutines.TimeoutCancellationException) {
295 exception = ex
296 }
297 }
298 // Rethrow from outside the coroutine to get the stacktrace
299 exception?.let { throw TimeoutException() }
300 }
301