1 /*
<lambda>null2  * Copyright 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 androidx.camera.compose
18 
19 import android.content.Context
20 import androidx.camera.camera2.Camera2Config
21 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
22 import androidx.camera.core.Camera
23 import androidx.camera.core.CameraXConfig
24 import androidx.camera.core.Preview
25 import androidx.camera.core.SurfaceRequest
26 import androidx.camera.lifecycle.ProcessCameraProvider
27 import androidx.camera.testing.impl.CameraPipeConfigTestRule
28 import androidx.camera.testing.impl.CameraUtil
29 import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
30 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
31 import androidx.camera.viewfinder.core.ImplementationMode
32 import androidx.compose.runtime.collectAsState
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableStateOf
35 import androidx.compose.runtime.setValue
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.platform.testTag
38 import androidx.compose.ui.test.SemanticsMatcher
39 import androidx.compose.ui.test.assert
40 import androidx.compose.ui.test.assertIsDisplayed
41 import androidx.compose.ui.test.assertIsNotDisplayed
42 import androidx.compose.ui.test.junit4.createComposeRule
43 import androidx.compose.ui.test.onNodeWithTag
44 import androidx.concurrent.futures.await
45 import androidx.test.core.app.ApplicationProvider
46 import androidx.test.filters.LargeTest
47 import com.google.common.truth.Truth.assertThat
48 import kotlin.coroutines.CoroutineContext
49 import kotlin.coroutines.resume
50 import kotlin.time.Duration.Companion.seconds
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.DelicateCoroutinesApi
53 import kotlinx.coroutines.Dispatchers
54 import kotlinx.coroutines.flow.MutableStateFlow
55 import kotlinx.coroutines.flow.StateFlow
56 import kotlinx.coroutines.flow.asStateFlow
57 import kotlinx.coroutines.flow.filterNotNull
58 import kotlinx.coroutines.flow.first
59 import kotlinx.coroutines.flow.produceIn
60 import kotlinx.coroutines.flow.take
61 import kotlinx.coroutines.runBlocking
62 import kotlinx.coroutines.suspendCancellableCoroutine
63 import kotlinx.coroutines.withContext
64 import kotlinx.coroutines.withTimeout
65 import org.junit.Rule
66 import org.junit.Test
67 import org.junit.runner.RunWith
68 import org.junit.runners.Parameterized
69 
70 @LargeTest
71 @RunWith(Parameterized::class)
72 class CameraXViewfinderTest(private val implName: String, private val cameraConfig: CameraXConfig) {
73     @get:Rule
74     val cameraPipeConfigTestRule =
75         CameraPipeConfigTestRule(
76             active = implName == CameraPipeConfig::class.simpleName,
77         )
78 
79     @get:Rule
80     val useCamera =
81         CameraUtil.grantCameraPermissionAndPreTestAndPostTest(PreTestCameraIdList(cameraConfig))
82 
83     @get:Rule val composeTest = createComposeRule()
84 
85     @Test
86     fun viewfinderIsDisplayed_withValidSurfaceRequest() = runViewfinderTest {
87         composeTest.setContent {
88             val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
89             currentSurfaceRequest?.let { surfaceRequest ->
90                 CameraXViewfinder(
91                     surfaceRequest = surfaceRequest,
92                     modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
93                 )
94             }
95         }
96 
97         // Start the camera
98         startCamera()
99 
100         // Wait for first SurfaceRequest
101         surfaceRequests.filterNotNull().first()
102 
103         composeTest.awaitIdle()
104 
105         // CameraXViewfinder should now have a child Viewfinder
106         composeTest
107             .onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG)
108             .assertIsDisplayed()
109             .assert(SemanticsMatcher.hasChild())
110     }
111 
112     @OptIn(DelicateCoroutinesApi::class)
113     @Test
114     fun changingImplementation_sendsNewSurfaceRequest() = runViewfinderTest {
115         var implementationMode: ImplementationMode by mutableStateOf(ImplementationMode.EXTERNAL)
116         composeTest.setContent {
117             val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
118             currentSurfaceRequest?.let { surfaceRequest ->
119                 CameraXViewfinder(
120                     surfaceRequest = surfaceRequest,
121                     implementationMode = implementationMode,
122                     modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
123                 )
124             }
125         }
126 
127         // Collect expected number of SurfaceRequests for 2 mode changes
128         val surfaceRequestSequence = surfaceRequests.filterNotNull().take(3).produceIn(this)
129 
130         // Start the camera
131         startCamera()
132 
133         // Swap implementation modes twice to produce 3 SurfaceRequests
134         val allSurfaceRequests = buildList {
135             for (surfaceRequest in surfaceRequestSequence) {
136                 add(surfaceRequest)
137                 composeTest.awaitIdle()
138 
139                 if (!surfaceRequestSequence.isClosedForReceive) {
140                     // Changing the implementation mode will invalidate the previous SurfaceRequest
141                     // and cause Preview to send a new SurfaceRequest
142                     implementationMode = implementationMode.swapMode()
143                     composeTest.awaitIdle()
144                 }
145             }
146         }
147 
148         assertThat(allSurfaceRequests.size).isEqualTo(3)
149         assertThat(allSurfaceRequests).containsNoDuplicates()
150     }
151 
152     @Test
153     fun cancelledSurfaceRequest_doesNotInstantiateViewfinder() = runViewfinderTest {
154         // Start the camera
155         startCamera()
156 
157         // Wait for first SurfaceRequest
158         val surfaceRequest = surfaceRequests.filterNotNull().first()
159 
160         // Reset surface provider to cause cancellation of the last SurfaceRequest
161         resetPreviewSurfaceProvider()
162 
163         // Ensure the SurfaceRequest is cancelled
164         surfaceRequest.awaitCancellation()
165 
166         // Pass on cancelled SurfaceRequest to CameraXViewfinder
167         composeTest.setContent {
168             CameraXViewfinder(
169                 surfaceRequest = surfaceRequest,
170                 modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
171             )
172         }
173 
174         composeTest.awaitIdle()
175 
176         // Viewfinder should not be displayed since SurfaceRequest was cancelled
177         composeTest.onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG).assertIsNotDisplayed()
178     }
179 
180     companion object {
181         @JvmStatic
182         @Parameterized.Parameters(name = "{0}")
183         fun data() =
184             listOf(
185                 arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
186                 arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
187             )
188 
189         private const val CAMERAX_VIEWFINDER_TEST_TAG = "CameraXViewfinderTestTag"
190     }
191 
192     private inline fun runViewfinderTest(crossinline block: suspend PreviewTestScope.() -> Unit) =
193         runBlocking {
194             val context = ApplicationProvider.getApplicationContext<Context>()
195             val cameraProvider =
196                 withTimeout(10.seconds) {
197                     ProcessCameraProvider.configureInstance(cameraConfig)
198                     ProcessCameraProvider.getInstance(context).await()
199                 }
200 
201             var fakeLifecycleOwner: FakeLifecycleOwner? = null
202             try {
203                 val preview = Preview.Builder().build()
204                 val surfaceRequests = MutableStateFlow<SurfaceRequest?>(null)
205                 val resetPreviewSurfaceProvider =
206                     suspend {
207                             withContext(Dispatchers.Main) {
208                                 // Reset the surface provider to a new lambda that will continue to
209                                 // publish to surfaceRequests
210                                 preview.setSurfaceProvider { surfaceRequest ->
211                                     surfaceRequests.value = surfaceRequest
212                                 }
213                             }
214                         }
215                         .also { it.invoke() }
216 
217                 val startCamera = suspend {
218                     withContext(Dispatchers.Main) {
219                         val lifecycleOwner =
220                             FakeLifecycleOwner().apply {
221                                 startAndResume()
222                                 fakeLifecycleOwner = this
223                             }
224 
225                         val firstAvailableCameraSelector =
226                             cameraProvider.availableCameraInfos
227                                 .asSequence()
228                                 .map { it.cameraSelector }
229                                 .first()
230                         cameraProvider.bindToLifecycle(
231                             lifecycleOwner,
232                             firstAvailableCameraSelector,
233                             preview
234                         )
235                     }
236                 }
237 
238                 with(
239                     PreviewTestScope(
240                         surfaceRequests = surfaceRequests.asStateFlow(),
241                         resetPreviewSurfaceProvider = resetPreviewSurfaceProvider,
242                         startCamera = startCamera,
243                         coroutineContext = coroutineContext
244                     )
245                 ) {
246                     block()
247                 }
248             } finally {
249                 fakeLifecycleOwner?.apply {
250                     withContext(Dispatchers.Main) {
251                         pauseAndStop()
252                         destroy()
253                     }
254                 }
255                 withTimeout(30.seconds) { cameraProvider.shutdownAsync().await() }
256             }
257         }
258 
259     private data class PreviewTestScope(
260         val surfaceRequests: StateFlow<SurfaceRequest?>,
261         val resetPreviewSurfaceProvider: suspend () -> Unit,
262         val startCamera: suspend () -> Camera,
263         override val coroutineContext: CoroutineContext
264     ) : CoroutineScope
265 }
266 
swapModenull267 private fun ImplementationMode.swapMode(): ImplementationMode {
268     return when (this) {
269         ImplementationMode.EXTERNAL -> ImplementationMode.EMBEDDED
270         ImplementationMode.EMBEDDED -> ImplementationMode.EXTERNAL
271     }
272 }
273 
hasChildnull274 private fun SemanticsMatcher.Companion.hasChild() =
275     SemanticsMatcher("Has child") { node -> node.children.isNotEmpty() }
276 
awaitCancellationnull277 private suspend fun SurfaceRequest.awaitCancellation(): Unit = suspendCancellableCoroutine { cont ->
278     addRequestCancellationListener(Runnable::run) { cont.resume(Unit) }
279 }
280