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