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 androidx.annotation.NonNull;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.core.CameraControl.OperationCanceledException;
import androidx.camera.core.Logger;
import androidx.camera.core.TorchState;
import androidx.camera.core.impl.annotation.ExecutedBy;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Executor;
/**
* Implementation of torch control used within {@link Camera2CameraControlImpl}.
*
* It is used to control the flash torch of camera device that {@link Camera2CameraControlImpl}
* 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";
static final int DEFAULT_TORCH_STATE = TorchState.OFF;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
private final Camera2CameraControlImpl mCamera2CameraControlImpl;
private final MutableLiveData<Integer> mTorchState;
private final boolean mHasFlashUnit;
@CameraExecutor
private final Executor mExecutor;
private boolean mIsActive;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
boolean mTargetTorchEnabled;
/**
* Constructs a TorchControl.
*
* @param camera2CameraControlImpl the camera control this TorchControl belongs.
* @param cameraCharacteristics the characteristics for the camera being controlled.
* @param executor the camera executor used to run camera task.
*/
TorchControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl,
@NonNull CameraCharacteristicsCompat cameraCharacteristics,
@CameraExecutor @NonNull Executor executor) {
mCamera2CameraControlImpl = camera2CameraControlImpl;
mExecutor = executor;
Boolean hasFlashUnit =
cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
mCamera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
}
/**
* Set current active state. Set active if it is ready to do torch operations.
*
* @param isActive true to activate or false otherwise.
*/
@ExecutedBy("mExecutor")
void setActive(boolean isActive) {
if (mIsActive == isActive) {
return;
}
mIsActive = isActive;
if (!isActive) {
if (mTargetTorchEnabled) {
mTargetTorchEnabled = false;
mCamera2CameraControlImpl.enableTorchInternal(false);
setLiveDataValue(mTorchState, TorchState.OFF);
}
if (mEnableTorchCompleter != null) {
mEnableTorchCompleter.setException(
new OperationCanceledException("Camera is not active."));
mEnableTorchCompleter = null;
}
}
}
/**
* 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) {
Logger.d(TAG, "Unable to enableTorch due to there is no flash unit.");
return Futures.immediateFailedFuture(new IllegalStateException("No flash unit"));
}
setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
return CallbackToFutureAdapter.getFuture(completer -> {
mExecutor.execute(
() -> enableTorchInternal(completer, enabled));
return "enableTorch: " + enabled;
});
}
/**
* 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;
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mExecutor")
void enableTorchInternal(@NonNull Completer<Void> completer, boolean enabled) {
if (!mIsActive) {
setLiveDataValue(mTorchState, TorchState.OFF);
completer.setException(new OperationCanceledException("Camera is not active."));
return;
}
mTargetTorchEnabled = enabled;
mCamera2CameraControlImpl.enableTorchInternal(enabled);
setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
if (mEnableTorchCompleter != null) {
mEnableTorchCompleter.setException(new OperationCanceledException(
"There is a new enableTorch being set"));
}
mEnableTorchCompleter = completer;
}
private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
if (Threads.isMainThread()) {
liveData.setValue(value);
} else {
liveData.postValue(value);
}
}
private final Camera2CameraControlImpl.CaptureResultListener mCaptureResultListener =
new Camera2CameraControlImpl.CaptureResultListener() {
@ExecutedBy("mExecutor")
@Override
public boolean onCaptureResult(@NonNull TotalCaptureResult captureResult) {
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) {
mEnableTorchCompleter.set(null);
mEnableTorchCompleter = null;
}
}
// Return false to keep getting captureResult.
return false;
}
};
}