/*
* 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 android.content.Context;
import android.os.Handler;
import android.util.Size;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Main interface for accessing CameraX library.
*
* <p>This is a singleton class that is responsible for managing the set of camera
* instances and {@link UseCase} instances that exist. A {@link UseCase} is bound to {@link
* LifecycleOwner} so that the lifecycle is used to control the use case. There are 3 distinct sets
* lifecycle states to be aware of.
*
* <p>When the lifecycle is in the STARTED or RESUMED states the cameras are opened asynchronously
* and made ready for capturing. Data capture starts when triggered by the bound {@link UseCase}.
*
* <p>When the lifecycle is in the CREATED state any cameras with no {@link UseCase} attached
* will be closed asynchronously.
*
* <p>When the lifecycle transitions to the DESTROYED state the {@link UseCase} will be unbound.
* A {@link #bindToLifecycle(LifecycleOwner, UseCase...)} when the lifecycle is already in the
* DESTROYED state will fail. A call to {@link #bindToLifecycle(LifecycleOwner, UseCase...)}
* will need to be made with another lifecycle to rebind the {@link UseCase} that has been
* unbound.
*
* <pre>{@code
* public void setup() {
* // Initialize UseCase
* useCase = ...;
*
* // UseCase binding event
* CameraX.bindToLifecycle(lifecycleOwner, useCase);
*
* // Make calls on useCase
* }
*
* public void operateOnUseCase() {
* if (CameraX.isBound(useCase)) {
* // Make calls on useCase
* }
* }
*
* public void prematureTearDown() {
* // Not required, but only if we want to remove it before the lifecycle automatically removes
* // the use case
* CameraX.unbind(useCase);
* }
* }</pre>
*
* <p>All operations on a use case, including binding and unbinding, should be done on the main
* thread, because lifecycle events are triggered on main thread. By only accessing the use case on
* the main thread it is a guaranteed that the use case will not be unbound in the middle of a
* method call.
*/
@MainThread
public final class CameraX {
private static final CameraX INSTANCE = new CameraX();
final CameraRepository mCameraRepository = new CameraRepository();
private final AtomicBoolean mInitialized = new AtomicBoolean(false);
private final UseCaseGroupRepository mUseCaseGroupRepository = new UseCaseGroupRepository();
private final ErrorHandler mErrorHandler = new ErrorHandler();
private CameraFactory mCameraFactory;
private CameraDeviceSurfaceManager mSurfaceManager;
private UseCaseConfigFactory mDefaultConfigFactory;
private Context mContext;
/** Prevents construction. */
private CameraX() {
}
/**
* Binds the collection of {@link UseCase} to a {@link LifecycleOwner}.
*
* <p>If the lifecycleOwner contains a {@link Lifecycle} that is already
* in the STARTED state or greater than the created use cases will attach to the cameras and
* trigger the appropriate notifications. This will generally cause a temporary glitch in the
* camera as part of the reset process. This will also help to calculate suggested resolutions
* depending on the use cases bound to the {@link Lifecycle}. If the use cases are bound
* separately, it will find the supported resolution with the priority depending on the
* binding sequence. If the use cases are bound with a single call, it will find the
* supported resolution with the priority in sequence of ImageCapture, VideoCapture, Preview
* and then ImageAnalysis. What resolutions can be supported will depend on the camera device
* hardware level that there are some default guaranteed resolutions listed in
* {@link android.hardware.camera2.CameraDevice#createCaptureSession}.
*
* <p> Currently up to 3 use cases may be bound at any time. Exceeding this will throw an
* IllegalArgumentException.
*
* @param lifecycleOwner The lifecycleOwner which controls the lifecycle transitions of the use
* cases.
* @param useCases The use cases to bind to a lifecycle.
* @throws IllegalArgumentException If the use case has already been bound to another lifecycle.
*/
public static void bindToLifecycle(LifecycleOwner lifecycleOwner, UseCase... useCases) {
UseCaseGroupLifecycleController useCaseGroupLifecycleController =
INSTANCE.getOrCreateUseCaseGroup(lifecycleOwner);
UseCaseGroup useCaseGroupToBind = useCaseGroupLifecycleController.getUseCaseGroup();
Collection<UseCaseGroupLifecycleController> controllers =
INSTANCE.mUseCaseGroupRepository.getUseCaseGroups();
for (UseCase useCase : useCases) {
for (UseCaseGroupLifecycleController controller : controllers) {
UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
if (useCaseGroup.contains(useCase) && useCaseGroup != useCaseGroupToBind) {
throw new IllegalStateException(
String.format(
"Use case %s already bound to a different lifecycle.",
useCase));
}
}
}
calculateSuggestedResolutions(useCases);
for (UseCase useCase : useCases) {
useCaseGroupToBind.addUseCase(useCase);
for (String cameraId : useCase.getAttachedCameraIds()) {
attach(cameraId, useCase);
}
}
useCaseGroupLifecycleController.notifyState();
}
/**
* Returns true if the {@link UseCase} is bound to a lifecycle. Otherwise returns false.
*
* <p>It is not strictly necessary to check if a use case is bound or not. As long as the
* lifecycle it was bound to has not entered a DESTROYED state or if it hasn't been unbound by
* {@link #unbind(UseCase...)} or {@link #unbindAll()} then the use case will remain bound.
* A use case will not be unbound in the middle of a method call as long as it is running on the
* main thread. This is because a lifecycle events will only automatically triggered on the main
* thread.
*/
public static boolean isBound(UseCase useCase) {
Collection<UseCaseGroupLifecycleController> controllers =
INSTANCE.mUseCaseGroupRepository.getUseCaseGroups();
for (UseCaseGroupLifecycleController controller : controllers) {
UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
if (useCaseGroup.contains(useCase)) {
return true;
}
}
return false;
}
/**
* Unbinds all specified use cases from the lifecycle and removes them from CameraX.
*
* <p>This will initiate a close of every open camera which has zero {@link UseCase}
* associated with it at the end of this call.
*
* <p>If a use case in the argument list is not bound, then then it is simply ignored.
*
* @param useCases The collection of use cases to remove.
*/
public static void unbind(UseCase... useCases) {
Collection<UseCaseGroupLifecycleController> useCaseGroups =
INSTANCE.mUseCaseGroupRepository.getUseCaseGroups();
Map<String, List<UseCase>> detachingUseCaseMap = new HashMap<>();
for (UseCase useCase : useCases) {
for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
if (useCaseGroup.removeUseCase(useCase)) {
// Saves all detaching use cases and detach them at once.
for (String cameraId : useCase.getAttachedCameraIds()) {
List<UseCase> useCasesOnCameraId = detachingUseCaseMap.get(cameraId);
if (useCasesOnCameraId == null) {
useCasesOnCameraId = new ArrayList<>();
detachingUseCaseMap.put(cameraId, useCasesOnCameraId);
}
useCasesOnCameraId.add(useCase);
}
}
}
}
for (String cameraId : detachingUseCaseMap.keySet()) {
detach(cameraId, detachingUseCaseMap.get(cameraId));
}
for (UseCase useCase : useCases) {
useCase.clear();
}
}
/**
* Unbinds all use cases from the lifecycle and removes them from CameraX.
*
* <p>This will initiate a close of every currently open camera.
*/
public static void unbindAll() {
Collection<UseCaseGroupLifecycleController> useCaseGroups =
INSTANCE.mUseCaseGroupRepository.getUseCaseGroups();
List<UseCase> useCases = new ArrayList<>();
for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
useCases.addAll(useCaseGroup.getUseCases());
}
unbind(useCases.toArray(new UseCase[0]));
}
/**
* Returns the camera id for a camera with the specified lens facing.
*
* <p>This only gives the first (primary) camera found with the specified facing.
*
* @param lensFacing the lens facing of the camera
* @return the cameraId if camera exists or {@code null} if no camera with specified facing
* exists
* @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
* insufficient permissions.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public static String getCameraWithLensFacing(LensFacing lensFacing)
throws CameraInfoUnavailableException {
return INSTANCE.getCameraFactory().cameraIdForLensFacing(lensFacing);
}
/**
* Returns the camera info for the camera with the given camera id.
*
* @param cameraId the internal id of the camera
* @return the camera info if it can be retrieved for the given id.
* @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
* insufficient permissions.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public static CameraInfo getCameraInfo(String cameraId) throws CameraInfoUnavailableException {
return INSTANCE.getCameraRepository().getCamera(cameraId).getCameraInfo();
}
/**
* Returns the {@link CameraDeviceSurfaceManager} which can be used to query for valid surface
* configurations.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static CameraDeviceSurfaceManager getSurfaceManager() {
return INSTANCE.getCameraDeviceSurfaceManager();
}
/**
* Returns the default configuration for the given use case configuration type.
*
* <p>The options contained in this configuration serve as fallbacks if they are not included in
* the user-provided configuration used to create a use case.
*
* @param configType the configuration type
* @param lensFacing The {@link LensFacing} that the default configuration will target to.
* @return the default configuration for the given configuration type
* @throws IllegalStateException if Camerax has not yet been initialized.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public static <C extends UseCaseConfig<?>> C getDefaultUseCaseConfig(
Class<C> configType, LensFacing lensFacing) {
return INSTANCE.getDefaultConfigFactory().getConfig(configType, lensFacing);
}
/**
* Sets an {@link ErrorListener} which will get called anytime a CameraX specific error is
* encountered.
*
* @param errorListener the listener which will get all the error messages. If this is set to
* {@code null} then the default error listener will be set.
* @param handler the handler for the thread to run the error handling on. If this is
* set to
* {@code null} then it will default to run on the main thread.
*/
public static void setErrorListener(ErrorListener errorListener, Handler handler) {
INSTANCE.mErrorHandler.setErrorListener(errorListener, handler);
}
/**
* Posts an error which can be handled by the {@link ErrorListener}.
*
* @param errorCode the type of error that occurred
* @param message the associated message with more details of the error
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static void postError(ErrorCode errorCode, String message) {
INSTANCE.mErrorHandler.postError(errorCode, message);
}
/**
* Initializes CameraX with the given context and application configuration.
*
* <p>The context enables CameraX to obtain access to necessary services, including the camera
* service. For example, the context can be provided by the application.
*
* @param context to attach
* @param appConfig configuration options for this application session.
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static void init(Context context, AppConfig appConfig) {
INSTANCE.initInternal(context, appConfig);
}
/**
* Returns the context used for CameraX.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static Context getContext() {
return INSTANCE.mContext;
}
/**
* Returns true if CameraX is initialized.
*
* <p>Any previous call to {@link #init(Context, AppConfig)} would have initialized
* CameraX.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public static boolean isInitialized() {
return INSTANCE.mInitialized.get();
}
/**
* Registers the callbacks for the {@link BaseCamera} to the {@link UseCase}.
*
* @param cameraId the id for the {@link BaseCamera}
* @param useCase the use case to register the callback for
*/
private static void attach(String cameraId, UseCase useCase) {
BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
if (camera == null) {
throw new IllegalArgumentException("Invalid camera: " + cameraId);
}
useCase.addStateChangeListener(camera);
useCase.attachCameraControl(cameraId, camera.getCameraControl());
}
/**
* Removes the callbacks registered by the {@link BaseCamera} to the {@link UseCase}.
*
* @param cameraId the id for the {@link BaseCamera}
* @param useCases the list of use case to remove the callback from.
*/
private static void detach(String cameraId, List<UseCase> useCases) {
BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
if (camera == null) {
throw new IllegalArgumentException("Invalid camera: " + cameraId);
}
for (UseCase useCase : useCases) {
useCase.removeStateChangeListener(camera);
useCase.detachCameraControl(cameraId);
}
camera.removeOnlineUseCase(useCases);
}
private static void calculateSuggestedResolutions(UseCase... useCases) {
Collection<UseCaseGroupLifecycleController> controllers =
INSTANCE.mUseCaseGroupRepository.getUseCaseGroups();
Map<String, List<UseCase>> originalCameraIdUseCaseMap = new HashMap<>();
Map<String, List<UseCase>> newCameraIdUseCaseMap = new HashMap<>();
// Collect original use cases for different camera devices
for (UseCaseGroupLifecycleController controller : controllers) {
UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
for (UseCase useCase : useCaseGroup.getUseCases()) {
for (String cameraId : useCase.getAttachedCameraIds()) {
List<UseCase> useCaseList = originalCameraIdUseCaseMap.get(cameraId);
if (useCaseList == null) {
useCaseList = new ArrayList<>();
originalCameraIdUseCaseMap.put(cameraId, useCaseList);
}
useCaseList.add(useCase);
}
}
}
// Collect new use cases for different camera devices
for (UseCase useCase : useCases) {
String cameraId = null;
LensFacing lensFacing =
useCase.getUseCaseConfig()
.retrieveOption(CameraDeviceConfig.OPTION_LENS_FACING);
try {
cameraId = getCameraWithLensFacing(lensFacing);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid camera lens facing: " + lensFacing, e);
}
List<UseCase> useCaseList = newCameraIdUseCaseMap.get(cameraId);
if (useCaseList == null) {
useCaseList = new ArrayList<>();
newCameraIdUseCaseMap.put(cameraId, useCaseList);
}
useCaseList.add(useCase);
}
// Get suggested resolutions and update the use case session configuration
for (String cameraId : newCameraIdUseCaseMap.keySet()) {
Map<UseCase, Size> suggestResolutionsMap =
getSurfaceManager()
.getSuggestedResolutions(
cameraId,
originalCameraIdUseCaseMap.get(cameraId),
newCameraIdUseCaseMap.get(cameraId));
for (UseCase useCase : useCases) {
Size resolution = suggestResolutionsMap.get(useCase);
Map<String, Size> suggestedCameraSurfaceResolutionMap = new HashMap<>();
suggestedCameraSurfaceResolutionMap.put(cameraId, resolution);
useCase.updateSuggestedResolution(suggestedCameraSurfaceResolutionMap);
}
}
}
/**
* Returns the {@link CameraFactory} instance.
*
* @throws IllegalStateException if the {@link CameraFactory} has not been set, due to being
* uninitialized.
*/
private CameraFactory getCameraFactory() {
if (mCameraFactory == null) {
throw new IllegalStateException("CameraX not initialized yet.");
}
return mCameraFactory;
}
/**
* Returns the {@link CameraDeviceSurfaceManager} instance.
*
* @throws IllegalStateException if the {@link CameraDeviceSurfaceManager} has not been set, due
* to being uninitialized.
*/
private CameraDeviceSurfaceManager getCameraDeviceSurfaceManager() {
if (mSurfaceManager == null) {
throw new IllegalStateException("CameraX not initialized yet.");
}
return mSurfaceManager;
}
private UseCaseConfigFactory getDefaultConfigFactory() {
if (mDefaultConfigFactory == null) {
throw new IllegalStateException("CameraX not initialized yet.");
}
return mDefaultConfigFactory;
}
private void initInternal(Context context, AppConfig appConfig) {
if (mInitialized.getAndSet(true)) {
return;
}
mContext = context.getApplicationContext();
mCameraFactory = appConfig.getCameraFactory(null);
if (mCameraFactory == null) {
throw new IllegalStateException(
"Invalid app configuration provided. Missing CameraFactory.");
}
mSurfaceManager = appConfig.getDeviceSurfaceManager(null);
if (mSurfaceManager == null) {
throw new IllegalStateException(
"Invalid app configuration provided. Missing CameraDeviceSurfaceManager.");
}
mDefaultConfigFactory = appConfig.getUseCaseConfigRepository(null);
if (mDefaultConfigFactory == null) {
throw new IllegalStateException(
"Invalid app configuration provided. Missing UseCaseConfigFactory.");
}
mCameraRepository.init(mCameraFactory);
}
private UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
return mUseCaseGroupRepository.getOrCreateUseCaseGroup(
lifecycleOwner, new UseCaseGroupRepository.UseCaseGroupSetup() {
@Override
public void setup(UseCaseGroup useCaseGroup) {
useCaseGroup.setListener(mCameraRepository);
}
});
}
private CameraRepository getCameraRepository() {
return mCameraRepository;
}
/** The types of error states that can occur. */
public enum ErrorCode {
/** The camera has moved into an unexpected state from which it can not recover from. */
CAMERA_STATE_INCONSISTENT,
/** A {@link UseCase} has encountered an error from which it can not recover from. */
USE_CASE_ERROR
}
/** The direction the camera faces relative to device screen. */
public enum LensFacing {
/** A camera on the device facing the same direction as the device's screen. */
FRONT,
/** A camera on the device facing the opposite direction as the device's screen. */
BACK
}
/** Listener called whenever an error condition occurs within CameraX. */
public interface ErrorListener {
/**
* Called whenever an error occurs within CameraX.
*
* @param error the type of error that occurred
* @param message detailed message of the error condition
*/
void onError(ErrorCode error, String message);
}
}