/*
* 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.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_PROCESSOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_FLASH_MODE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IMAGE_READER_PROXY_PROVIDER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_IO_EXECUTOR;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_CLASS;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_NAME;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_RESOLUTION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ROTATION;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.location.Location;
import android.media.Image;
import android.media.ImageReader;
import android.net.Uri;
import android.os.Looper;
import android.os.SystemClock;
import android.provider.MediaStore;
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.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
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.CameraInternal;
import androidx.camera.core.impl.CaptureBundle;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.CaptureProcessor;
import androidx.camera.core.impl.CaptureStage;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.Exif;
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.IoConfig;
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.utils.ImageUtil;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* 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}.
*/
public final class ImageCapture extends UseCase {
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase lifetime constant] - Stays constant for the lifetime of the UseCase. Which means
// they could be created in the constructor.
////////////////////////////////////////////////////////////////////////////////////////////
/**
* 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.
*/
private static final int FLASH_MODE_UNKNOWN = -1;
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";
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
private static final int MAX_IMAGES = 2;
// TODO(b/149336664) Move the quality to a compatibility class when there is a per device case.
private static final byte JPEG_QUALITY_MAXIMIZE_QUALITY_MODE = 100;
private static final byte JPEG_QUALITY_MINIMIZE_LATENCY_MODE = 95;
@CaptureMode
private static final int DEFAULT_CAPTURE_MODE = CAPTURE_MODE_MINIMIZE_LATENCY;
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
private final ImageReaderProxy.OnImageAvailableListener mClosingListener = (imageReader -> {
try (ImageProxy image = imageReader.acquireLatestImage()) {
Log.d(TAG, "Discarding ImageProxy which was inadvertently acquired: " + image);
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to acquire latest image.", e);
}
});
@NonNull
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Executor mIoExecutor;
@CaptureMode
private final int mCaptureMode;
/**
* 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 final boolean mEnableCheck3AConverged;
@GuardedBy("mLockedFlashMode")
private final AtomicReference<Integer> mLockedFlashMode = new AtomicReference<>(null);
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase lifetime dynamic] - Dynamic variables which could change during anytime during
// the UseCase lifetime.
////////////////////////////////////////////////////////////////////////////////////////////
/** Current flash mode. */
@GuardedBy("mLockedFlashMode")
@FlashMode
private int mFlashMode = FLASH_MODE_UNKNOWN;
private Rational mCropAspectRatio = null;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
////////////////////////////////////////////////////////////////////////////////////////////
private ExecutorService mExecutor;
private CaptureConfig mCaptureConfig;
/** The set of requests that will be sent to the camera for the final captured image. */
private CaptureBundle mCaptureBundle;
private int mMaxCaptureStages;
/**
* Processing that gets done to the mCaptureBundle to produce the final image that is produced
* by {@link #takePicture(Executor, OnImageCapturedCallback)}
*/
private CaptureProcessor mCaptureProcessor;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
SessionConfig.Builder mSessionConfigBuilder;
/** synthetic accessor */
@SuppressWarnings("WeakerAccess")
SafeCloseImageReaderProxy mImageReader;
@SuppressWarnings("WeakerAccess")
ProcessingImageReader mProcessingImageReader;
/** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
private DeferrableSurface mDeferrableSurface;
private ImageCaptureRequestProcessor mImageCaptureRequestProcessor;
/**
* 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);
ImageCaptureConfig useCaseConfig = (ImageCaptureConfig) getUseCaseConfig();
if (useCaseConfig.containsOption(OPTION_IMAGE_CAPTURE_MODE)) {
mCaptureMode = useCaseConfig.getCaptureMode();
} else {
mCaptureMode = DEFAULT_CAPTURE_MODE;
}
mIoExecutor = Preconditions.checkNotNull(
useCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor()));
if (mCaptureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
} else {
mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
}
}
@UiThread
@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);
// Setup the ImageReader to do processing
if (config.getImageReaderProxyProvider() != null) {
mImageReader =
new SafeCloseImageReaderProxy(
config.getImageReaderProxyProvider().newInstance(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES, 0));
mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
};
} else if (mCaptureProcessor != null) {
// TODO: To allow user to use an Executor for the image processing.
mProcessingImageReader =
new ProcessingImageReader(
resolution.getWidth(),
resolution.getHeight(),
getImageFormat(), mMaxCaptureStages,
/* postProcessExecutor */mExecutor,
getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
mCaptureProcessor);
mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
} else {
MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES);
mMetadataMatchingCaptureCallback = metadataImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(metadataImageReader);
}
mImageCaptureRequestProcessor = new ImageCaptureRequestProcessor(MAX_IMAGES,
request -> takePictureInternal(request));
// By default close images that come from the listener.
mImageReader.setOnImageAvailableListener(mClosingListener,
CameraXExecutors.mainThreadExecutor());
SafeCloseImageReaderProxy imageReaderProxy = mImageReader;
if (mDeferrableSurface != null) {
mDeferrableSurface.close();
}
mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
mDeferrableSurface.getTerminationFuture().addListener(
imageReaderProxy::safeClose, CameraXExecutors.mainThreadExecutor());
sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
clearPipeline();
// Ensure the attached camera has not changed before resetting.
// TODO(b/143915543): Ensure this never gets called by a camera that is not attached
// to this use case so we don't need to do this check.
if (isCurrentCamera(cameraId)) {
// Only reset the pipeline when the bound camera is the same.
mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
}
});
return sessionConfigBuilder;
}
/**
* Clear the internal pipeline so that the pipeline can be set up again.
*/
@UiThread
@SuppressWarnings("WeakerAccess")
void clearPipeline() {
Threads.checkMainThread();
DeferrableSurface deferrableSurface = mDeferrableSurface;
mDeferrableSurface = null;
mImageReader = null;
mProcessingImageReader = null;
if (deferrableSurface != null) {
deferrableSurface.close();
}
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@Nullable
@RestrictTo(Scope.LIBRARY_GROUP)
public UseCaseConfig.Builder<?, ?, ?> getDefaultBuilder() {
ImageCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(ImageCaptureConfig.class);
if (defaults != null) {
return Builder.fromConfig(defaults);
}
return null;
}
/**
* {@inheritDoc}
*
* @hide
*/
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder() {
return Builder.fromConfig((ImageCaptureConfig) getUseCaseConfig());
}
/**
* Configures flash mode to CameraControlInternal once it is ready.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
protected void onCameraControlReady() {
trySetFlashModeToCameraControl();
}
/**
* Get the flash mode.
*
* @return the flashMode. Value is {@link #FLASH_MODE_AUTO}, {@link #FLASH_MODE_ON}, or
* {@link #FLASH_MODE_OFF}.
*/
@FlashMode
public int getFlashMode() {
synchronized (mLockedFlashMode) {
return mFlashMode != FLASH_MODE_UNKNOWN ? mFlashMode
: ((ImageCaptureConfig) getUseCaseConfig()).getFlashMode(DEFAULT_FLASH_MODE);
}
}
/**
* 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. Value is {@link #FLASH_MODE_AUTO}, {@link #FLASH_MODE_ON},
* or {@link #FLASH_MODE_OFF}.
*/
public void setFlashMode(@FlashMode int flashMode) {
if (flashMode != FLASH_MODE_AUTO && flashMode != FLASH_MODE_ON
&& flashMode != FLASH_MODE_OFF) {
throw new IllegalArgumentException("Invalid flash mode: " + flashMode);
}
synchronized (mLockedFlashMode) {
mFlashMode = flashMode;
trySetFlashModeToCameraControl();
}
}
/**
* Sets target cropping aspect ratio for output image.
*
* <p>This aspect ratio is orientation-dependent. It should be expressed in the coordinate
* frame after rotating the image by the target rotation.
*
* <p>This sets the cropping rectangle returned by {@link ImageProxy#getCropRect()} returned
* from {@link ImageCapture#takePicture(Executor, OnImageCapturedCallback)}.
*
* <p>For example, assume the {@code aspectRatio} of 3x4. If an image has a resolution of
* 480x640 after applying the target rotation, then the output {@link ImageProxy} of
* {@link ImageCapture#takePicture(Executor, OnImageCapturedCallback)} would have a cropping
* rectangle of 480x640 after applying the rotation degrees. However, if an image has a
* resolution of 640x480 after applying the target rotation, then the cropping rectangle
* of the output {@link ImageProxy} would be 360x480 after applying the rotation degrees.
*
* <p>This crops the saved image when calling
* {@link ImageCapture#takePicture(OutputFileOptions, Executor, OnImageSavedCallback)}. Note
* that the cropping will introduce an additional latency.
*
* <p>Cropping occurs around the center of the image and as though it were in the target
* rotation. For example, assume the {@code aspectRatio} of 3x4. If an image has a resolution
* of 480x640 after applying the target rotation, then the saved output image would be
* 480x640 after applying the EXIF orientation value. However, if an image has a resolution
* of 640x480 after applying the target rotation, then the saved output image would be
* 360x480 after applying the EXIF orientation value.
*
* <p>This setting value will be automatically updated to match the new target rotation value
* when {@link ImageCapture#setTargetRotation(int)} is called.
*
* @param aspectRatio New target aspect ratio.
*/
public void setCropAspectRatio(@NonNull Rational aspectRatio) {
mCropAspectRatio = aspectRatio;
}
/**
* Returns the desired rotation of the output image.
*
* <p>The rotation can be set prior to constructing an ImageCapture using
* {@link ImageCapture.Builder#setTargetRotation(int)} or dynamically by calling
* {@link ImageCapture#setTargetRotation(int)}. The rotation of an image taken is determined
* by the rotation value set at the time image capture is initiated, such as when calling
* {@link #takePicture(Executor, OnImageCapturedCallback)}.
*
* <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. The use case is fully created once it has been attached to a camera.
*
* @return The rotation of the intended target.
*/
@RotationValue
public int getTargetRotation() {
return getTargetRotationInternal();
}
/**
* 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. The mapping values are listed as the following.
* <p>{@link android.view.OrientationEventListener#ORIENTATION_UNKNOWN}: orientation == -1
* <p>{@link Surface#ROTATION_0}: orientation >= 315 || orientation < 45
* <p>{@link Surface#ROTATION_90}: orientation >= 225 && orientation < 315
* <p>{@link Surface#ROTATION_180}: orientation >= 135 && orientation < 225
* <p>{@link Surface#ROTATION_270}: orientation >= 45 && orientation < 135
*
* <p>When this function is called, value set by
* {@link ImageCapture.Builder#setTargetResolution(Size)} will be updated automatically to make
* sure the suitable resolution can be selected when the use case is bound. Value set by
* {@link ImageCapture#setCropAspectRatio(Rational)} will also be updated automatically to
* make sure the output image is cropped into expected aspect ratio.
*
* <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. The
* use case is fully created once it has been attached to a camera.
*
* <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) {
int oldRotation = getTargetRotation();
if (setTargetRotationInternal(rotation)) {
// For the crop aspect ratio value, the numerator and denominator of original setting
// value will be swapped then set back. It is an orientation-dependent value that will
// be used to crop ImageCapture's output image.
if (mCropAspectRatio != null) {
int oldRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(
oldRotation);
int newRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(rotation);
mCropAspectRatio = ImageUtil.getRotatedAspectRatio(
Math.abs(newRotationDegrees - oldRotationDegrees), mCropAspectRatio);
}
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
}
}
/**
* Returns the set capture mode.
*
* <p>This is set when constructing an ImageCapture using
* {@link ImageCapture.Builder#setCaptureMode(int)}. This is static for an instance of
* ImageCapture.
*/
@CaptureMode
public int getCaptureMode() {
return mCaptureMode;
}
/**
* 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
*/
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 along with application specified metadata.
*
* <p>The callback will be called only once for every invocation of this method.
*
* <p> If the {@link ImageCapture} is in a {@link UseCaseGroup} where {@link ViewPort} is
* set, or {@link #setCropAspectRatio} is used, the image may be cropped before saving to
* disk which causes an additional latency.
*
* @param outputFileOptions Options to store the newly captured image.
* @param executor The executor in which the callback methods will be run.
* @param imageSavedCallback Callback to be called for the newly captured image.
* @see ViewPort
*/
public void takePicture(
final @NonNull OutputFileOptions outputFileOptions,
final @NonNull Executor executor,
final @NonNull OnImageSavedCallback imageSavedCallback) {
if (Looper.getMainLooper() != Looper.myLooper()) {
CameraXExecutors.mainThreadExecutor().execute(
() -> takePicture(outputFileOptions, executor, imageSavedCallback));
return;
}
// Check whether the captured image can be saved. If it cannot, fail fast and notify user
if (!ImageSaveLocationValidator.isValid(outputFileOptions)) {
executor.execute(() -> imageSavedCallback.onError(
new ImageCaptureException(ERROR_FILE_IO,
"Cannot save capture result to specified location", null)));
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(@NonNull OutputFileResults outputFileResults) {
imageSavedCallback.onImageSaved(outputFileResults);
}
@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(
new ImageCaptureException(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,
outputFileOptions,
image.getImageInfo().getRotationDegrees(),
executor,
imageSavedCallbackWrapper));
}
@Override
public void onError(@NonNull final ImageCaptureException exception) {
imageSavedCallback.onError(exception);
}
};
// 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 onStateDetached() {
abortImageCaptureRequests();
}
private void abortImageCaptureRequests() {
Throwable throwable = new CameraClosedException("Camera is closed.");
mImageCaptureRequestProcessor.cancelRequests(throwable);
}
@UiThread
private void sendImageCaptureRequest(
@NonNull Executor callbackExecutor, @NonNull OnImageCapturedCallback callback) {
// TODO(b/143734846): From here on, the image capture request should be
// self-contained and use this camera for everything. Currently the pre-capture
// sequence does not follow this approach and could fail if this use case is unbound
// or reattached to a different camera in the middle of pre-capture.
CameraInternal attachedCamera = getCamera();
if (attachedCamera == null) {
// Not bound. Notify callback.
callbackExecutor.execute(
() -> callback.onError(new ImageCaptureException(ERROR_INVALID_CAMERA,
"Not bound to a valid Camera [" + ImageCapture.this + "]", null)));
return;
}
mImageCaptureRequestProcessor.sendRequest(new ImageCaptureRequest(
getRelativeRotation(attachedCamera), getJpegQuality(), mCropAspectRatio,
getViewPortCropRect(), callbackExecutor, callback));
}
private void lockFlashMode() {
synchronized (mLockedFlashMode) {
if (mLockedFlashMode.get() != null) {
// FlashMode is locked.
return;
}
mLockedFlashMode.set(getFlashMode());
}
}
private void unlockFlashMode() {
synchronized (mLockedFlashMode) {
Integer lockedFlashMode = mLockedFlashMode.getAndSet(null);
if (lockedFlashMode == null) {
// FlashMode is not locked yet.
return;
}
if (lockedFlashMode.intValue() != getFlashMode()) {
// Flash Mode is changed during lock session.
trySetFlashModeToCameraControl();
}
}
}
private void trySetFlashModeToCameraControl() {
synchronized (mLockedFlashMode) {
if (mLockedFlashMode.get() != null) {
// Flash Mode is locked.
return;
}
getCameraControl().setFlashMode(getFlashMode());
}
}
/**
* Gets the JPEG quality based on {@link #mCaptureMode}.
*
* <p> Range is 1-100; larger is higher quality.
*
* @return Compression quality of the captured JPEG image.
*/
@IntRange(from = 1, to = 100)
private int getJpegQuality() {
switch (mCaptureMode) {
case CAPTURE_MODE_MAXIMIZE_QUALITY:
return JPEG_QUALITY_MAXIMIZE_QUALITY_MODE;
case CAPTURE_MODE_MINIMIZE_LATENCY:
return JPEG_QUALITY_MINIMIZE_LATENCY_MODE;
default:
throw new IllegalStateException("CaptureMode " + mCaptureMode + " is invalid");
}
}
/**
* 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 ListenableFuture<ImageProxy> takePictureInternal(
@NonNull ImageCaptureRequest imageCaptureRequest) {
return CallbackToFutureAdapter.getFuture(
completer -> {
mImageReader.setOnImageAvailableListener(
(imageReader) -> {
try {
ImageProxy image = imageReader.acquireLatestImage();
if (image != null) {
if (!completer.set(image)) {
// If the future is already complete (probably be
// cancelled), then close the image.
image.close();
}
} else {
completer.setException(new IllegalStateException(
"Unable to acquire image"));
}
} catch (IllegalStateException e) {
completer.setException(e);
}
},
CameraXExecutors.mainThreadExecutor());
TakePictureState state = new TakePictureState();
ListenableFuture<Void> future = FutureChain.from(preTakePicture(state))
.transformAsync(v -> issueTakePicture(imageCaptureRequest), mExecutor);
Futures.addCallback(future,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
postTakePicture(state);
}
@Override
public void onFailure(Throwable throwable) {
postTakePicture(state);
completer.setException(throwable);
}
},
mExecutor);
completer.addCancellationListener(() -> future.cancel(true),
CameraXExecutors.directExecutor());
return "takePictureInternal";
});
}
/**
* A processor that manages and issues the pending {@link ImageCaptureRequest}s.
*
* <p>It ensures that only one single {@link ImageCaptureRequest} is in progress at a time
* and is able to process next request only when there is not over the maximum number of
* dispatched image.
*/
@VisibleForTesting
static class ImageCaptureRequestProcessor implements OnImageCloseListener {
@GuardedBy("mLock")
private final Deque<ImageCaptureRequest> mPendingRequests = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
ImageCaptureRequest mCurrentRequest = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
ListenableFuture<ImageProxy> mCurrentRequestFuture = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
int mOutstandingImages = 0;
@GuardedBy("mLock")
private final ImageCaptor mImageCaptor;
private final int mMaxImages;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mLock = new Object();
ImageCaptureRequestProcessor(int maxImages, @NonNull ImageCaptor imageCaptor) {
mMaxImages = maxImages;
mImageCaptor = imageCaptor;
}
/**
* Sends an {@link ImageCaptureRequest} to queue.
*
* @param imageCaptureRequest the image request
*/
public void sendRequest(@NonNull ImageCaptureRequest imageCaptureRequest) {
synchronized (mLock) {
mPendingRequests.offer(imageCaptureRequest);
Logger.d(TAG, String.format(Locale.US,
"Send image capture request [current, pending] = [%d, %d]",
mCurrentRequest != null ? 1 : 0, mPendingRequests.size()));
processNextRequest();
}
}
/** Cancels current processing and pending requests. */
public void cancelRequests(@NonNull Throwable throwable) {
ImageCaptureRequest currentRequest;
ListenableFuture<ImageProxy> currentRequestFuture;
List<ImageCaptureRequest> pendingRequests;
synchronized (mLock) {
currentRequest = mCurrentRequest;
mCurrentRequest = null;
currentRequestFuture = mCurrentRequestFuture;
mCurrentRequestFuture = null;
pendingRequests = new ArrayList<>(mPendingRequests);
mPendingRequests.clear();
}
if (currentRequest != null && currentRequestFuture != null) {
currentRequest.notifyCallbackError(getError(throwable), throwable.getMessage(),
throwable);
currentRequestFuture.cancel(true);
}
for (ImageCaptureRequest request : pendingRequests) {
request.notifyCallbackError(getError(throwable), throwable.getMessage(), throwable);
}
}
@Override
public void onImageClose(ImageProxy image) {
synchronized (mLock) {
mOutstandingImages--;
processNextRequest();
}
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
void processNextRequest() {
synchronized (mLock) {
// Unable to issue request if there is currently a request in flight
if (mCurrentRequest != null) {
return;
}
// Unable to issue request if the ImageReader has no available image buffer left.
if (mOutstandingImages >= mMaxImages) {
Logger.w(TAG,
"Too many acquire images. Close image to be able to process next.");
return;
}
ImageCaptureRequest imageCaptureRequest = mPendingRequests.poll();
if (imageCaptureRequest == null) {
return;
}
mCurrentRequest = imageCaptureRequest;
mCurrentRequestFuture = mImageCaptor.capture(imageCaptureRequest);
Futures.addCallback(mCurrentRequestFuture, new FutureCallback<ImageProxy>() {
@Override
public void onSuccess(@Nullable ImageProxy image) {
synchronized (mLock) {
Preconditions.checkNotNull(image);
SingleCloseImageProxy wrappedImage = new SingleCloseImageProxy(image);
wrappedImage.addOnImageCloseListener(ImageCaptureRequestProcessor.this);
mOutstandingImages++;
imageCaptureRequest.dispatchImage(wrappedImage);
mCurrentRequest = null;
mCurrentRequestFuture = null;
processNextRequest();
}
}
@Override
public void onFailure(Throwable t) {
synchronized (mLock) {
if (t instanceof CancellationException) {
// Do not trigger callback which should be done in cancelRequests()
// with a given throwable.
} else {
imageCaptureRequest.notifyCallbackError(getError(t),
t != null ? t.getMessage() : "Unknown error", t);
}
mCurrentRequest = null;
mCurrentRequestFuture = null;
processNextRequest();
}
}
}, CameraXExecutors.directExecutor());
}
}
/** An interface of an {@link ImageProxy} captor. */
interface ImageCaptor {
/**
* Captures an {@link ImageProxy} by giving a {@link ImageCaptureRequest}.
*
* @param imageCaptureRequest an {@link ImageCaptureRequest} contains required
* parameters for this capture.
* @return a {@link ListenableFuture represents the capture result. Cancellation to
* the future should cancel the capture task.
*/
@NonNull
ListenableFuture<ImageProxy> capture(@NonNull ImageCaptureRequest imageCaptureRequest);
}
}
@NonNull
@Override
public String toString() {
return TAG + ":" + getName();
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ImageCaptureError
static 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 onDetached() {
abortImageCaptureRequests();
clearPipeline();
mExecutor.shutdown();
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
public void onAttached() {
ImageCaptureConfig useCaseConfig = (ImageCaptureConfig) getUseCaseConfig();
CaptureConfig.Builder captureBuilder = CaptureConfig.Builder.createFrom(useCaseConfig);
mCaptureConfig = captureBuilder.build();
// Retrieve camera specific settings.
mCaptureProcessor = useCaseConfig.getCaptureProcessor(null);
mMaxCaptureStages = useCaseConfig.getMaxCaptureStages(MAX_IMAGES);
mCaptureBundle = useCaseConfig.getCaptureBundle(
CaptureBundles.singleDefaultCaptureBundle());
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());
}
});
}
/**
* {@inheritDoc}
*
* @hide
*/
@NonNull
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
mSessionConfigBuilder = createPipeline(getCameraId(),
(ImageCaptureConfig) getUseCaseConfig(), suggestedResolution);
updateSessionConfig(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 suggestedResolution;
}
/**
* 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) {
lockFlashMode();
return FutureChain.from(getPreCaptureStateIfNeeded())
.transformAsync(captureResult -> {
state.mPreCaptureState = captureResult;
triggerAfIfNeeded(state);
if (isAePrecaptureRequired(state)) {
// trigger AE precapture and await the result.
return triggerAePrecapture(state);
}
return Futures.immediateFuture(null);
}, mExecutor)
.transformAsync(v -> 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) {
cancelAfAeTrigger(state);
unlockFlashMode();
}
/**
* 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) {
if (Logger.isDebugEnabled(TAG)) {
Logger.d(TAG, "preCaptureState, AE=" + captureResult.getAeState()
+ " AF =" + captureResult.getAfState()
+ " AWB=" + captureResult.getAwbState());
}
return captureResult;
}
});
}
return Futures.immediateFuture(null);
}
boolean isAePrecaptureRequired(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) {
// Skip the 3A converged check if enableCheck3AConverged is false and AE precapture is
// not triggered.
if (!mEnableCheck3AConverged && !state.mIsAePrecaptureTriggered) {
return Futures.immediateFuture(false);
}
return mSessionCallbackChecker.checkCaptureResult(
new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
@Override
public Boolean check(@NonNull CameraCaptureResult captureResult) {
if (Logger.isDebugEnabled(TAG)) {
Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
+ " AF =" + captureResult.getAfState()
+ " AWB=" + captureResult.getAwbState());
}
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.FLASH_REQUIRED
|| 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) {
Logger.d(TAG, "triggerAf");
state.mIsAfTriggered = true;
ListenableFuture<CameraCaptureResult> future = getCameraControl().triggerAf();
// Add listener to avoid FutureReturnValueIgnored error.
future.addListener(() -> {
}, CameraXExecutors.directExecutor());
}
/** Issues a request to start auto exposure scan. */
ListenableFuture<CameraCaptureResult> triggerAePrecapture(TakePictureState state) {
Logger.d(TAG, "triggerAePrecapture");
state.mIsAePrecaptureTriggered = true;
return getCameraControl().triggerAePrecapture();
}
/** Issues a request to cancel auto focus and/or auto exposure scan. */
void cancelAfAeTrigger(TakePictureState state) {
if (!state.mIsAfTriggered && !state.mIsAePrecaptureTriggered) {
return;
}
getCameraControl()
.cancelAfAeTrigger(state.mIsAfTriggered, state.mIsAePrecaptureTriggered);
state.mIsAfTriggered = false;
state.mIsAePrecaptureTriggered = false;
}
/**
* Initiates a set of captures that will be used to create the output of
* {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)} and its variants.
*
* <p> This returns a {@link ListenableFuture} whose completion indicates that the
* captures are finished. Before the future is complete, any modification to the camera state
* such as 3A could affect the result of the captures. After the future is complete, then it
* is safe to reset or modify the 3A state.
*/
ListenableFuture<Void> issueTakePicture(@NonNull ImageCaptureRequest imageCaptureRequest) {
Logger.d(TAG, "issueTakePicture");
final List<ListenableFuture<Void>> futureList = new ArrayList<>();
final List<CaptureConfig> captureConfigs = new ArrayList<>();
String tagBundleKey = null;
CaptureBundle captureBundle;
if (mProcessingImageReader != 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"));
}
mProcessingImageReader.setCaptureBundle(captureBundle);
tagBundleKey = mProcessingImageReader.getTagBundleKey();
} 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());
// Add the default implementation options of ImageCapture
builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
builder.addAllCameraCaptureCallbacks(
mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
builder.addSurface(mDeferrableSurface);
// Add the dynamic implementation options of ImageCapture
builder.addImplementationOption(CaptureConfig.OPTION_ROTATION,
imageCaptureRequest.mRotationDegrees);
builder.addImplementationOption(CaptureConfig.OPTION_JPEG_QUALITY,
imageCaptureRequest.mJpegQuality);
// Add the implementation options required by the CaptureStage
builder.addImplementationOptions(
captureStage.getCaptureConfig().getImplementationOptions());
// Use CaptureBundle object as the key for TagBundle
if (tagBundleKey != null) {
builder.addTag(tagBundleKey, captureStage.getId());
}
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);
}
getCameraControl().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(ImageCaptureException)}.
*
* @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_UNKNOWN, 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. */
void onImageSaved(@NonNull OutputFileResults outputFileResults);
/**
* Called when an error occurs while attempting to save an image.
*
* @param exception An {@link ImageCaptureException} that contains the type of error, the
* error message and the throwable that caused it.
*/
void onError(@NonNull ImageCaptureException exception);
}
/**
* 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 exception An {@link ImageCaptureException} that contains the type of error, the
* error message and the throwable that caused it.
*/
public void onError(@NonNull final ImageCaptureException exception) {
}
}
/**
* 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> {
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
private static final ImageCaptureConfig DEFAULT_CONFIG;
static {
Builder builder = new Builder().setSurfaceOccupancyPriority(
DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@NonNull
@Override
public ImageCaptureConfig getConfig() {
return DEFAULT_CONFIG;
}
}
/**
* Options for saving newly captured image.
*
* <p> this class is used to configure save location and metadata. Save location can be
* either a {@link File}, {@link MediaStore} or a {@link OutputStream}. The metadata will be
* stored with the saved image. For JPEG this will be included in the EXIF.
*/
public static final class OutputFileOptions {
// 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 final File mFile;
@Nullable
private final ContentResolver mContentResolver;
@Nullable
private final Uri mSaveCollection;
@Nullable
private final ContentValues mContentValues;
@Nullable
private final OutputStream mOutputStream;
@NonNull
private final Metadata mMetadata;
OutputFileOptions(@Nullable File file,
@Nullable ContentResolver contentResolver,
@Nullable Uri saveCollection,
@Nullable ContentValues contentValues,
@Nullable OutputStream outputStream,
@Nullable Metadata metadata) {
mFile = file;
mContentResolver = contentResolver;
mSaveCollection = saveCollection;
mContentValues = contentValues;
mOutputStream = outputStream;
mMetadata = metadata == null ? EMPTY_METADATA : metadata;
}
@Nullable
File getFile() {
return mFile;
}
@Nullable
ContentResolver getContentResolver() {
return mContentResolver;
}
@Nullable
Uri getSaveCollection() {
return mSaveCollection;
}
@Nullable
ContentValues getContentValues() {
return mContentValues;
}
@Nullable
OutputStream getOutputStream() {
return mOutputStream;
}
/**
* Exposed internally so that CameraView can overwrite the flip horizontal flag for front
* camera. External core API users shouldn't need this be cause they are the one who
* created the {@link Metadata}.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Metadata getMetadata() {
return mMetadata;
}
/**
* Builder class for {@link OutputFileOptions}.
*/
public static final class Builder {
@Nullable
private File mFile;
@Nullable
private ContentResolver mContentResolver;
@Nullable
private Uri mSaveCollection;
@Nullable
private ContentValues mContentValues;
@Nullable
private OutputStream mOutputStream;
@Nullable
private Metadata mMetadata;
/**
* Creates options to write captured image to a {@link File}.
*
* @param file save location of the image.
*/
public Builder(@NonNull File file) {
mFile = file;
}
/**
* Creates options to write captured image to {@link MediaStore}.
*
* Example:
*
* <pre>{@code
*
* ContentValues contentValues = new ContentValues();
* contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_IMAGE");
* contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
*
* ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(
* getContentResolver(),
* MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
* contentValues).build();
*
* }</pre>
*
* @param contentResolver to access {@link MediaStore}
* @param saveCollection The URL of the table to insert into.
* @param contentValues to be included in the created image file.
*/
public Builder(@NonNull ContentResolver contentResolver,
@NonNull Uri saveCollection,
@NonNull ContentValues contentValues) {
mContentResolver = contentResolver;
mSaveCollection = saveCollection;
mContentValues = contentValues;
}
/**
* Creates options that write captured image to a {@link OutputStream}.
*
* @param outputStream save location of the image.
*/
public Builder(@NonNull OutputStream outputStream) {
mOutputStream = outputStream;
}
/**
* Sets the metadata to be stored with the saved image.
*
* <p> For JPEG this will be included in the EXIF.
*
* @param metadata Metadata to be stored with the saved image. For JPEG this will
* be included in the EXIF.
*/
@NonNull
public Builder setMetadata(@NonNull Metadata metadata) {
mMetadata = metadata;
return this;
}
/**
* Builds {@link OutputFileOptions}.
*/
@NonNull
public OutputFileOptions build() {
return new OutputFileOptions(mFile, mContentResolver, mSaveCollection,
mContentValues, mOutputStream, mMetadata);
}
}
}
/**
* Info about the saved image file.
*/
public static class OutputFileResults {
@Nullable
private Uri mSavedUri;
OutputFileResults(@Nullable Uri savedUri) {
mSavedUri = savedUri;
}
/**
* Returns the {@link Uri} of the saved file.
*
* <p> This field is only returned if the {@link OutputFileOptions} is backed by
* {@link MediaStore} constructed with
*
* {@link androidx.camera.core.ImageCapture.OutputFileOptions.Builder
* #Builder(ContentResolver, Uri, ContentValues)}.
*/
@Nullable
public Uri getSavedUri() {
return mSavedUri;
}
}
/** 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;
}
/**
* 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);
}
}
@VisibleForTesting
static class ImageCaptureRequest {
@RotationValue
final int mRotationDegrees;
@IntRange(from = 1, to = 100)
final int mJpegQuality;
private final Rational mTargetRatio;
@NonNull
private final Executor mListenerExecutor;
@NonNull
private final OnImageCapturedCallback mCallback;
AtomicBoolean mDispatched = new AtomicBoolean(false);
private final Rect mViewPortCropRect;
/**
* @param rotationDegrees The degrees to rotate the image buffer from sensor
* coordinates into the final output coordinate space.
* @param targetRatio The aspect ratio of the image in final output coordinate space.
* This must be a non-negative, non-zero value.
* @throws IllegalArgumentException If targetRatio is not a valid value.
*/
ImageCaptureRequest(
@RotationValue int rotationDegrees,
@IntRange(from = 1, to = 100) int jpegQuality,
Rational targetRatio,
@Nullable Rect viewPortCropRect,
@NonNull Executor executor,
@NonNull OnImageCapturedCallback callback) {
mRotationDegrees = rotationDegrees;
mJpegQuality = jpegQuality;
if (targetRatio != null) {
Preconditions.checkArgument(!targetRatio.isZero(), "Target ratio cannot be zero");
Preconditions.checkArgument(targetRatio.floatValue() > 0, "Target ratio must be "
+ "positive");
}
mTargetRatio = targetRatio;
mViewPortCropRect = viewPortCropRect;
mListenerExecutor = executor;
mCallback = callback;
}
void dispatchImage(final ImageProxy image) {
// Check to make sure image hasn't been already dispatched or error has been notified
if (!mDispatched.compareAndSet(false, true)) {
image.close();
return;
}
Size dispatchResolution = null;
int dispatchRotation = 0;
if (image.getFormat() == ImageFormat.JPEG) {
// JPEG needs to have rotation/crop based on the EXIF
try {
ImageProxy.PlaneProxy[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
Exif exif;
buffer.rewind();
byte[] data = new byte[buffer.capacity()];
buffer.get(data);
exif = Exif.createFromInputStream(new ByteArrayInputStream(data));
buffer.rewind();
dispatchResolution = new Size(exif.getWidth(), exif.getHeight());
dispatchRotation = exif.getRotation();
} catch (IOException e) {
notifyCallbackError(ERROR_FILE_IO, "Unable to parse JPEG exif", e);
image.close();
return;
}
} else {
// All other formats take the rotation based simply on the target rotation
dispatchRotation = mRotationDegrees;
}
// Construct the ImageProxy with the updated rotation & crop for the output
ImageInfo imageInfo = ImmutableImageInfo.create(
image.getImageInfo().getTagBundle(),
image.getImageInfo().getTimestamp(), dispatchRotation);
final ImageProxy dispatchedImageProxy = new SettableImageProxy(image,
dispatchResolution,
imageInfo);
// Update the crop rect aspect ratio after it has been rotated into the buffer
// orientation
if (mViewPortCropRect != null) {
// If Viewport is present, use the crop rect based on Viewport.
dispatchedImageProxy.setCropRect(mViewPortCropRect);
} else if (mTargetRatio != null) {
// Fall back to crop aspect ratio if view port is not available.
Rational dispatchRatio = mTargetRatio;
if ((dispatchRotation % 180) != 0) {
dispatchRatio = new Rational(
/* invert the ratio numerator=*/ mTargetRatio.getDenominator(),
/* invert the ratio denominator=*/ mTargetRatio.getNumerator());
}
Size sourceSize = new Size(dispatchedImageProxy.getWidth(),
dispatchedImageProxy.getHeight());
if (ImageUtil.isAspectRatioValid(sourceSize, dispatchRatio)) {
dispatchedImageProxy.setCropRect(
ImageUtil.computeCropRectFromAspectRatio(sourceSize,
dispatchRatio));
}
}
try {
mListenerExecutor.execute(() -> {
mCallback.onCaptureSuccess(dispatchedImageProxy);
});
} catch (RejectedExecutionException e) {
Logger.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) {
// Check to make sure image hasn't been already dispatched or error has been notified
if (!mDispatched.compareAndSet(false, true)) {
return;
}
try {
mListenerExecutor.execute(() -> mCallback.onError(
new ImageCaptureException(imageCaptureError, message, cause)));
} catch (RejectedExecutionException e) {
Logger.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>,
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.");
}
// Update the input format base on the other options set (mainly whether processing
// is done)
Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
if (bufferFormat != null) {
Preconditions.checkArgument(
getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) == null,
"Cannot set buffer format with CaptureProcessor defined.");
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, bufferFormat);
} else {
if (getMutableConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null) != null) {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
} else {
getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
}
}
ImageCapture imageCapture = new ImageCapture(getUseCaseConfig());
// Makes the crop aspect ratio match the target resolution setting as what mentioned
// in javadoc of setTargetResolution(). When the target resolution is set, {@link
// ImageCapture#setCropAspectRatio(Rational)} will be automatically called to set
// corresponding value.
Size targetResolution = getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION,
null);
if (targetResolution != null) {
imageCapture.setCropAspectRatio(new Rational(targetResolution.getWidth(),
targetResolution.getHeight()));
}
Preconditions.checkArgument(
getMutableConfig().retrieveOption(OPTION_MAX_CAPTURE_STAGES, MAX_IMAGES) >= 1,
"Maximum outstanding image count must be at least 1");
Preconditions.checkNotNull(getMutableConfig().retrieveOption(OPTION_IO_EXECUTOR,
CameraXExecutors.ioExecutor()), "The IO executor can't be null");
if (getMutableConfig().containsOption(OPTION_FLASH_MODE)) {
int flashMode = getMutableConfig().retrieveOption(OPTION_FLASH_MODE);
if (flashMode != FLASH_MODE_AUTO && flashMode != FLASH_MODE_ON
&& flashMode != FLASH_MODE_OFF) {
throw new IllegalArgumentException(
"The flash mode is not allowed to set: " + flashMode);
}
}
return imageCapture;
}
/**
* 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. Value is {@link #FLASH_MODE_AUTO},
* {@link #FLASH_MODE_ON}, or {@link #FLASH_MODE_OFF}.
* @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 ImageOutputConfig.Builder default methods
/**
* 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 and the resulting
* aspect ratio may not be exactly as requested.
*
* <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.
*
* <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 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. The use case is fully created once it has been attached to a camera.
*
* @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 in the coordinate frame after
* rotating the supported sizes by the 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>When the target resolution is set,
* {@link ImageCapture#setCropAspectRatio(Rational)} will be automatically called to set
* corresponding value. Such that the output image will be cropped into the desired
* aspect ratio.
*
* <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);
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;
}
/** @hide */
@NonNull
@RestrictTo(Scope.LIBRARY_GROUP)
public Builder setImageReaderProxyProvider(
@NonNull ImageReaderProxyProvider imageReaderProxyProvider) {
getMutableConfig().insertOption(OPTION_IMAGE_READER_PROXY_PROVIDER,
imageReaderProxyProvider);
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(OutputFileOptions, 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 setCameraSelector(@NonNull CameraSelector cameraSelector) {
getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
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;
}
}
}