[go: nahoru, domu]

blob: e15c9e065f25babad1d237016686cf6d5f929b6f [file] [log] [blame]
/*
* Copyright (C) 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.core;
import static androidx.camera.core.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
import static androidx.camera.core.ImageCaptureConfig.OPTION_CAMERA_ID_FILTER;
import static androidx.camera.core.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
import static androidx.camera.core.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR;
import static androidx.camera.core.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.ImageCaptureConfig.OPTION_FLASH_MODE;
import static androidx.camera.core.ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE;
import static androidx.camera.core.ImageCaptureConfig.OPTION_IO_EXECUTOR;
import static androidx.camera.core.ImageCaptureConfig.OPTION_LENS_FACING;
import static androidx.camera.core.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
import static androidx.camera.core.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.ImageCaptureConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO_CUSTOM;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_CLASS;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_NAME;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_RESOLUTION;
import static androidx.camera.core.ImageCaptureConfig.OPTION_TARGET_ROTATION;
import static androidx.camera.core.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.location.Location;
import android.media.Image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
import android.view.Display;
import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureFailure;
import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
import androidx.camera.core.impl.CameraCaptureMetaData.AfMode;
import androidx.camera.core.impl.CameraCaptureMetaData.AfState;
import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraCaptureResult.EmptyCameraCaptureResult;
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CameraDeviceConfig;
import androidx.camera.core.impl.CameraIdFilter;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.core.internal.TargetConfig;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A use case for taking a picture.
*
* <p>This class is designed for basic picture taking. It provides takePicture() functions to take
* a picture to memory or save to a file, and provides image metadata. Pictures are taken in
* automatic mode after focus has converged. The flash mode can additionally be set by the
* application.
*
* <p>TakePicture returns immediately and a listener is called to provide the results after the
* capture completes. Multiple calls to takePicture will take pictures sequentially starting
* after the previous picture is captured.
*
* <p>Note that focus and exposure metering regions can be controlled via {@link Preview}.
*
* <p>When capturing to memory, the captured image is made available through an {@link ImageProxy}
* via an {@link ImageCapture.OnImageCapturedCallback}.
*/
@SuppressWarnings("ClassCanBeStatic") // TODO(b/141958189): Suppressed during upgrade to AGP 3.6.
public class ImageCapture extends UseCase {
/**
* An unknown error occurred.
*
* <p>See message parameter in onError callback or log for more details.
*/
public static final int ERROR_UNKNOWN = 0;
/**
* An error occurred while attempting to read or write a file, such as when saving an image
* to a File.
*/
public static final int ERROR_FILE_IO = 1;
/**
* An error reported by camera framework indicating the capture request is failed.
*/
public static final int ERROR_CAPTURE_FAILED = 2;
/**
* An error indicating the request cannot be done due to camera is closed.
*/
public static final int ERROR_CAMERA_CLOSED = 3;
/**
* An error indicating this ImageCapture is not bound to a valid camera.
*/
public static final int ERROR_INVALID_CAMERA = 4;
/**
* Optimizes capture pipeline to prioritize image quality over latency. When the capture
* mode is set to MAX_QUALITY, images may take longer to capture.
*/
public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0;
/**
* Optimizes capture pipeline to prioritize latency over image quality. When the capture
* mode is set to MIN_LATENCY, images may capture faster but the image quality may be
* reduced.
*/
public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1;
/**
* Auto flash. The flash will be used according to the camera system's determination when taking
* a picture.
*/
public static final int FLASH_MODE_AUTO = 0;
/** Always flash. The flash will always be used when taking a picture. */
public static final int FLASH_MODE_ON = 1;
/** No flash. The flash will never be used when taking a picture. */
public static final int FLASH_MODE_OFF = 2;
/**
* Provides a static configuration with implementation-agnostic options.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final Defaults DEFAULT_CONFIG = new Defaults();
private static final String TAG = "ImageCapture";
private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
private static final int MAX_IMAGES = 2;
// Empty metadata object used as a placeholder for no user-supplied metadata.
// Should be initialized to all default values.
private static final Metadata EMPTY_METADATA = new Metadata();
@Nullable
private HandlerThread mProcessingImageResultThread;
@Nullable
private Handler mProcessingImageResultHandler;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Deque<ImageCaptureRequest> mImageCaptureRequests = new ConcurrentLinkedDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder mSessionConfigBuilder;
private final CaptureConfig mCaptureConfig;
private final ExecutorService mExecutor =
Executors.newFixedThreadPool(
1,
new ThreadFactory() {
private final AtomicInteger mId = new AtomicInteger(0);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(
r,
CameraXThreads.TAG + "image_capture_" + mId.getAndIncrement());
}
});
@NonNull
final Executor mIoExecutor;
private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
@CaptureMode
private final int mCaptureMode;
/** The set of requests that will be sent to the camera for the final captured image. */
private final CaptureBundle mCaptureBundle;
private final int mMaxCaptureStages;
/**
* Processing that gets done to the mCaptureBundle to produce the final image that is produced
* by {@link #takePicture(Executor, OnImageCapturedCallback)}
*/
private final CaptureProcessor mCaptureProcessor;
private final Builder mUseCaseConfigBuilder;
/** synthetic accessor */
@SuppressWarnings("WeakerAccess")
ImageReaderProxy mImageReader;
/** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
private ImageCaptureConfig mConfig;
private DeferrableSurface mDeferrableSurface;
/**
* A flag to check 3A converged or not.
*
* <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
* the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
* minimum latency mode.
*/
private boolean mEnableCheck3AConverged;
/** Current flash mode. */
@FlashMode
private int mFlashMode;
/**
* Creates a new image capture use case from the given configuration.
*
* @param userConfig for this use case instance
* @throws IllegalArgumentException if the configuration is invalid.
*/
ImageCapture(@NonNull ImageCaptureConfig userConfig) {
super(userConfig);
mUseCaseConfigBuilder = Builder.fromConfig(userConfig);
// Ensure we're using the combined configuration (user config + defaults)
mConfig = (ImageCaptureConfig) getUseCaseConfig();
mCaptureMode = mConfig.getCaptureMode();
mFlashMode = mConfig.getFlashMode();
mCaptureProcessor = mConfig.getCaptureProcessor(null);
mMaxCaptureStages = mConfig.getMaxCaptureStages(MAX_IMAGES);
if (mMaxCaptureStages < 1) {
throw new IllegalArgumentException(
"Maximum outstanding image count must be at least 1");
}
Integer bufferFormat = mConfig.getBufferFormat(null);
if (bufferFormat != null) {
if (mCaptureProcessor != null) {
throw new IllegalArgumentException(
"Cannot set buffer format with CaptureProcessor defined.");
} else {
setImageFormat(bufferFormat);
}
} else {
if (mCaptureProcessor != null) {
setImageFormat(ImageFormat.YUV_420_888);
} else {
setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageCaptureFormat());
}
}
mCaptureBundle = mConfig.getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
mIoExecutor = mConfig.getIoExecutor(CameraXExecutors.ioExecutor());
if (mCaptureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
} else if (mCaptureMode == CAPTURE_MODE_MINIMIZE_LATENCY) {
mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
}
CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(mConfig);
mCaptureConfig = captureBuilder.build();
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder createPipeline(@NonNull String cameraId,
@NonNull ImageCaptureConfig config, @NonNull Size resolution) {
Threads.checkMainThread();
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
sessionConfigBuilder.addRepeatingCameraCaptureCallback(mSessionCallbackChecker);
mProcessingImageResultThread = new HandlerThread("OnImageAvailableHandlerThread");
mProcessingImageResultThread.start();
mProcessingImageResultHandler = new Handler(mProcessingImageResultThread.getLooper());
// Setup the ImageReader to do processing
if (mCaptureProcessor != null) {
// TODO: To allow user to use an Executor for the image processing.
ProcessingImageReader processingImageReader =
new ProcessingImageReader(
resolution.getWidth(),
resolution.getHeight(),
getImageFormat(), mMaxCaptureStages,
mProcessingImageResultHandler,
getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
mCaptureProcessor);
mMetadataMatchingCaptureCallback = processingImageReader.getCameraCaptureCallback();
mImageReader = processingImageReader;
} else {
MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES,
mProcessingImageResultHandler);
mMetadataMatchingCaptureCallback = metadataImageReader.getCameraCaptureCallback();
mImageReader = metadataImageReader;
}
mImageReader.setOnImageAvailableListener(
new ImageReaderProxy.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReaderProxy imageReader) {
ImageProxy image = null;
try {
image = imageReader.acquireLatestImage();
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to acquire latest image.", e);
} finally {
if (image != null) {
// Call the head request listener to process the captured image.
ImageCaptureRequest imageCaptureRequest;
if ((imageCaptureRequest = mImageCaptureRequests.peek()) != null) {
SingleCloseImageProxy wrappedImage = new SingleCloseImageProxy(
image);
wrappedImage.addOnImageCloseListener(mOnImageCloseListener);
imageCaptureRequest.dispatchImage(wrappedImage);
} else {
// Discard the image if we have no requests.
image.close();
}
}
}
}
},
mProcessingImageResultHandler);
mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
@Override
public void onError(@NonNull SessionConfig sessionConfig,
@NonNull SessionConfig.SessionError error) {
clearPipeline();
// Ensure the bound camera has not changed before resetting.
// TODO(b/143915543): Ensure this never gets called by a camera that is not bound
// to this use case so we don't need to do this check.
if (isCurrentlyBoundCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
attachToCamera(cameraId, mSessionConfigBuilder.build());
notifyReset();
}
}
});
return sessionConfigBuilder;
}
/**
* Clear the internal pipeline so that the pipeline can be set up again.
*/
void clearPipeline() {
Threads.checkMainThread();
DeferrableSurface deferrableSurface = mDeferrableSurface;
mDeferrableSurface = null;
ImageReaderProxy imageReaderProxy = mImageReader;
mImageReader = null;
HandlerThread handlerThread = mProcessingImageResultThread;
if (deferrableSurface != null) {
deferrableSurface.setOnSurfaceDetachedListener(
CameraXExecutors.mainThreadExecutor(),
new DeferrableSurface.OnSurfaceDetachedListener() {
@Override
public void onSurfaceDetached() {
if (imageReaderProxy != null) {
imageReaderProxy.close();
}
// Close the handlerThread after the ImageReader was closed.
handlerThread.quitSafely();
}
});
}
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@Nullable
@RestrictTo(Scope.LIBRARY_GROUP)
protected UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder(@Nullable Integer lensFacing) {
ImageCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
ImageCaptureConfig.class, lensFacing);
if (defaults != null) {
return Builder.fromConfig(defaults);
}
return null;
}
private CameraControlInternal getCurrentCameraControl() {
String cameraId = getBoundCameraId();
return getCameraControl(cameraId);
}
/**
* Configures flash mode to CameraControlInternal once it is ready.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
protected void onCameraControlReady(@NonNull String cameraId) {
getCameraControl(cameraId).setFlashMode(mFlashMode);
}
/**
* Get the flash mode.
*
* @return the flashMode.
*/
@FlashMode
public int getFlashMode() {
return mFlashMode;
}
/**
* Set the flash mode.
*
* <p>The flash control for the subsequent photo capture requests. Applications can check if
* there is a flash unit via {@link CameraInfo#hasFlashUnit()} and update UI component if
* necessary. If there is no flash unit, then calling this API will take no effect for the
* subsequent photo capture requests and they will act like {@link #FLASH_MODE_OFF}.
*
* <p>When the torch is enabled via {@link CameraControl#enableTorch(boolean)}, the torch
* will remain enabled during photo capture regardless of flashMode setting. When
* the torch is disabled, flash will function as specified by {@link #setFlashMode(int)}.
*
* @param flashMode the flash mode.
*/
public void setFlashMode(@FlashMode int flashMode) {
this.mFlashMode = flashMode;
// The camera control will be ready after the use case is attached. The {@link
// CameraDeviceConfig} containing camera id info is also generated at meanwhile. Developers
// may update flash mode before the use case is bound. If the camera control has been
// ready, directly updating the flash mode into camera control. If the camera control has
// been not ready yet, just saving the flash mode and updating into camera control when
// camera control ready callback is called.
if (getBoundCamera() != null) {
getCurrentCameraControl().setFlashMode(flashMode);
}
}
/**
* Sets target aspect ratio.
*
* <p>This sets the cropping rectangle returned by {@link ImageProxy#getCropRect()} returned
* from {@link ImageCapture#takePicture(Executor, OnImageCapturedCallback)}.
*
* <p>This crops the saved image when calling
* {@link ImageCapture#takePicture(File, Executor, OnImageSavedCallback)} or
* {@link ImageCapture#takePicture(File, Metadata, Executor, OnImageSavedCallback)}.
*
* <p>Cropping occurs around the center of the image and as though it were in the target
* rotation.
*
* @param aspectRatio New target aspect ratio.
*/
public void setTargetAspectRatioCustom(@NonNull Rational aspectRatio) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
Rational oldRatio = oldConfig.getTargetAspectRatioCustom(null);
if (!aspectRatio.equals(oldRatio)) {
mUseCaseConfigBuilder.setTargetAspectRatioCustom(aspectRatio);
updateUseCaseConfig(mUseCaseConfigBuilder.getUseCaseConfig());
mConfig = (ImageCaptureConfig) getUseCaseConfig();
// TODO(b/122846516): Reconfigure capture session if the ratio is changed drastically.
}
}
/**
* Sets the desired rotation of the output image.
*
* <p>This will affect the EXIF rotation metadata in images saved by takePicture calls and the
* {@link ImageInfo#getRotationDegrees()} value of the {@link ImageProxy} returned by
* {@link OnImageCapturedCallback}. These will be set to be the rotation, which if applied to
* the output image data, will make the image match target rotation specified here.
*
* <p>While rotation can also be set via {@link Builder#setTargetRotation(int)}, using
* {@link ImageCapture#setTargetRotation(int)} allows the target rotation to be set dynamically.
*
* <p>In general, it is best to use an {@link android.view.OrientationEventListener} to
* set the target rotation. This way, the rotation output will indicate which way is down for
* a given image. This is important since display orientation may be locked by device
* default, user setting, or app configuration, and some devices may not transition to a
* reverse-portrait display orientation. In these cases,
* use {@link ImageCapture#setTargetRotation} to set target rotation dynamically according to
* the {@link android.view.OrientationEventListener}, without re-creating the use case. Note
* the OrientationEventListener output of degrees in the range [0..359] should be converted to
* a surface rotation, i.e. one of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*
* <p>If no target rotation is set by the application, it is set to the value of
* {@link Display#getRotation()} of the default display at the time the use case is created.
*
* <p>takePicture uses the target rotation at the time it begins executing (which may be delayed
* waiting on a previous takePicture call to complete).
*
* @param rotation Target rotation of the output image, expressed as one of
* {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
public void setTargetRotation(@RotationValue int rotation) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
mUseCaseConfigBuilder.setTargetRotation(rotation);
updateUseCaseConfig(mUseCaseConfigBuilder.build().getUseCaseConfig());
mConfig = (ImageCaptureConfig) getUseCaseConfig();
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
}
}
/**
* Captures a new still image for in memory access.
*
* <p>The callback will be called only once for every invocation of this method. The listener
* is responsible for calling {@link Image#close()} on the returned image.
*
* @param executor The executor in which the callback methods will be run.
* @param callback Callback to be invoked for the newly captured image
*/
@SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
public void takePicture(@NonNull Executor executor,
final @NonNull OnImageCapturedCallback callback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(() -> takePicture(executor, callback));
return;
}
sendImageCaptureRequest(executor, callback);
}
/**
* Captures a new still image and saves to a file.
*
* <p>The listener's callback will be called only once for every invocation of this method.
*
* @param saveLocation Location to store the newly captured image.
* @param executor The executor in which the listener callback methods will be run.
* @param imageSavedCallback Callback to be called for the newly captured image.
*/
// Maybe remove after https://issuetracker.google.com/135275901
// Todo: b/145130873 - Methods accepting `File` should also accept `FileDescriptor` or streams
@SuppressLint({"LambdaLast", "StreamFiles"})
public void takePicture(@NonNull File saveLocation,
@NonNull Executor executor,
@NonNull OnImageSavedCallback imageSavedCallback) {
takePicture(saveLocation, EMPTY_METADATA, executor, imageSavedCallback);
}
/**
* Captures a new still image and saves to a file along with application specified metadata.
*
* <p>The callback will be called only once for every invocation of this method.
*
* <p>This function accepts metadata as a parameter from application code. For JPEGs, this
* metadata will be included in the EXIF.
*
* @param saveLocation Location to store the newly captured image.
* @param metadata Metadata to be stored with the saved image. For JPEG this will
* be included in the EXIF.
* @param executor The executor in which the callback methods will be run.
* @param imageSavedCallback Callback to be called for the newly captured image.
*/
// Maybe remove after https://issuetracker.google.com/135275901
// Todo: b/145130873 - Methods accepting `File` should also accept `FileDescriptor` or streams
@SuppressLint({"LambdaLast", "StreamFiles"})
public void takePicture(
final @NonNull File saveLocation,
final @NonNull Metadata metadata, @NonNull Executor executor,
final @NonNull OnImageSavedCallback imageSavedCallback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(
() -> takePicture(saveLocation, metadata, executor, imageSavedCallback));
return;
}
/*
* We need to chain the following callbacks to save the image to disk:
*
* +-----------------------+
* | |
* |ImageCapture. |
* |OnImageCapturedCallback|
* | |
* +-----------+-----------+
* |
* |
* +-----------v-----------+ +----------------------+
* | | | |
* | ImageSaver. | | ImageCapture. |
* | OnImageSavedCallback +------> OnImageSavedCallback |
* | | | |
* +-----------------------+ +----------------------+
*/
// Convert the ImageSaver.OnImageSavedCallback to ImageCapture.OnImageSavedCallback
final ImageSaver.OnImageSavedCallback imageSavedCallbackWrapper =
new ImageSaver.OnImageSavedCallback() {
@Override
public void onImageSaved(File file) {
imageSavedCallback.onImageSaved(file);
}
@Override
public void onError(ImageSaver.SaveError error, String message,
@Nullable Throwable cause) {
@ImageCaptureError int imageCaptureError = ERROR_UNKNOWN;
switch (error) {
case FILE_IO_FAILED:
imageCaptureError = ERROR_FILE_IO;
break;
default:
// Keep the imageCaptureError as UNKNOWN_ERROR
break;
}
imageSavedCallback.onError(imageCaptureError, message, cause);
}
};
// Wrap the ImageCapture.OnImageSavedCallback with an OnImageCapturedCallback so it can
// be put into the capture request queue
OnImageCapturedCallback imageCaptureCallbackWrapper =
new OnImageCapturedCallback() {
@Override
public void onCaptureSuccess(@NonNull ImageProxy image) {
mIoExecutor.execute(
new ImageSaver(
image,
saveLocation,
image.getImageInfo().getRotationDegrees(),
metadata.isReversedHorizontal(),
metadata.isReversedVertical(),
metadata.getLocation(),
executor,
imageSavedCallbackWrapper));
}
@Override
public void onError(@ImageCaptureError int error, @NonNull String message,
@Nullable Throwable cause) {
imageSavedCallback.onError(error, message, cause);
}
};
// Always use the mainThreadExecutor for the initial callback so we don't need to double
// post to another thread
sendImageCaptureRequest(CameraXExecutors.mainThreadExecutor(), imageCaptureCallbackWrapper);
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@UiThread
@Override
public void onStateOffline(@NonNull String cameraId) {
super.onStateOffline(cameraId);
abortImagecaptureRequests();
}
private void abortImagecaptureRequests() {
Throwable throwable = new CameraClosedException("Camera is closed.");
for (ImageCaptureRequest captureRequest :
mImageCaptureRequests) {
captureRequest.notifyCallbackError(
getError(throwable),
throwable.getMessage(), throwable);
}
mImageCaptureRequests.clear();
}
@UiThread
private void sendImageCaptureRequest(
@Nullable Executor listenerExecutor, OnImageCapturedCallback callback) {
String cameraId;
try {
// TODO(b/143734846): From here on, the image capture request should be
// self-contained and use this camera ID for everything. Currently the pre-capture
// sequence does not follow this approach and could fail if this use case is unbound
// or unbound to a different camera in the middle of pre-capture.
cameraId = getBoundCameraId();
} catch (Throwable e) {
// Not bound. Notify callback.
callback.onError(ERROR_INVALID_CAMERA,
"Not bound to a valid Camera [" + ImageCapture.this + "]", e);
return;
}
CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId);
int relativeRotation = cameraInfoInternal.getSensorRotationDegrees(
mConfig.getTargetRotation(Surface.ROTATION_0));
Rational targetRatio = mConfig.getTargetAspectRatioCustom(null);
targetRatio = ImageUtil.rotate(targetRatio, relativeRotation);
mImageCaptureRequests.offer(
new ImageCaptureRequest(relativeRotation, targetRatio, listenerExecutor, callback));
if (mImageCaptureRequests.size() == 1) {
issueImageCaptureRequests();
}
}
/** Issues saved ImageCaptureRequest. */
@UiThread
void issueImageCaptureRequests() {
if (mImageCaptureRequests.isEmpty()) {
return;
}
takePictureInternal();
}
/**
* The take picture flow.
*
* <p>There are three steps to take a picture.
*
* <p>(1) Pre-take picture, which will trigger af/ae scan or open torch if necessary. Then check
* 3A converged if necessary.
*
* <p>(2) Issue take picture single request.
*
* <p>(3) Post-take picture, which will cancel af/ae scan or close torch if necessary.
*/
private void takePictureInternal() {
final TakePictureState state = new TakePictureState();
FutureChain.from(preTakePicture(state))
.transformAsync(v -> issueTakePicture(), mExecutor)
.addCallback(
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
postTakePicture(state);
}
@Override
public void onFailure(Throwable throwable) {
Log.e(TAG, "takePictureInternal onFailure", throwable);
postTakePicture(state);
// To handle the error and issue the next capture request
// when the capture stages have any fail.
CameraXExecutors.mainThreadExecutor().execute(() -> {
ImageCaptureRequest request = mImageCaptureRequests.poll();
// It could happens if we clear the mImageCaptureRequests queue
// in the middle of takePicture.
if (request == null) {
return;
}
request.notifyCallbackError(getError(throwable),
(throwable != null) ? throwable.getMessage()
: "Unknown error", throwable);
// Handle the next request.
issueImageCaptureRequests();
});
}
},
mExecutor);
}
@NonNull
@Override
public String toString() {
return TAG + ":" + getName();
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ImageCaptureError
int getError(Throwable throwable) {
if (throwable instanceof CameraClosedException) {
return ERROR_CAMERA_CLOSED;
} else if (throwable instanceof CaptureFailedException) {
return ERROR_CAPTURE_FAILED;
} else {
return ERROR_UNKNOWN;
}
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void clear() {
clearPipeline();
mExecutor.shutdown();
super.clear();
}
/**
* {@inheritDoc}
*
* @hide
*/
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
protected Map<String, Size> onSuggestedResolutionUpdated(
@NonNull Map<String, Size> suggestedResolutionMap) {
String cameraId = getBoundCameraId();
Size resolution = suggestedResolutionMap.get(cameraId);
if (resolution == null) {
throw new IllegalArgumentException(
"Suggested resolution map missing resolution for camera " + cameraId);
}
if (mImageReader != null) {
if (mImageReader.getHeight() == resolution.getHeight()
&& mImageReader.getWidth() == resolution.getWidth()) {
// Resolution does not need to be updated. Return early.
return suggestedResolutionMap;
}
mImageReader.close();
}
mSessionConfigBuilder = createPipeline(cameraId, mConfig, resolution);
attachToCamera(cameraId, mSessionConfigBuilder.build());
// In order to speed up the take picture process, notifyActive at an early stage to
// attach the session capture callback to repeating and get capture result all the time.
notifyActive();
return suggestedResolutionMap;
}
final OnImageCloseListener mOnImageCloseListener = new OnImageCloseListener() {
/**
* {@inheritDoc}
*
* <p>Issues next image capture request when dispatched image is closed, which can ensure
* the image buffer in ImageReader is always available.
*/
@Override
public void onImageClose(ImageProxy image) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(() -> onImageClose(image));
return;
}
mImageCaptureRequests.poll();
issueImageCaptureRequests();
}
};
/**
* Routine before taking picture.
*
* <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
*/
private ListenableFuture<Void> preTakePicture(final TakePictureState state) {
return FutureChain.from(getPreCaptureStateIfNeeded())
.transformAsync(captureResult -> {
state.mPreCaptureState = captureResult;
triggerAfIfNeeded(state);
if (isFlashRequired(state)) {
state.mIsFlashTriggered = true;
triggerAePrecapture(state);
}
return check3AConverged(state);
}, mExecutor)
// Ignore the 3A convergence result.
.transform(is3AConverged -> null, mExecutor);
}
/**
* Routine after picture was taken.
*
* <p>For example, cancel 3A scan, close torch if necessary.
*/
void postTakePicture(final TakePictureState state) {
mExecutor.execute(() -> ImageCapture.this.cancelAfAeTrigger(state));
}
/**
* Gets a capture result or not according to current configuration.
*
* <p>Conditions to get a capture result.
*
* <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
* state.
*
* <p>(2) The flashMode is AUTO because it needs to know the current AE state.
*/
// Currently this method is used to prevent there is no repeating surface to get capture result.
// If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
// checking the capture result. Remove this check once no repeating surface issue is fixed.
private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
if (mEnableCheck3AConverged || getFlashMode() == FLASH_MODE_AUTO) {
return mSessionCallbackChecker.checkCaptureResult(
new CaptureCallbackChecker.CaptureResultChecker<CameraCaptureResult>() {
@Override
public CameraCaptureResult check(
@NonNull CameraCaptureResult captureResult) {
return captureResult;
}
});
}
return Futures.immediateFuture(null);
}
boolean isFlashRequired(TakePictureState state) {
switch (getFlashMode()) {
case FLASH_MODE_ON:
return true;
case FLASH_MODE_AUTO:
return state.mPreCaptureState.getAeState() == AeState.FLASH_REQUIRED;
case FLASH_MODE_OFF:
return false;
}
throw new AssertionError(getFlashMode());
}
ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
// Besides enableCheck3AConverged == true (MAX_QUALITY), if flash is triggered we also need
// to
// wait for 3A convergence.
if (!mEnableCheck3AConverged && !state.mIsFlashTriggered) {
return Futures.immediateFuture(false);
}
// if current capture result shows 3A is converged, no need to check upcoming capture
// result.
if (is3AConverged(state.mPreCaptureState)) {
return Futures.immediateFuture(true);
}
return mSessionCallbackChecker.checkCaptureResult(
new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
@Override
public Boolean check(@NonNull CameraCaptureResult captureResult) {
if (is3AConverged(captureResult)) {
return true;
}
// Return null to continue check.
return null;
}
},
CHECK_3A_TIMEOUT_IN_MS,
false);
}
boolean is3AConverged(CameraCaptureResult captureResult) {
if (captureResult == null) {
return false;
}
// If afMode is CAF, don't check af locked to speed up.
// If afMode is OFF or UNKNOWN , no need for waiting.
// otherwise wait until af is locked or focused.
boolean isAfReady = (captureResult.getAfMode() == AfMode.ON_CONTINUOUS_AUTO
|| captureResult.getAfMode() == AfMode.OFF
|| captureResult.getAfMode() == AfMode.UNKNOWN
|| captureResult.getAfState() == AfState.FOCUSED
|| captureResult.getAfState() == AfState.LOCKED_FOCUSED
|| captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED);
// Unknown means cannot get valid state from CaptureResult
boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
|| captureResult.getAeState() == AeState.UNKNOWN;
// Unknown means cannot get valid state from CaptureResult
boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
|| captureResult.getAwbState() == AwbState.UNKNOWN;
return (isAfReady && isAeReady && isAwbReady);
}
/**
* Issues the AF scan if needed.
*
* <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
* triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
* {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
* inactive, it means that a manual or auto focus request may be in progress or completed.
*/
void triggerAfIfNeeded(TakePictureState state) {
if (mEnableCheck3AConverged
&& state.mPreCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
&& state.mPreCaptureState.getAfState() == AfState.INACTIVE) {
triggerAf(state);
}
}
/** Issues a request to start auto focus scan. */
private void triggerAf(TakePictureState state) {
state.mIsAfTriggered = true;
getCurrentCameraControl().triggerAf();
}
/** Issues a request to start auto exposure scan. */
void triggerAePrecapture(TakePictureState state) {
state.mIsAePrecaptureTriggered = true;
getCurrentCameraControl().triggerAePrecapture();
}
/** Issues a request to cancel auto focus and/or auto exposure scan. */
void cancelAfAeTrigger(TakePictureState state) {
if (!state.mIsAfTriggered && !state.mIsAePrecaptureTriggered) {
return;
}
getCurrentCameraControl()
.cancelAfAeTrigger(state.mIsAfTriggered, state.mIsAePrecaptureTriggered);
state.mIsAfTriggered = false;
state.mIsAePrecaptureTriggered = false;
}
/** Issues a take picture request. */
ListenableFuture<Void> issueTakePicture() {
final List<ListenableFuture<Void>> futureList = new ArrayList<>();
final List<CaptureConfig> captureConfigs = new ArrayList<>();
CaptureBundle captureBundle;
if (mCaptureProcessor != null) {
// If the Processor is provided, check if we have valid CaptureBundle and update
// ProcessingImageReader before actually issuing a take picture request.
captureBundle = getCaptureBundle(null);
if (captureBundle == null) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture cannot set empty CaptureBundle."));
}
if (captureBundle.getCaptureStages().size() > mMaxCaptureStages) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture has CaptureStages > Max CaptureStage size"));
}
((ProcessingImageReader) mImageReader).setCaptureBundle(captureBundle);
} else {
captureBundle = getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle());
if (captureBundle.getCaptureStages().size() > 1) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ImageCapture have no CaptureProcess set with CaptureBundle size > 1."));
}
}
for (final CaptureStage captureStage : captureBundle.getCaptureStages()) {
final CaptureConfig.Builder builder = new CaptureConfig.Builder();
builder.setTemplateType(mCaptureConfig.getTemplateType());
builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
builder.addAllCameraCaptureCallbacks(
mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
builder.addSurface(mDeferrableSurface);
builder.addImplementationOptions(
captureStage.getCaptureConfig().getImplementationOptions());
builder.setTag(captureStage.getCaptureConfig().getTag());
builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(
completer -> {
CameraCaptureCallback completerCallback = new CameraCaptureCallback() {
@Override
public void onCaptureCompleted(
@NonNull CameraCaptureResult result) {
completer.set(null);
}
@Override
public void onCaptureFailed(
@NonNull CameraCaptureFailure failure) {
String msg = "Capture request failed with reason "
+ failure.getReason();
completer.setException(new CaptureFailedException(msg));
}
@Override
public void onCaptureCancelled() {
String msg = "Capture request is cancelled because "
+ "camera is closed";
completer.setException(new CameraClosedException(msg));
}
};
builder.addCameraCaptureCallback(completerCallback);
captureConfigs.add(builder.build());
return "issueTakePicture[stage=" + captureStage.getId() + "]";
});
futureList.add(future);
}
getCurrentCameraControl().submitCaptureRequests(captureConfigs);
return Futures.transform(Futures.allAsList(futureList),
input -> null, CameraXExecutors.directExecutor());
}
/** This exception is thrown when request is failed (reported by framework) */
static final class CaptureFailedException extends RuntimeException {
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
CaptureFailedException(String s, Throwable e) {
super(s, e);
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
CaptureFailedException(String s) {
super(s);
}
}
private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
if (captureStages == null || captureStages.isEmpty()) {
return defaultCaptureBundle;
}
return CaptureBundles.createCaptureBundle(captureStages);
}
/**
* Describes the error that occurred during an image capture operation (such as {@link
* ImageCapture#takePicture(Executor, OnImageCapturedCallback)}).
*
* <p>This is a parameter sent to the error callback functions set in listeners such as {@link
* ImageCapture.OnImageSavedCallback#onError(int, String, Throwable)}.
*
* @hide
*/
@IntDef({ERROR_UNKNOWN, ERROR_FILE_IO, ERROR_CAPTURE_FAILED, ERROR_CAMERA_CLOSED,
ERROR_INVALID_CAMERA})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY_GROUP)
public @interface ImageCaptureError {
}
/**
* Capture mode options for ImageCapture. A picture will always be taken regardless of
* mode, and the mode will be used on devices that support it.
*
* @hide
*/
@IntDef({CAPTURE_MODE_MAXIMIZE_QUALITY, CAPTURE_MODE_MINIMIZE_LATENCY})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(Scope.LIBRARY_GROUP)
public @interface CaptureMode {
}
/**
* The flash mode options when taking a picture using ImageCapture.
*
* <p>Applications can check if there is a flash unit via {@link CameraInfo#hasFlashUnit()} and
* update UI component if necessary. If there is no flash unit, then the FlashMode set to
* {@link #setFlashMode(int)} will take no effect for the subsequent photo capture requests
* and they will act like {@link #FLASH_MODE_OFF}.
*
* <p>When the torch is enabled via {@link CameraControl#enableTorch(boolean)}, the torch
* will remain enabled during photo capture regardless of flash mode setting. When
* the torch is disabled, flash will function as specified by
* {@link #setFlashMode(int)}.
*
* @hide
*/
@IntDef({FLASH_MODE_AUTO, FLASH_MODE_ON, FLASH_MODE_OFF})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @interface FlashMode {
}
/** Listener containing callbacks for image file I/O events. */
public interface OnImageSavedCallback {
/**
* Called when an image has been successfully saved.
*
* @param file The file object which the image is saved to
*/
// Todo: b/145130873 - Methods accepting `File` should also accept `FileDescriptor`
@SuppressLint("StreamFiles")
void onImageSaved(@NonNull File file);
/**
* Called when an error occurs while attempting to save an image.
*
* @param imageCaptureError The type of error composed by CameraX
* @param message The error message composed by CameraX
* @param cause The throwable from lower-layer error
*/
void onError(
@ImageCaptureError int imageCaptureError,
@NonNull String message,
@Nullable Throwable cause);
}
/**
* Callback for when an image capture has completed.
*/
public abstract static class OnImageCapturedCallback {
/**
* Callback for when the image has been captured.
*
* <p>The application is responsible for calling {@link ImageProxy#close()} to close the
* image.
*
* <p>The image is of format {@link ImageFormat#JPEG}, queryable via
* {@link ImageProxy#getFormat()}.
*
* <p>The image is provided as captured by the underlying {@link ImageReader} without
* rotation applied. rotationDegrees describes the magnitude of clockwise rotation, which
* if applied to the image will make it match the currently configured target rotation.
*
* <p>For example, if the current target rotation is set to the display rotation,
* rotationDegrees is the rotation to apply to the image to match the display orientation.
* A rotation of 90 degrees would mean rotating the image 90 degrees clockwise produces an
* image that will match the display orientation.
*
* <p>See also {@link Builder#setTargetRotation(int)} and
* {@link #setTargetRotation(int)}.
*
* <p>Timestamps are in nanoseconds and monotonic and can be compared to timestamps from
* images produced from UseCases bound to the same camera instance. More detail is
* available depending on the implementation. For example with CameraX using a
* {@link androidx.camera.camera2} implementation additional detail can be found in
* {@link android.hardware.camera2.CameraDevice} documentation.
*
* @param image The captured image
*/
public void onCaptureSuccess(@NonNull ImageProxy image) {
image.close();
}
/**
* Callback for when an error occurred during image capture.
*
* @param imageCaptureError The type of error composed by CameraX
* @param message The error message composed by CameraX
* @param cause The throwable from lower-layer error
*/
public void onError(@ImageCaptureError int imageCaptureError, @NonNull String message,
@Nullable Throwable cause) {
}
}
/**
* Provides a base static default configuration for the ImageCapture
*
* <p>These values may be overridden by the implementation. They only provide a minimum set of
* defaults that are implementation independent.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static final class Defaults
implements ConfigProvider<ImageCaptureConfig> {
@CaptureMode
private static final int DEFAULT_CAPTURE_MODE = CAPTURE_MODE_MINIMIZE_LATENCY;
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
private static final ImageCaptureConfig DEFAULT_CONFIG;
static {
Builder builder =
new Builder()
.setCaptureMode(DEFAULT_CAPTURE_MODE)
.setFlashMode(DEFAULT_FLASH_MODE)
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@Override
public ImageCaptureConfig getConfig(@Nullable Integer lensFacing) {
return DEFAULT_CONFIG;
}
}
/** Holder class for metadata that will be saved with captured images. */
public static final class Metadata {
/**
* Indicates a left-right mirroring (reflection).
*
* <p>The reflection is meant to be applied to the upright image (after rotation to the
* target orientation). When saving the image to file, it is combined with the rotation
* degrees, to generate the corresponding EXIF orientation value.
*/
private boolean mIsReversedHorizontal;
/**
* Indicates an upside down mirroring, equivalent to a horizontal mirroring (reflection)
* followed by a 180 degree rotation.
*
* <p>The reflection is meant to be applied to the upright image (after rotation to the
* target orientation). When saving the image to file, it is combined with the rotation
* degrees, to generate the corresponding EXIF orientation value.
*/
private boolean mIsReversedVertical;
/** Data representing a geographic location. */
@Nullable
private Location mLocation;
/**
* Gets left-right mirroring of the capture.
*
* @return true if the capture is left-right mirrored.
*/
public boolean isReversedHorizontal() {
return mIsReversedHorizontal;
}
/**
* Sets left-right mirroring of the capture.
*
* @param isReversedHorizontal true if the capture is left-right mirrored.
*/
public void setReversedHorizontal(boolean isReversedHorizontal) {
mIsReversedHorizontal = isReversedHorizontal;
}
/**
* Gets upside-down mirroring of the capture.
*
* @return true if the capture is upside-down.
*/
public boolean isReversedVertical() {
return mIsReversedVertical;
}
/**
* Sets upside-down mirroring of the capture.
*
* @param isReversedVertical true if the capture is upside-down.
*/
public void setReversedVertical(boolean isReversedVertical) {
mIsReversedVertical = isReversedVertical;
}
/**
* Gets the geographic location of the capture.
*
* @return the geographic location.
*/
@Nullable
public Location getLocation() {
return mLocation;
}
/**
* Sets the geographic location of the capture.
*
* @param location the geographic location.
*/
public void setLocation(@Nullable Location location) {
mLocation = location;
}
}
/**
* An intermediate action recorder while taking picture. It is used to restore certain states.
* For example, cancel AF/AE scan, and close flash light.
*/
static final class TakePictureState {
CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
boolean mIsAfTriggered = false;
boolean mIsAePrecaptureTriggered = false;
boolean mIsFlashTriggered = false;
}
/**
* A helper class to check camera capture result.
*
* <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
* specified list of condition and sets a ListenableFuture when the conditions have been met. It
* is mainly used to continuously capture callbacks to detect specific conditions. It also
* handles the timeout condition if the check condition does not satisfy the given timeout, and
* returns the given default value if the timeout is met.
*/
static final class CaptureCallbackChecker extends CameraCaptureCallback {
private static final long NO_TIMEOUT = 0L;
/** Capture listeners. */
private final Set<CaptureResultListener> mCaptureResultListeners = new HashSet<>();
@Override
public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
deliverCaptureResultToListeners(cameraCaptureResult);
}
/**
* Check the capture results of current session capture callback by giving a {@link
* CaptureResultChecker}.
*
* @param checker a CaptureResult checker that returns an object with type T if the check is
* complete, returning null to continue the check process.
* @param <T> the type parameter for CaptureResult checker.
* @return a listenable future for capture result check process.
*/
<T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
return checkCaptureResult(checker, NO_TIMEOUT, null);
}
/**
* Check the capture results of current session capture callback with timeout limit by
* giving a {@link CaptureResultChecker}.
*
* @param checker a CaptureResult checker that returns an object with type T if the
* check is
* complete, returning null to continue the check process.
* @param timeoutInMs used to force stop checking.
* @param defValue the default return value if timeout occur.
* @param <T> the type parameter for CaptureResult checker.
* @return a listenable future for capture result check process.
*/
<T> ListenableFuture<T> checkCaptureResult(
final CaptureResultChecker<T> checker, final long timeoutInMs, final T defValue) {
if (timeoutInMs < NO_TIMEOUT) {
throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
}
final long startTimeInMs =
(timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
return CallbackToFutureAdapter.getFuture(
completer -> {
addListener(
new CaptureResultListener() {
@Override
public boolean onCaptureResult(
@NonNull CameraCaptureResult captureResult) {
T result = checker.check(captureResult);
if (result != null) {
completer.set(result);
return true;
} else if (startTimeInMs > 0
&& SystemClock.elapsedRealtime() - startTimeInMs
> timeoutInMs) {
completer.set(defValue);
return true;
}
// Return false to continue check.
return false;
}
});
return "checkCaptureResult";
});
}
/**
* Delivers camera capture result to {@link CaptureCallbackChecker#mCaptureResultListeners}.
*/
private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
synchronized (mCaptureResultListeners) {
Set<CaptureResultListener> removeSet = null;
for (CaptureResultListener listener : new HashSet<>(mCaptureResultListeners)) {
// Remove listener if the callback return true
if (listener.onCaptureResult(captureResult)) {
if (removeSet == null) {
removeSet = new HashSet<>();
}
removeSet.add(listener);
}
}
if (removeSet != null) {
mCaptureResultListeners.removeAll(removeSet);
}
}
}
/** Add capture result listener. */
void addListener(CaptureResultListener listener) {
synchronized (mCaptureResultListeners) {
mCaptureResultListeners.add(listener);
}
}
/** An interface to check camera capture result. */
public interface CaptureResultChecker<T> {
/**
* The callback to check camera capture result.
*
* @param captureResult the camera capture result.
* @return the check result, return null to continue checking.
*/
@Nullable
T check(@NonNull CameraCaptureResult captureResult);
}
/** An interface to listen to camera capture results. */
private interface CaptureResultListener {
/**
* Callback to handle camera capture results.
*
* @param captureResult camera capture result.
* @return true to finish listening, false to continue listening.
*/
boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
}
}
private final class ImageCaptureRequest {
@RotationValue
int mRotationDegrees;
Rational mTargetRatio;
@NonNull
Executor mListenerExecutor;
@NonNull
OnImageCapturedCallback mCallback;
ImageCaptureRequest(
@RotationValue int rotationDegrees,
Rational targetRatio,
@NonNull Executor executor,
@NonNull OnImageCapturedCallback callback) {
mRotationDegrees = rotationDegrees;
mTargetRatio = targetRatio;
mListenerExecutor = executor;
mCallback = callback;
}
void dispatchImage(final ImageProxy image) {
try {
mListenerExecutor.execute(() -> {
Size sourceSize = new Size(image.getWidth(), image.getHeight());
if (ImageUtil.isAspectRatioValid(sourceSize, mTargetRatio)) {
image.setCropRect(
ImageUtil.computeCropRectFromAspectRatio(sourceSize,
mTargetRatio));
}
ImageInfo imageInfo = ImmutableImageInfo.create(image.getImageInfo().getTag(),
image.getImageInfo().getTimestamp(), mRotationDegrees);
mCallback.onCaptureSuccess(new SettableImageProxy(image, imageInfo));
});
} catch (RejectedExecutionException e) {
Log.e(TAG, "Unable to post to the supplied executor.");
// Unable to execute on the supplied executor, close the image.
image.close();
}
}
void notifyCallbackError(final @ImageCaptureError int imageCaptureError,
final String message, final Throwable cause) {
try {
mListenerExecutor.execute(
() -> mCallback.onError(imageCaptureError, message, cause));
} catch (RejectedExecutionException e) {
Log.e(TAG, "Unable to post to the supplied executor.");
}
}
}
/** Builder for an {@link ImageCapture}. */
public static final class Builder implements
UseCaseConfig.Builder<ImageCapture, ImageCaptureConfig, Builder>,
ImageOutputConfig.Builder<Builder>,
CameraDeviceConfig.Builder<Builder>,
IoConfig.Builder<Builder> {
private final MutableOptionsBundle mMutableConfig;
/** Creates a new Builder object. */
public Builder() {
this(MutableOptionsBundle.create());
}
private Builder(MutableOptionsBundle mutableConfig) {
mMutableConfig = mutableConfig;
Class<?> oldConfigClass =
mutableConfig.retrieveOption(TargetConfig.OPTION_TARGET_CLASS, null);
if (oldConfigClass != null && !oldConfigClass.equals(ImageCapture.class)) {
throw new IllegalArgumentException(
"Invalid target class configuration for "
+ Builder.this
+ ": "
+ oldConfigClass);
}
setTargetClass(ImageCapture.class);
}
/**
* Generates a Builder from another Config object
*
* @param configuration An immutable configuration to pre-populate this builder.
* @return The new Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public static Builder fromConfig(@NonNull ImageCaptureConfig configuration) {
return new Builder(MutableOptionsBundle.from(configuration));
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public MutableConfig getMutableConfig() {
return mMutableConfig;
}
/**
* {@inheritDoc}
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
public ImageCaptureConfig getUseCaseConfig() {
return new ImageCaptureConfig(OptionsBundle.from(mMutableConfig));
}
/**
* Builds an immutable {@link ImageCapture} from the current state.
*
* @return A {@link ImageCapture} populated with the current state.
* @throws IllegalArgumentException if attempting to set both target aspect ratio and
* target resolution.
*/
@Override
@NonNull
public ImageCapture build() {
// Error at runtime for using both setTargetResolution and setTargetAspectRatio on
// the same config.
if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
&& getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
throw new IllegalArgumentException(
"Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+ "config.");
}
return new ImageCapture(getUseCaseConfig());
}
/**
* Sets the image capture mode.
*
* <p>Valid capture modes are {@link CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY}, which
* prioritizes
* latency over image quality, or {@link CaptureMode#CAPTURE_MODE_MAXIMIZE_QUALITY},
* which prioritizes
* image quality over latency.
*
* <p>If not set, the capture mode will default to
* {@link CaptureMode#CAPTURE_MODE_MINIMIZE_LATENCY}.
*
* @param captureMode The requested image capture mode.
* @return The current Builder.
*/
@NonNull
public Builder setCaptureMode(@CaptureMode int captureMode) {
getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_MODE, captureMode);
return this;
}
/**
* Sets the flashMode.
*
* <p>If not set, the flash mode will default to {@link #FLASH_MODE_OFF}.
*
* <p>See {@link ImageCapture#setFlashMode(int)} for more information.
*
* @param flashMode The requested flash mode.
* @return The current Builder.
*/
@NonNull
public Builder setFlashMode(@FlashMode int flashMode) {
getMutableConfig().insertOption(OPTION_FLASH_MODE, flashMode);
return this;
}
/**
* Sets the {@link CaptureBundle}.
*
* @param captureBundle The requested capture bundle for extension.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setCaptureBundle(@NonNull CaptureBundle captureBundle) {
getMutableConfig().insertOption(OPTION_CAPTURE_BUNDLE, captureBundle);
return this;
}
/**
* Sets the {@link CaptureProcessor}.
*
* @param captureProcessor The requested capture processor for extension.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
getMutableConfig().insertOption(OPTION_CAPTURE_PROCESSOR, captureProcessor);
return this;
}
/**
* Sets the {@link ImageFormat} of the {@link ImageProxy} returned by the
* {@link ImageCapture.OnImageCapturedCallback}.
*
* <p>Warning. This could lead to an invalid configuration as image format support is per
* device. Also, setting the buffer format in conjuncture with image capture extensions will
* result in an invalid configuration. In this case {@link
* ImageCapture#ImageCapture(ImageCaptureConfig)} will throw an
* {@link IllegalArgumentException}.
*
* @param bufferImageFormat The image format for captured images.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setBufferFormat(int bufferImageFormat) {
getMutableConfig().insertOption(OPTION_BUFFER_FORMAT, bufferImageFormat);
return this;
}
/**
* Sets the max number of {@link CaptureStage}.
*
* @param maxCaptureStages The max CaptureStage number.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setMaxCaptureStages(int maxCaptureStages) {
getMutableConfig().insertOption(OPTION_MAX_CAPTURE_STAGES, maxCaptureStages);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutions) {
getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
return this;
}
// Implementations of TargetConfig.Builder default methods
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setTargetClass(@NonNull Class<ImageCapture> targetClass) {
getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
// If no name is set yet, then generate a unique name
if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
setTargetName(targetName);
}
return this;
}
/**
* Sets the name of the target object being configured, used only for debug logging.
*
* <p>The name should be a value that can uniquely identify an instance of the object being
* configured.
*
* <p>If not set, the target name will default to a unique name automatically generated
* with the class canonical name and random UUID.
*
* @param targetName A unique string identifier for the instance of the class being
* configured.
* @return the current Builder.
*/
@Override
@NonNull
public Builder setTargetName(@NonNull String targetName) {
getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
return this;
}
// Implementations of CameraDeviceConfig.Builder default methods
/**
* Sets the primary camera to be configured based on the direction the lens is facing.
*
* <p>If multiple cameras exist with equivalent lens facing direction, the first ("primary")
* camera for that direction will be chosen.
*
* @param lensFacing The direction of the camera's lens.
* @return the current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setLensFacing(@CameraSelector.LensFacing int lensFacing) {
getMutableConfig().insertOption(OPTION_LENS_FACING, lensFacing);
return this;
}
/**
* Sets a {@link CameraIdFilter} that filter out the unavailable camera id.
*
* <p>The camera id filter will be used to filter those cameras with lens facing
* specified in the config.
*
* @param cameraIdFilter The {@link CameraIdFilter}.
* @return the current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setCameraIdFilter(@NonNull CameraIdFilter cameraIdFilter) {
getMutableConfig().insertOption(OPTION_CAMERA_ID_FILTER, cameraIdFilter);
return this;
}
// Implementations of ImageOutputConfig.Builder default methods
/**
* Sets the aspect ratio of the intended target for images from this configuration.
*
* <p>This is the ratio of the target's width to the image's height, where the numerator of
* the provided {@link Rational} corresponds to the width, and the denominator corresponds
* to the height.
*
* <p>The target aspect ratio is used as a hint when determining the resulting output aspect
* ratio which may differ from the request, possibly due to device constraints.
* Application code should check the resulting output's resolution.
*
* <p>This method can be used to request an aspect ratio that is not from the standard set
* of aspect ratios defined in the {@link AspectRatio}.
*
* <p>This method will remove any value set by setTargetAspectRatio().
*
* <p>setTargetAspectRatioCustom will crop the file outputs from takePicture methods
* as opposed to {@link #setTargetAspectRatio(int)} which does not apply a crop. The source
* (pre-crop) resolution will an automatically selected resolution suitable for still
* images.
*
* <p>For ImageCapture, the outputs are the {@link ImageProxy} or the File passed to image
* capture listeners.
*
* @param aspectRatio A {@link Rational} representing the ratio of the target's width and
* height.
* @return The current Builder.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
public Builder setTargetAspectRatioCustom(@NonNull Rational aspectRatio) {
getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO_CUSTOM, aspectRatio);
getMutableConfig().removeOption(OPTION_TARGET_ASPECT_RATIO);
return this;
}
/**
* Sets the aspect ratio of the intended target for images from this configuration.
*
* <p>The aspect ratio is the ratio of width to height in the sensor orientation.
*
* <p>It is not allowed to set both target aspect ratio and target resolution on the same
* use case. Attempting so will throw an IllegalArgumentException when building the
* Config.
*
* <p>The target aspect ratio is used as a hint when determining the resulting output aspect
* ratio which may differ from the request, possibly due to device constraints.
* Application code should check the resulting output's resolution.
*
* <p>If not set, resolutions with aspect ratio 4:3 will be considered in higher
* priority.
*
* @param aspectRatio The desired ImageCapture {@link AspectRatio}
* @return The current Builder.
*/
@NonNull
@Override
public Builder setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
return this;
}
/**
* Sets the rotation of the intended target for images from this configuration.
*
* This will affect the EXIF rotation metadata in images saved by takePicture calls and the
* {@link ImageInfo#getRotationDegrees()} value of the {@link ImageProxy} returned by
* {@link OnImageCapturedCallback}. These will be set to be the rotation, which if
* applied to the output image data, will make the image match the target rotation
* specified here.
*
* <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
* Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
* Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
*
* <p>In general, it is best to additionally set the target rotation dynamically on the use
* case. See {@link androidx.camera.core.ImageCapture#setTargetRotation(int)} for
* additional documentation.
*
* <p>If not set, the target rotation will default to the value of
* {@link android.view.Display#getRotation()} of the default display at the time the use
* case is created.
*
* @param rotation The rotation of the intended target.
* @return The current Builder.
* @see androidx.camera.core.ImageCapture#setTargetRotation(int)
* * @see android.view.OrientationEventListener
*/
@NonNull
@Override
public Builder setTargetRotation(@RotationValue int rotation) {
getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
return this;
}
/**
* Sets the intended output target resolution.
*
* <p>The target resolution attempts to establish a minimum bound for the image resolution.
* The actual image resolution will be the closest available resolution in size that is not
* smaller than the target resolution, as determined by the Camera implementation. However,
* if no resolution exists that is equal to or larger than the target resolution, the
* nearest available resolution smaller than the target resolution will be chosen.
* Resolutions with the same aspect ratio of the provided {@link Size} will be considered in
* higher priority before resolutions of different aspect ratios.
*
* <p>It is not allowed to set both target aspect ratio and target resolution on the same
* use case. Attempting so will throw an IllegalArgumentException when building the
* Config.
*
* <p>The resolution {@link Size} should be expressed at the use cases's target rotation.
* For example, a device with portrait natural orientation in natural target rotation
* requesting a portrait image may specify 480x640, and the same device, rotated 90 degrees
* and targeting landscape orientation may specify 640x480.
*
* <p>The maximum available resolution that could be selected for an {@link ImageCapture}
* will depend on the camera device's capability.
*
* <p>If not set, the largest available resolution will be selected to use. Usually,
* users will intend to get the largest still image that the camera device can support.
*
* @param resolution The target resolution to choose from supported output sizes list.
* @return The current Builder.
*/
@NonNull
@Override
public Builder setTargetResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
if (resolution != null) {
getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO_CUSTOM,
new Rational(resolution.getWidth(), resolution.getHeight()));
}
return this;
}
/**
* Sets the default resolution of the intended target from this configuration.
*
* @param resolution The default resolution to choose from supported output sizes list.
* @return The current Builder.
* @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public Builder setDefaultResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(ImageOutputConfig.OPTION_DEFAULT_RESOLUTION,
resolution);
return this;
}
/** @hide */
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public Builder setMaxResolution(@NonNull Size resolution) {
getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
return this;
}
// Implementations of IoConfig.Builder default methods
/**
* Sets the default executor that will be used for IO tasks.
*
* <p> This executor will be used for any IO tasks specifically for ImageCapture, such as
* {@link ImageCapture#takePicture(File, Executor, ImageCapture.OnImageSavedCallback)}
* and {@link ImageCapture#takePicture(File, ImageCapture.Metadata, Executor,
* ImageCapture.OnImageSavedCallback)}. If no executor is set, then a default Executor
* specifically for IO will be used instead.
*
* @param executor The executor which will be used for IO tasks.
* @return the current Builder.
*/
@Override
@NonNull
public Builder setIoExecutor(@NonNull Executor executor) {
getMutableConfig().insertOption(OPTION_IO_EXECUTOR, executor);
return this;
}
// Implementations of UseCaseConfig.Builder default methods
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSessionOptionUnpacker(
@NonNull SessionConfig.OptionUnpacker optionUnpacker) {
getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setCaptureOptionUnpacker(
@NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setSurfaceOccupancyPriority(int priority) {
getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
return this;
}
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
public Builder setUseCaseEventCallback(
@NonNull UseCase.EventCallback useCaseEventCallback) {
getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
return this;
}
}
}