TorchControl.java
/*
* Copyright 2019 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.internal;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraControl.OperationCanceledException;
import androidx.camera.core.TorchState;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.google.common.util.concurrent.ListenableFuture;
/**
* Implementation of torch control used within {@link Camera2CameraControl}.
*
* It is used to control the flash torch of camera device that {@link Camera2CameraControl}
* operates. The torch control must be activated via {@link #setActive(boolean)} when the
* camera device is ready to do torch operations and be deactivated when the camera device is
* closing or closed.
*/
final class TorchControl {
private static final String TAG = "TorchControl";
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mEnableTorchLock = new Object();
private final Object mActiveLock = new Object();
private final Camera2CameraControl mCamera2CameraControl;
private final MutableLiveData<Integer> mTorchState;
private final boolean mHasFlashUnit;
@GuardedBy("mActiveLock")
private boolean mIsActive;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mEnableTorchLock")
CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mEnableTorchLock")
boolean mTargetTorchEnabled;
/**
* Constructs a TorchControl.
*
* @param camera2CameraControl the camera control this TorchControl belongs.
* @param cameraCharacteristics the characteristics for the camera being controlled.
*/
TorchControl(@NonNull Camera2CameraControl camera2CameraControl,
@NonNull CameraCharacteristics cameraCharacteristics) {
mCamera2CameraControl = camera2CameraControl;
Boolean hasFlashUnit =
cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
mTorchState = new MutableLiveData<>(TorchState.OFF);
mCamera2CameraControl.addCaptureResultListener(mCaptureResultListener);
}
/**
* Set current active state. Set active if it is ready to do torch operations.
*
* @param isActive true to activate or false otherwise.
*/
void setActive(boolean isActive) {
synchronized (mActiveLock) {
if (mIsActive == isActive) {
return;
}
mIsActive = isActive;
boolean shouldResetDefault = false;
CallbackToFutureAdapter.Completer<Void> completerToCancel = null;
synchronized (mEnableTorchLock) {
if (!isActive) {
if (mEnableTorchCompleter != null) {
completerToCancel = mEnableTorchCompleter;
mEnableTorchCompleter = null;
}
if (mTargetTorchEnabled) {
shouldResetDefault = true;
mTargetTorchEnabled = false;
mCamera2CameraControl.enableTorchInternal(false);
}
}
}
if (shouldResetDefault) {
setLiveDataValue(mTorchState, TorchState.OFF);
}
if (completerToCancel != null) {
completerToCancel.setException(new OperationCanceledException(
"Camera is not active."));
}
}
}
/**
* Enable the torch or disable the torch.
*
* <p>The returned {@link ListenableFuture} will succeed when the request is sent to camera
* device. But it may get an {@link OperationCanceledException} result when:
* <ol>
* <li>There are multiple {@link #enableTorch(boolean)} requests in the same time, the older
* and incomplete futures will get cancelled.
* <li>When the TorchControl is set to inactive.
* </ol>
*
* <p>The returned {@link ListenableFuture} will fail immediately when:
* <ol>
* <li>The TorchControl is not in active state.
* <li>The camera doesn't have a flash unit. (see
* {@link CameraCharacteristics#FLASH_INFO_AVAILABLE})
* </ol>
*
* @param enabled true to open the torch, false to close it.
* @return A {@link ListenableFuture} which is successful when the torch was changed to the
* value specified. It fails when it is unable to change the torch state.
*/
ListenableFuture<Void> enableTorch(boolean enabled) {
if (!mHasFlashUnit) {
Log.d(TAG, "Unable to enableTorch due to there is no flash unit.");
return Futures.immediateFailedFuture(new IllegalStateException("No flash unit"));
}
synchronized (mActiveLock) {
if (!mIsActive) {
return Futures.immediateFailedFuture(new OperationCanceledException(
"Camera is not active."));
}
ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(
completer -> {
CallbackToFutureAdapter.Completer<Void> completerToCancel = null;
synchronized (mEnableTorchLock) {
if (mEnableTorchCompleter != null) {
completerToCancel = mEnableTorchCompleter;
}
mEnableTorchCompleter = completer;
mTargetTorchEnabled = enabled;
mCamera2CameraControl.enableTorchInternal(enabled);
}
setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
if (completerToCancel != null) {
completerToCancel.setException(new OperationCanceledException(
"There is a new enableTorch being set"));
}
return "enableTorch: " + enabled;
});
return future;
}
}
/**
* Returns a {@link LiveData} of current {@link TorchState}.
*
* <p>The torch state can be enabled or disabled via {@link #enableTorch(boolean)} which will
* trigger the change event to the returned {@link LiveData}.
*
* @return a {@link LiveData} containing current torch state.
*/
@NonNull
LiveData<Integer> getTorchState() {
return mTorchState;
}
private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
if (Threads.isMainThread()) {
liveData.setValue(value);
} else {
liveData.postValue(value);
}
}
private final Camera2CameraControl.CaptureResultListener mCaptureResultListener =
new Camera2CameraControl.CaptureResultListener() {
@Override
public boolean onCaptureResult(@NonNull TotalCaptureResult captureResult) {
CallbackToFutureAdapter.Completer<Void> completerToSet = null;
synchronized (mEnableTorchLock) {
if (mEnableTorchCompleter != null) {
CaptureRequest captureRequest = captureResult.getRequest();
Integer flashMode = captureRequest.get(CaptureRequest.FLASH_MODE);
boolean torchEnabled =
flashMode != null && flashMode == CaptureRequest.FLASH_MODE_TORCH;
if (torchEnabled == mTargetTorchEnabled) {
completerToSet = mEnableTorchCompleter;
mEnableTorchCompleter = null;
}
}
}
if (completerToSet != null) {
completerToSet.set(null);
}
// Return false to keep getting captureResult.
return false;
}
};
}