1 /* <lambda>null2 * Copyright 2025 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.camera2.pipe.integration.impl 18 19 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY 20 import android.hardware.camera2.CameraMetadata.CONTROL_LOW_LIGHT_BOOST_STATE_ACTIVE 21 import android.hardware.camera2.CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE 22 import android.os.Build 23 import androidx.camera.camera2.pipe.CameraMetadata 24 import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsLowLightBoost 25 import androidx.camera.camera2.pipe.FrameInfo 26 import androidx.camera.camera2.pipe.FrameNumber 27 import androidx.camera.camera2.pipe.Request 28 import androidx.camera.camera2.pipe.RequestMetadata 29 import androidx.camera.camera2.pipe.core.Log.debug 30 import androidx.camera.camera2.pipe.integration.adapter.propagateTo 31 import androidx.camera.camera2.pipe.integration.config.CameraScope 32 import androidx.camera.core.CameraControl 33 import androidx.camera.core.LowLightBoostState 34 import androidx.camera.core.impl.CameraControlInternal 35 import androidx.camera.core.impl.utils.Threads 36 import androidx.lifecycle.LiveData 37 import androidx.lifecycle.MutableLiveData 38 import dagger.Binds 39 import dagger.Module 40 import dagger.multibindings.IntoSet 41 import java.util.concurrent.atomic.AtomicInteger 42 import javax.inject.Inject 43 import kotlinx.coroutines.CompletableDeferred 44 import kotlinx.coroutines.Deferred 45 46 /** Implementation of LowLightBoost control exposed by [CameraControlInternal]. */ 47 @CameraScope 48 public class LowLightBoostControl 49 @Inject 50 constructor( 51 private val cameraMetadata: CameraMetadata?, 52 private val state3AControl: State3AControl, 53 private val threads: UseCaseThreads, 54 private val comboRequestListener: ComboRequestListener, 55 ) : UseCaseCameraControl { 56 57 private var _requestControl: UseCaseCameraRequestControl? = null 58 override var requestControl: UseCaseCameraRequestControl? 59 get() = _requestControl 60 set(value) { 61 _requestControl = value 62 63 if (isLowLightBoostOn) { 64 if (value != null) { 65 setLowLightBoostAsync(lowLightBoost = true, cancelPreviousTask = false) 66 } else { 67 // Updates the state to be INACTIVE when the LLB is ON but the control becomes 68 // inactive. 69 _lowLightBoostState.setLiveDataValue(LowLightBoostState.INACTIVE) 70 } 71 } 72 } 73 74 override fun reset() { 75 stopRunningTaskInternal() 76 setLowLightBoostAsync(false) 77 } 78 79 private val isLowLightBoostSupported: Boolean = cameraMetadata?.supportsLowLightBoost == true 80 81 private var isLowLightBoostOn = false 82 83 private val _lowLightBoostState = MutableLiveData(LowLightBoostState.OFF) 84 public val lowLightBoostStateLiveData: LiveData<Int> 85 get() = _lowLightBoostState 86 87 private val lowLightBoostStateAtomic = AtomicInteger(LowLightBoostState.OFF) 88 89 private var _updateSignal: CompletableDeferred<Unit>? = null 90 91 init { 92 /** Sets the state update listener when low-light boost is supported */ 93 if (isLowLightBoostSupported) { 94 object : Request.Listener { 95 override fun onTotalCaptureResult( 96 requestMetadata: RequestMetadata, 97 frameNumber: FrameNumber, 98 totalCaptureResult: FrameInfo 99 ) { 100 if ( 101 Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && 102 _requestControl != null 103 ) { 104 // Updates the state to the LLB state live data 105 if (isLowLightBoostOn) { 106 totalCaptureResult.metadata[CONTROL_LOW_LIGHT_BOOST_STATE]?.let { 107 _lowLightBoostState.setLiveDataValue( 108 when (it) { 109 CONTROL_LOW_LIGHT_BOOST_STATE_ACTIVE -> 110 LowLightBoostState.ACTIVE 111 else -> LowLightBoostState.INACTIVE 112 } 113 ) 114 } 115 } 116 } 117 } 118 } 119 .let { comboRequestListener.addListener(it, threads.sequentialExecutor) } 120 } 121 } 122 123 private var isLowLightBoostDisabledByUseCaseSessionConfig = false 124 125 /** 126 * Turn the Low Light Boost on or off. 127 * 128 * @param lowLightBoost Whether the low-light boost should be on or off. 129 * @param cancelPreviousTask Whether to cancel the previous task if it's running. 130 */ 131 public fun setLowLightBoostAsync( 132 lowLightBoost: Boolean, 133 cancelPreviousTask: Boolean = true 134 ): Deferred<Unit> { 135 debug { "LowLightBoostControl#setLowLightBoostAsync: lowLightBoost = $lowLightBoost" } 136 137 val signal = CompletableDeferred<Unit>() 138 139 if (!isLowLightBoostSupported) { 140 return signal.createFailureResult( 141 IllegalStateException("Low Light Boost is not supported!") 142 ) 143 } 144 145 if (isLowLightBoostDisabledByUseCaseSessionConfig) { 146 _lowLightBoostState.setLiveDataValue(LowLightBoostState.OFF) 147 return signal.createFailureResult( 148 IllegalStateException( 149 "Low Light Boost is disabled" + 150 " when expected frame rate range exceeds 30 or HDR 10-bit is on." 151 ) 152 ) 153 } 154 155 isLowLightBoostOn = lowLightBoost 156 157 if (!lowLightBoost) { 158 _lowLightBoostState.setLiveDataValue(LowLightBoostState.OFF) 159 } 160 161 requestControl?.let { 162 if (lowLightBoost) { 163 _lowLightBoostState.setLiveDataValue(LowLightBoostState.INACTIVE) 164 } 165 166 if (cancelPreviousTask) { 167 stopRunningTaskInternal() 168 } else { 169 // Propagate the result to the previous updateSignal 170 _updateSignal?.let { previousUpdateSignal -> 171 signal.propagateTo(previousUpdateSignal) 172 } 173 } 174 175 _updateSignal = signal 176 177 // Hold the internal AE mode to ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY while the 178 // low-light boost is turned ON. If low-light boost is OFF, a value of null will make 179 // the state3AControl calculate the correct AE mode based on other settings. 180 state3AControl.preferredAeMode = 181 if (lowLightBoost) CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY else null 182 183 state3AControl.updateSignal?.propagateTo(signal) ?: run { signal.complete(Unit) } 184 185 signal.invokeOnCompletion { 186 if (signal == _updateSignal) { 187 _updateSignal = null 188 } 189 } 190 } 191 ?: run { 192 signal.createFailureResult( 193 CameraControl.OperationCanceledException("Camera is not active.") 194 ) 195 } 196 197 return signal 198 } 199 200 public fun setLowLightBoostDisabledByUseCaseSessionConfig(disabled: Boolean) { 201 isLowLightBoostDisabledByUseCaseSessionConfig = disabled 202 } 203 204 public fun isLowLightBoostDisabledByUseCaseSessionConfig(): Boolean = 205 isLowLightBoostDisabledByUseCaseSessionConfig 206 207 private fun stopRunningTaskInternal() { 208 _updateSignal?.createFailureResult( 209 CameraControl.OperationCanceledException("There is a new enableLowLightBoost being set") 210 ) 211 _updateSignal = null 212 } 213 214 private fun CompletableDeferred<Unit>.createFailureResult(exception: Exception) = apply { 215 completeExceptionally(exception) 216 } 217 218 private fun MutableLiveData<Int>.setLiveDataValue(@LowLightBoostState.State state: Int) { 219 if (lowLightBoostStateAtomic.getAndSet(state) != state) { 220 if (Threads.isMainThread()) { 221 this.value = state 222 } else { 223 this.postValue(state) 224 } 225 } 226 } 227 228 @Module 229 public abstract class Bindings { 230 @Binds 231 @IntoSet 232 public abstract fun provideControls( 233 lowLightBoostControl: LowLightBoostControl 234 ): UseCaseCameraControl 235 } 236 } 237