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