public final class

CameraStateRegistry

extends java.lang.Object

 java.lang.Object

↳androidx.camera.core.impl.CameraStateRegistry

Gradle dependencies

compile group: 'androidx.camera', name: 'camera-core', version: '1.2.0-alpha01'

  • groupId: androidx.camera
  • artifactId: camera-core
  • version: 1.2.0-alpha01

Artifact androidx.camera:camera-core:1.2.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A registry that tracks the state of cameras.

The registry tracks internally how many cameras are open and how many are available to open. Cameras that are in a CameraInternal.State.PENDING_OPEN state can be notified when there is a slot available to open a camera.

Summary

Constructors
publicCameraStateRegistry(int maxAllowedOpenedCameras)

Creates a new registry with a limit of maxAllowedOpenCameras allowed to be opened.

Methods
public booleanisCameraClosing()

Returns whether at least 1 camera is closing.

public voidmarkCameraState(Camera camera, CameraInternal.State state)

Mark the state of a registered camera.

public voidmarkCameraState(Camera camera, CameraInternal.State state, boolean notifyImmediately)

Mark the state of a registered camera.

public voidregisterCamera(Camera camera, java.util.concurrent.Executor notifyExecutor, CameraStateRegistry.OnOpenAvailableListener cameraAvailableListener)

Registers a camera with the registry.

public booleantryOpenCamera(Camera camera)

Should be called before attempting to actually open a camera.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public CameraStateRegistry(int maxAllowedOpenedCameras)

Creates a new registry with a limit of maxAllowedOpenCameras allowed to be opened.

Parameters:

maxAllowedOpenedCameras: The limit for number of simultaneous open cameras.

Methods

public void registerCamera(Camera camera, java.util.concurrent.Executor notifyExecutor, CameraStateRegistry.OnOpenAvailableListener cameraAvailableListener)

Registers a camera with the registry.

Once registered, the camera's state must be updated through CameraStateRegistry.markCameraState(Camera, CameraInternal.State).

Before attempting to open a camera, CameraStateRegistry.tryOpenCamera(Camera) must be called and callers should only continue to open the camera if it returns true.

Cameras will be automatically unregistered when they are marked as being in a CameraInternal.State.RELEASED state.

Parameters:

camera: The camera to register.

public boolean tryOpenCamera(Camera camera)

Should be called before attempting to actually open a camera.

This must be called before attempting to open a camera. If too many cameras are already open, then this will return false, and the caller should not attempt to open the camera. Instead, the caller should mark its state as CameraInternal.State.PENDING_OPEN with CameraStateRegistry.markCameraState(Camera, CameraInternal.State), and the listener registered with CameraStateRegistry.registerCamera(Camera, Executor, CameraStateRegistry.OnOpenAvailableListener) will be notified when a camera becomes available. At that time, the caller should attempt to call this method again.

Returns:

true if it is safe to open the camera. If this returns true, it is assumed the camera is now in an CameraInternal.State.OPENING state, and the available camera count will be reduced by 1.

public void markCameraState(Camera camera, CameraInternal.State state)

Mark the state of a registered camera.

This is used to track the states of all cameras in order to determine how many cameras are available to be opened.

Parameters:

camera: Registered camera whose state is being set
state: New state of the registered camera

public void markCameraState(Camera camera, CameraInternal.State state, boolean notifyImmediately)

Mark the state of a registered camera.

This is used to track the states of all cameras in order to determine how many cameras are available to be opened.

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 notifyImmediately is set to false. This can be useful if a camera moves its state to 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.

Parameters:

camera: Registered camera whose state is being set
state: New state of the registered camera
notifyImmediately: true if the registered camera should be notified immediately if a new slot for opening is available, false otherwise.

public boolean isCameraClosing()

Returns whether at least 1 camera is closing.

Source

/*
 * 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);
            }
        }
    }
}