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 package com.google.jetpackcamera.feature.preview.ui
17 
18 import android.content.pm.ActivityInfo
19 import android.os.Build
20 import android.util.Log
21 import androidx.camera.core.DynamicRange
22 import androidx.camera.core.Preview
23 import androidx.camera.core.SurfaceRequest
24 import androidx.camera.core.SurfaceRequest.TransformationInfo as CXTransformationInfo
25 import androidx.camera.viewfinder.compose.MutableCoordinateTransformer
26 import androidx.camera.viewfinder.compose.Viewfinder
27 import androidx.camera.viewfinder.surface.ImplementationMode
28 import androidx.camera.viewfinder.surface.TransformationInfo
29 import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
30 import androidx.compose.foundation.gestures.detectTapGestures
31 import androidx.compose.foundation.layout.fillMaxSize
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.LaunchedEffect
34 import androidx.compose.runtime.getValue
35 import androidx.compose.runtime.produceState
36 import androidx.compose.runtime.rememberUpdatedState
37 import androidx.compose.runtime.snapshotFlow
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.input.pointer.pointerInput
40 import kotlinx.coroutines.CoroutineStart
41 import kotlinx.coroutines.Runnable
42 import kotlinx.coroutines.flow.MutableStateFlow
43 import kotlinx.coroutines.flow.collect
44 import kotlinx.coroutines.flow.collectLatest
45 import kotlinx.coroutines.flow.combine
46 import kotlinx.coroutines.flow.distinctUntilChanged
47 import kotlinx.coroutines.flow.filterNotNull
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.onCompletion
50 import kotlinx.coroutines.flow.onEach
51 import kotlinx.coroutines.flow.takeWhile
52 import kotlinx.coroutines.launch
53 
54 private const val TAG = "CameraXViewfinder"
55 
56 /**
57  * A composable viewfinder that adapts CameraX's [Preview.SurfaceProvider] to [Viewfinder]
58  *
59  * This adapter code will eventually be upstreamed to CameraX, but for now can be copied
60  * in its entirety to connect CameraX to [Viewfinder].
61  *
62  * @param[modifier] the modifier to be applied to the layout
63  * @param[surfaceRequest] a [SurfaceRequest] from [Preview.SurfaceProvider].
64  * @param[implementationMode] the implementation mode, either [ImplementationMode.EXTERNAL] or
65  * [ImplementationMode.EMBEDDED].
66  */
67 @Composable
68 fun CameraXViewfinder(
69     surfaceRequest: SurfaceRequest,
70     modifier: Modifier = Modifier,
71     implementationMode: ImplementationMode = ImplementationMode.EXTERNAL,
72     onRequestWindowColorMode: (Int) -> Unit = {},
_null73     onTap: (x: Float, y: Float) -> Unit = { _, _ -> }
74 ) {
75     val currentImplementationMode by rememberUpdatedState(implementationMode)
76     val currentOnRequestWindowColorMode by rememberUpdatedState(onRequestWindowColorMode)
77 
<lambda>null78     val viewfinderArgs by produceState<ViewfinderArgs?>(initialValue = null, surfaceRequest) {
79         val viewfinderSurfaceRequest = ViewfinderSurfaceRequest.Builder(surfaceRequest.resolution)
80             .build()
81 
82         surfaceRequest.addRequestCancellationListener(Runnable::run) {
83             viewfinderSurfaceRequest.markSurfaceSafeToRelease()
84         }
85 
86         // Launch undispatched so we always reach the try/finally in this coroutine
87         launch(start = CoroutineStart.UNDISPATCHED) {
88             try {
89                 val surface = viewfinderSurfaceRequest.getSurface()
90                 surfaceRequest.provideSurface(surface, Runnable::run) {
91                     viewfinderSurfaceRequest.markSurfaceSafeToRelease()
92                 }
93             } finally {
94                 // If we haven't provided the surface, such as if we're cancelled
95                 // while suspending on getSurface(), this call will succeed. Otherwise
96                 // it will be a no-op.
97                 surfaceRequest.willNotProvideSurface()
98             }
99         }
100 
101         val transformationInfos = MutableStateFlow<CXTransformationInfo?>(null)
102         surfaceRequest.setTransformationInfoListener(Runnable::run) {
103             transformationInfos.value = it
104         }
105 
106         // The ImplementationMode that will be used for all TransformationInfo updates.
107         // This is locked in once we have updated ViewfinderArgs and won't change until
108         // this produceState block is cancelled and restarted
109         var snapshotImplementationMode: ImplementationMode? = null
110 
111         snapshotFlow { currentImplementationMode }
112             .combine(transformationInfos.filterNotNull()) { implMode, transformInfo ->
113                 Pair(implMode, transformInfo)
114             }.takeWhile { (implMode, _) ->
115                 val shouldAbort =
116                     snapshotImplementationMode != null && implMode != snapshotImplementationMode
117                 if (shouldAbort) {
118                     // Abort flow and invalidate SurfaceRequest so a new one will be sent
119                     surfaceRequest.invalidate()
120                 }
121                 !shouldAbort
122             }.collectLatest { (implMode, transformInfo) ->
123                 // We'll only ever get here with a single non-null implMode,
124                 // so setting it every time is ok
125                 snapshotImplementationMode = implMode
126                 value = ViewfinderArgs(
127                     viewfinderSurfaceRequest,
128                     isSourceHdr = surfaceRequest.dynamicRange.encoding != DynamicRange.ENCODING_SDR,
129                     implMode,
130                     TransformationInfo(
131                         sourceRotation = transformInfo.rotationDegrees,
132                         cropRectLeft = transformInfo.cropRect.left,
133                         cropRectTop = transformInfo.cropRect.top,
134                         cropRectRight = transformInfo.cropRect.right,
135                         cropRectBottom = transformInfo.cropRect.bottom,
136                         shouldMirror = transformInfo.isMirroring
137                     )
138                 )
139             }
140     }
141 
142     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
<lambda>null143         LaunchedEffect(Unit) {
144             snapshotFlow { viewfinderArgs }
145                 .filterNotNull()
146                 .map { args ->
147                     if (args.isSourceHdr &&
148                         args.implementationMode == ImplementationMode.EXTERNAL
149                     ) {
150                         ActivityInfo.COLOR_MODE_HDR
151                     } else {
152                         ActivityInfo.COLOR_MODE_DEFAULT
153                     }
154                 }.distinctUntilChanged()
155                 .onEach { currentOnRequestWindowColorMode(it) }
156                 .onCompletion { currentOnRequestWindowColorMode(ActivityInfo.COLOR_MODE_DEFAULT) }
157                 .collect()
158         }
159     }
160 
161     val coordinateTransformer = MutableCoordinateTransformer()
162 
argsnull163     viewfinderArgs?.let { args ->
164         Viewfinder(
165             surfaceRequest = args.viewfinderSurfaceRequest,
166             implementationMode = args.implementationMode,
167             transformationInfo = args.transformationInfo,
168             modifier = modifier.fillMaxSize().pointerInput(Unit) {
169                 detectTapGestures {
170                     with(coordinateTransformer) {
171                         val tapOffset = it.transform()
172                         Log.d(TAG, "onTap: $tapOffset")
173                         onTap(tapOffset.x, tapOffset.y)
174                     }
175                 }
176             },
177             coordinateTransformer = coordinateTransformer
178         )
179     }
180 }
181 
182 private data class ViewfinderArgs(
183     val viewfinderSurfaceRequest: ViewfinderSurfaceRequest,
184     val isSourceHdr: Boolean,
185     val implementationMode: ImplementationMode,
186     val transformationInfo: TransformationInfo
187 )
188