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