1 /*
2  * 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.viewfinder.compose
18 
19 import android.os.Build
20 import android.util.Size
21 import android.view.Surface
22 import androidx.annotation.RequiresApi
23 import androidx.camera.viewfinder.core.ImplementationMode
24 import androidx.camera.viewfinder.core.TransformationInfo
25 import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.size
28 import androidx.compose.runtime.Composable
29 import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.mutableStateOf
31 import androidx.compose.runtime.remember
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.graphics.Matrix
34 import androidx.compose.ui.platform.LocalDensity
35 import androidx.compose.ui.test.junit4.createComposeRule
36 import androidx.test.ext.junit.runners.AndroidJUnit4
37 import androidx.test.filters.MediumTest
38 import androidx.test.filters.SdkSuppress
39 import com.google.common.truth.Truth.assertThat
40 import kotlinx.coroutines.CompletableDeferred
41 import kotlinx.coroutines.runBlocking
42 import org.junit.Ignore
43 import org.junit.Rule
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 
47 @MediumTest
48 @RunWith(AndroidJUnit4::class)
49 class ViewfinderTest {
50     @get:Rule val rule = createComposeRule()
51 
52     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
53     @Test
<lambda>null54     fun canRetrievePerformanceSurface() = runBlocking {
55         assertCanRetrieveSurface(implementationMode = ImplementationMode.EXTERNAL)
56     }
57 
58     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
59     @Test
<lambda>null60     fun canRetrieveCompatibleSurface() = runBlocking {
61         assertCanRetrieveSurface(implementationMode = ImplementationMode.EMBEDDED)
62     }
63 
64     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
65     @Test
<lambda>null66     fun coordinatesTransformationSameSizeNoRotation(): Unit = runBlocking {
67         val coordinateTransformer = MutableCoordinateTransformer()
68 
69         rule.setContent {
70             with(LocalDensity.current) {
71                 TestViewfinder(
72                     modifier = Modifier.size(540.toDp(), 960.toDp()),
73                     coordinateTransformer = coordinateTransformer
74                 ) {}
75             }
76         }
77 
78         val expectedMatrix =
79             Matrix(
80                 values =
81                     floatArrayOf(1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f)
82             )
83 
84         assertThat(coordinateTransformer.transformMatrix.values).isEqualTo(expectedMatrix.values)
85     }
86 
87     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
88     @Test
<lambda>null89     fun coordinatesTransformationSameSizeWithHalfCrop(): Unit = runBlocking {
90         // Viewfinder size: 1080x1920
91         // Surface size: 1080x1920
92         // Crop rect size: 540x960
93 
94         val coordinateTransformer = MutableCoordinateTransformer()
95 
96         rule.setContent {
97             with(LocalDensity.current) {
98                 TestViewfinder(
99                     modifier = Modifier.size(540.toDp(), 960.toDp()),
100                     surfaceRequest =
101                         ViewfinderSurfaceRequest(
102                             width = ViewfinderTestParams.Default.sourceResolution.width,
103                             height = ViewfinderTestParams.Default.sourceResolution.height,
104                             implementationMode = ImplementationMode.EXTERNAL,
105                         ),
106                     transformationInfo =
107                         TransformationInfo(
108                             sourceRotation = 0,
109                             isSourceMirroredHorizontally = false,
110                             isSourceMirroredVertically = false,
111                             cropRectLeft = 0f,
112                             cropRectTop = 0f,
113                             cropRectRight = 270f,
114                             cropRectBottom = 480f
115                         ),
116                     coordinateTransformer = coordinateTransformer
117                 ) {}
118             }
119         }
120 
121         val expectedMatrix =
122             Matrix(
123                 values =
124                     floatArrayOf(0.5f, 0f, 0f, 0f, 0f, 0.5f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f)
125             )
126 
127         assertThat(coordinateTransformer.transformMatrix.values).isEqualTo(expectedMatrix.values)
128     }
129 
130     @Test
verifySurfacesAreReleased_surfaceRequestReleased_thenComposableDestroyednull131     fun verifySurfacesAreReleased_surfaceRequestReleased_thenComposableDestroyed(): Unit =
132         runBlocking {
133             val surfaceDeferred = CompletableDeferred<Surface>()
134             val sessionCompleteDeferred = CompletableDeferred<Unit>()
135 
136             val showViewfinder = mutableStateOf(true)
137 
138             rule.setContent {
139                 val showView by remember { showViewfinder }
140                 TestViewfinder(showViewfinder = showView) {
141                     onSurfaceSession {
142                         surfaceDeferred.complete(surface)
143                         sessionCompleteDeferred.await()
144                     }
145                 }
146             }
147 
148             val surface = surfaceDeferred.await()
149             assertThat(surface.isValid).isTrue()
150 
151             sessionCompleteDeferred.complete(Unit)
152             rule.awaitIdle()
153             assertThat(surface.isValid).isTrue()
154 
155             showViewfinder.value = false
156             rule.awaitIdle()
157             assertThat(surface.isValid).isFalse()
158         }
159 
160     @Ignore("b/390508238: Surface release needs to be delayed by TextureView/SurfaceView ")
161     @Test
verifySurfacesAreReleased_composableDestroyed_thenSurfaceRequestReleasednull162     fun verifySurfacesAreReleased_composableDestroyed_thenSurfaceRequestReleased(): Unit =
163         runBlocking {
164             val surfaceDeferred = CompletableDeferred<Surface>()
165             val sessionCompleteDeferred = CompletableDeferred<Unit>()
166 
167             val showViewfinder = mutableStateOf(true)
168 
169             rule.setContent {
170                 val showView by remember { showViewfinder }
171                 TestViewfinder(showViewfinder = showView) {
172                     onSurfaceSession { surfaceDeferred.complete(surface) }
173                 }
174             }
175 
176             val surface = surfaceDeferred.await()
177             assertThat(surface.isValid).isTrue()
178 
179             showViewfinder.value = false
180             rule.awaitIdle()
181             assertThat(surface.isValid).isTrue()
182 
183             sessionCompleteDeferred.complete(Unit)
184             rule.awaitIdle()
185             assertThat(surface.isValid).isFalse()
186         }
187 
188     @RequiresApi(Build.VERSION_CODES.M) // Needed for Surface.lockHardwareCanvas()
assertCanRetrieveSurfacenull189     private suspend fun assertCanRetrieveSurface(implementationMode: ImplementationMode) {
190         val surfaceDeferred = CompletableDeferred<Surface>()
191         val surfaceRequest =
192             ViewfinderSurfaceRequest(
193                 width = ViewfinderTestParams.Default.sourceResolution.width,
194                 height = ViewfinderTestParams.Default.sourceResolution.height,
195                 implementationMode = implementationMode
196             )
197         rule.setContent {
198             TestViewfinder(surfaceRequest = surfaceRequest) {
199                 onSurfaceSession { surfaceDeferred.complete(surface) }
200             }
201         }
202 
203         val surface = surfaceDeferred.await()
204         surface.lockHardwareCanvas().apply {
205             try {
206                 assertThat(Size(width, height))
207                     .isEqualTo(ViewfinderTestParams.Default.sourceResolution)
208             } finally {
209                 surface.unlockCanvasAndPost(this)
210             }
211         }
212     }
213 }
214 
215 @Composable
TestViewfindernull216 fun TestViewfinder(
217     modifier: Modifier = Modifier.size(ViewfinderTestParams.Default.viewfinderSize),
218     showViewfinder: Boolean = true,
219     transformationInfo: TransformationInfo = ViewfinderTestParams.Default.transformationInfo,
220     surfaceRequest: ViewfinderSurfaceRequest = remember {
221         ViewfinderSurfaceRequest(
222             width = ViewfinderTestParams.Default.sourceResolution.width,
223             height = ViewfinderTestParams.Default.sourceResolution.height,
224             implementationMode = ImplementationMode.EXTERNAL,
225         )
226     },
227     coordinateTransformer: MutableCoordinateTransformer? = null,
228     onInit: ViewfinderInitScope.() -> Unit
229 ) {
<lambda>null230     Column {
231         if (showViewfinder) {
232             Viewfinder(
233                 modifier = modifier,
234                 surfaceRequest = surfaceRequest,
235                 transformationInfo = transformationInfo,
236                 coordinateTransformer = coordinateTransformer,
237                 onInit = onInit
238             )
239         }
240     }
241 }
242