/*
* Copyright 2020 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.impl;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.camera.core.Camera;
import androidx.camera.core.Logger;
import androidx.core.util.Preconditions;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
/**
* A registry that tracks the state of cameras.
*
* <p>The registry tracks internally how many cameras are open and how many are available to open.
* Cameras that are in a {@link CameraInternal.State#PENDING_OPEN} state can be notified when
* there is a slot available to open a camera.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class CameraStateRegistry {
private static final String TAG = "CameraStateRegistry";
private final StringBuilder mDebugString = new StringBuilder();
private final Object mLock = new Object();
private final int mMaxAllowedOpenedCameras;
@GuardedBy("mLock")
private final Map<Camera, CameraRegistration> mCameraStates = new HashMap<>();
@GuardedBy("mLock")
private int mAvailableCameras;
/**
* Creates a new registry with a limit of {@code maxAllowedOpenCameras} allowed to be opened.
*
* @param maxAllowedOpenedCameras The limit for number of simultaneous open cameras.
*/
public CameraStateRegistry(int maxAllowedOpenedCameras) {
mMaxAllowedOpenedCameras = maxAllowedOpenedCameras;
synchronized ("mLock") {
mAvailableCameras = mMaxAllowedOpenedCameras;
}
}
/**
* Registers a camera with the registry.
*
* <p>Once registered, the camera's state must be updated through
* {@link #markCameraState(Camera, CameraInternal.State)}.
*
* <p>Before attempting to open a camera, {@link #tryOpenCamera(Camera)} must be called and
* callers should only continue to open the camera if it returns {@code true}.
*
* <p>Cameras will be automatically unregistered when they are marked as being in a
* {@link CameraInternal.State#RELEASED} state.
*
* @param camera The camera to register.
*/
public void registerCamera(@NonNull Camera camera, @NonNull Executor notifyExecutor,
@NonNull OnOpenAvailableListener cameraAvailableListener) {
synchronized (mLock) {
Preconditions.checkState(!mCameraStates.containsKey(camera), "Camera is "
+ "already registered: " + camera);
mCameraStates.put(camera,
new CameraRegistration(null, notifyExecutor, cameraAvailableListener));
}
}
/**
* Should be called before attempting to actually open a camera.
*
* <p>This must be called before attempting to open a camera. If too many cameras are already
* open, then this will return {@code false}, and the caller should not attempt to open the
* camera. Instead, the caller should mark its state as
* {@link CameraInternal.State#PENDING_OPEN} with
* {@link #markCameraState(Camera, CameraInternal.State)}, and the listener registered with
* {@link #registerCamera(Camera, Executor, OnOpenAvailableListener)} will be notified when a
* camera becomes available. At that time, the caller should attempt to call this method again.
*
* @return {@code true} if it is safe to open the camera. If this returns {@code true}, it is
* assumed the camera is now in an {@link CameraInternal.State#OPENING} state, and the
* available camera count will be reduced by 1.
*/
public boolean tryOpenCamera(@NonNull Camera camera) {
synchronized (mLock) {
CameraRegistration registration = Preconditions.checkNotNull(mCameraStates.get(camera),
"Camera must first be registered with registerCamera()");
boolean success = false;
if (Logger.isDebugEnabled(TAG)) {
mDebugString.setLength(0);
mDebugString.append(String.format(Locale.US, "tryOpenCamera(%s) [Available "
+ "Cameras: %d, Already Open: %b (Previous state: %s)]",
camera, mAvailableCameras, isOpen(registration.getState()),
registration.getState()));
}
if (mAvailableCameras > 0 || isOpen(registration.getState())) {
// Set state directly to OPENING.
registration.setState(CameraInternal.State.OPENING);
success = true;
}
if (Logger.isDebugEnabled(TAG)) {
mDebugString.append(
String.format(Locale.US, " --> %s", success ? "SUCCESS" : "FAIL"));
Logger.d(TAG, mDebugString.toString());
}
if (success) {
// Successfully opening a camera should only make the total available count go
// down, so no need to notify cameras in a PENDING_OPEN state.
recalculateAvailableCameras();
}
return success;
}
}
/**
* Mark the state of a registered camera.
*
* <p>This is used to track the states of all cameras in order to determine how many cameras
* are available to be opened.
*
* @param camera Registered camera whose state is being set
* @param state New state of the registered camera
*/
public void markCameraState(@NonNull Camera camera, @NonNull CameraInternal.State state) {
markCameraState(camera, state, true);
}
/**
* Mark the state of a registered camera.
*
* <p>This is used to track the states of all cameras in order to determine how many cameras
* are available to be opened.
*
* <p>If a camera slot if found to be available for opening during the execution of this
* method, the caller will not be notified of it if {@code notifyImmediately} is set to
* {@code false}. This can be useful if a camera moves its state to
* {@link CameraInternal.State#PENDING_OPEN} but doesn't wish to be opened even if a camera
* slot is available for opening, for example after the camera has continuously failed to open.
*
* @param camera Registered camera whose state is being set
* @param state New state of the registered camera
* @param notifyImmediately {@code true} if the registered camera should be notified
* immediately if a new slot for opening is available, {@code false}
* otherwise.
*/
public void markCameraState(@NonNull Camera camera, @NonNull CameraInternal.State state,
boolean notifyImmediately) {
Map<Camera, CameraRegistration> camerasToNotify = null;
synchronized (mLock) {
CameraInternal.State previousState = null;
int previousAvailableCameras = mAvailableCameras;
if (state == CameraInternal.State.RELEASED) {
previousState = unregisterCamera(camera);
} else {
previousState = updateAndVerifyState(camera, state);
}
if (previousState == state) {
// Nothing has changed. No need to notify.
return;
}
if (previousAvailableCameras < 1 && mAvailableCameras > 0) {
// Cameras are now available, notify ALL cameras in a PENDING_OPEN state.
camerasToNotify = new HashMap<>();
for (Map.Entry<Camera, CameraRegistration> entry : mCameraStates.entrySet()) {
if (entry.getValue().getState() == CameraInternal.State.PENDING_OPEN) {
camerasToNotify.put(entry.getKey(), entry.getValue());
}
}
} else if (state == CameraInternal.State.PENDING_OPEN && mAvailableCameras > 0) {
// This camera entered a PENDING_OPEN state while there are available cameras,
// only notify the single camera.
camerasToNotify = new HashMap<>();
camerasToNotify.put(camera, mCameraStates.get(camera));
}
// Omit notifying this camera if `notifyImmediately` is false
if (camerasToNotify != null && !notifyImmediately) {
camerasToNotify.remove(camera);
}
}
// Notify pending cameras unlocked.
if (camerasToNotify != null) {
for (CameraRegistration registration : camerasToNotify.values()) {
registration.notifyListener();
}
}
}
// Unregisters the given camera and returns the state before being unregistered
@GuardedBy("mLock")
@Nullable
private CameraInternal.State unregisterCamera(Camera camera) {
CameraRegistration registration = mCameraStates.remove(camera);
if (registration != null) {
recalculateAvailableCameras();
return registration.getState();
}
return null;
}
// Updates the state of the given camera and returns the previous state.
@GuardedBy("mLock")
@Nullable
private CameraInternal.State updateAndVerifyState(@NonNull Camera camera,
@NonNull CameraInternal.State state) {
CameraInternal.State previousState = Preconditions.checkNotNull(mCameraStates.get(camera),
"Cannot update state of camera which has not yet been registered. Register with "
+ "CameraStateRegistry.registerCamera()").setState(state);
if (state == CameraInternal.State.OPENING) {
// A camera should only enter an OPENING state if it is already in an open state or
// it has been allowed to by tryOpenCamera().
Preconditions.checkState(isOpen(state) || previousState == CameraInternal.State.OPENING,
"Cannot mark camera as opening until camera was successful at calling "
+ "CameraStateRegistry.tryOpenCamera()");
}
// Only update the available camera count if the camera state has changed.
if (previousState != state) {
recalculateAvailableCameras();
}
return previousState;
}
private static boolean isOpen(@Nullable CameraInternal.State state) {
return state != null && state.holdsCameraSlot();
}
@WorkerThread
@GuardedBy("mLock")
private void recalculateAvailableCameras() {
if (Logger.isDebugEnabled(TAG)) {
mDebugString.setLength(0);
mDebugString.append("Recalculating open cameras:\n");
mDebugString.append(String.format(Locale.US, "%-45s%-22s\n", "Camera", "State"));
mDebugString.append(
"-------------------------------------------------------------------\n");
}
// Count the number of cameras that are not in a closed state. Closed states are
// considered to be CLOSED, PENDING_OPEN or OPENING, since we can't guarantee a camera
// has actually been open in these states. All cameras that are in a CLOSING or RELEASING
// state may have previously been open, so we will count them as open.
int openCount = 0;
for (Map.Entry<Camera, CameraRegistration> entry : mCameraStates.entrySet()) {
if (Logger.isDebugEnabled(TAG)) {
String stateString =
entry.getValue().getState() != null ? entry.getValue().getState().toString()
: "UNKNOWN";
mDebugString.append(
String.format(Locale.US, "%-45s%-22s\n", entry.getKey().toString(),
stateString));
}
if (isOpen(entry.getValue().getState())) {
openCount++;
}
}
if (Logger.isDebugEnabled(TAG)) {
mDebugString.append(
"-------------------------------------------------------------------\n");
mDebugString.append(String.format(Locale.US, "Open count: %d (Max allowed: %d)",
openCount,
mMaxAllowedOpenedCameras));
Logger.d(TAG, mDebugString.toString());
}
// Calculate available cameras value (clamped to 0 or more)
mAvailableCameras = Math.max(mMaxAllowedOpenedCameras - openCount, 0);
}
/** Returns whether at least 1 camera is closing. */
public boolean isCameraClosing() {
synchronized (mLock) {
for (Map.Entry<Camera, CameraRegistration> entry : mCameraStates.entrySet()) {
if (entry.getValue().getState() == CameraInternal.State.CLOSING) {
return true;
}
}
return false;
}
}
/**
* A listener that is notified when a camera slot becomes available for opening.
*/
public interface OnOpenAvailableListener {
/**
* Called when a camera slot becomes available for opening.
*
* <p>Listeners can attempt to open a slot with
* {@link CameraStateRegistry#tryOpenCamera(Camera)} after receiving this signal.
*
* <p>Only cameras that are in a {@link CameraInternal.State#PENDING_OPEN} state will
* receive this signal.
*/
void onOpenAvailable();
}
private static class CameraRegistration {
private CameraInternal.State mState;
private final Executor mNotifyExecutor;
private final OnOpenAvailableListener mCameraAvailableListener;
CameraRegistration(@Nullable CameraInternal.State initialState,
@NonNull Executor notifyExecutor,
@NonNull OnOpenAvailableListener cameraAvailableListener) {
mState = initialState;
mNotifyExecutor = notifyExecutor;
mCameraAvailableListener = cameraAvailableListener;
}
CameraInternal.State setState(@Nullable CameraInternal.State state) {
CameraInternal.State previousState = mState;
mState = state;
return previousState;
}
CameraInternal.State getState() {
return mState;
}
void notifyListener() {
try {
mNotifyExecutor.execute(mCameraAvailableListener::onOpenAvailable);
} catch (RejectedExecutionException e) {
Logger.e(TAG, "Unable to notify camera.", e);
}
}
}
}