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