blob: 833a10f80681ca17d7a679386540f4eea509c901 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.camera2.pipe.integration.impl
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.integration.adapter.ZoomValue
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.internal.ZoomMath.getLinearZoomFromZoomRatio
import androidx.camera.camera2.pipe.integration.internal.ZoomMath.getZoomRatioFromLinearZoom
import androidx.camera.core.CameraControl
import androidx.camera.core.ZoomState
import androidx.camera.core.impl.utils.futures.Futures
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.common.util.concurrent.ListenableFuture
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
const val DEFAULT_ZOOM_RATIO = 1.0f
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@CameraScope
class ZoomControl @Inject constructor(
private val threads: UseCaseThreads,
private val zoomCompat: ZoomCompat,
) : UseCaseCameraControl {
// NOTE: minZoom may be lower than 1.0
// NOTE: Default zoom ratio is 1.0 (DEFAULT_ZOOM_RATIO)
val minZoomRatio: Float = zoomCompat.minZoomRatio
val maxZoomRatio: Float = zoomCompat.maxZoomRatio
val defaultZoomState by lazy {
ZoomValue(DEFAULT_ZOOM_RATIO, minZoomRatio, maxZoomRatio)
}
private val _zoomState by lazy {
MutableLiveData<ZoomState>(defaultZoomState)
}
val zoomStateLiveData: LiveData<ZoomState>
get() = _zoomState
/** Linear zoom is between 0.0f and 1.0f */
fun toLinearZoom(zoomRatio: Float) = getLinearZoomFromZoomRatio(
zoomRatio = zoomRatio,
minZoomRatio = minZoomRatio,
maxZoomRatio = maxZoomRatio
)
/** Zoom ratio is commonly used as the "1x, 2x, 5x" zoom ratio. Zoom ratio may be less than 1 */
private fun toZoomRatio(linearZoom: Float) = getZoomRatioFromLinearZoom(
linearZoom = linearZoom,
minZoomRatio = minZoomRatio,
maxZoomRatio = maxZoomRatio
)
private var _useCaseCamera: UseCaseCamera? = null
override var useCaseCamera: UseCaseCamera?
get() = _useCaseCamera
set(value) {
_useCaseCamera = value
applyZoomState(_zoomState.value ?: defaultZoomState, false)
}
private var updateSignal: CompletableDeferred<Unit>? = null
override fun reset() {
// TODO: 1.0 may not be a reasonable value to reset the zoom state to.
applyZoomState(defaultZoomState)
}
private suspend fun setZoomState(value: ZoomState) {
// TODO: camera-camera2 updates livedata with setValue if calling thread is main thread,
// and updates with postValue otherwise. Need to consider if always using setValue
// via main thread is alright in camera-pipe.
withContext(Dispatchers.Main) {
_zoomState.value = value
}
}
fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
if (linearZoom > 1.0f || linearZoom < 0f) {
val outOfRangeDesc =
"Requested linearZoom $linearZoom is not within valid range [0, 1]"
return Futures.immediateFailedFuture(
IllegalArgumentException(outOfRangeDesc)
)
}
val zoomValue = ZoomValue(
ZoomValue.LinearZoom(linearZoom),
minZoomRatio,
maxZoomRatio,
)
return applyZoomState(zoomValue)
}
fun setZoomRatio(zoomRatio: Float): ListenableFuture<Void> {
if (zoomRatio > maxZoomRatio || zoomRatio < minZoomRatio) {
val outOfRangeDesc =
"Requested zoomRatio $zoomRatio is not within valid range" +
" [$minZoomRatio, $maxZoomRatio]"
return Futures.immediateFailedFuture(
IllegalArgumentException(outOfRangeDesc)
)
}
val zoomValue = ZoomValue(
zoomRatio,
minZoomRatio,
maxZoomRatio,
)
return applyZoomState(zoomValue)
}
fun applyZoomState(
zoomState: ZoomState,
cancelPreviousTask: Boolean = true,
): ListenableFuture<Void> {
val signal = CompletableDeferred<Unit>()
updateSignal?.let { previousUpdateSignal ->
if (cancelPreviousTask) {
// Cancel the previous request signal if exist.
previousUpdateSignal.completeExceptionally(
CameraControl.OperationCanceledException(
"Cancelled due to another zoom value being set."
)
)
} else {
// Propagate the result to the previous updateSignal
signal.propagateTo(previousUpdateSignal)
}
}
updateSignal = signal
threads.sequentialScope.launch(start = CoroutineStart.UNDISPATCHED) {
setZoomState(zoomState)
useCaseCamera?.let {
zoomCompat.applyAsync(zoomState.zoomRatio, it).propagateTo(signal)
} ?: signal.completeExceptionally(
CameraControl.OperationCanceledException("Camera is not active.")
)
}
/**
* TODO: Use signal.asListenableFuture() directly.
* Deferred<T>.asListenableFuture() returns a ListenableFuture<T>, so this currently reports
* a type mismatch error (Required: Void!, Found: Unit).
* Currently, Job.asListenableFuture() is used as a workaround for this problem.
*/
return Futures.nonCancellationPropagating((signal as Job).asListenableFuture())
}
@Module
abstract class Bindings {
@Binds
@IntoSet
abstract fun provideControls(zoomControl: ZoomControl): UseCaseCameraControl
}
}