• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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