CameraAvailabilityRegistry.java

/*
 * Copyright 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.camera2.internal;

import android.util.Log;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.LiveDataObservable;
import androidx.camera.core.impl.Observable;
import androidx.core.util.Preconditions;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * A registry that tracks the state of cameras and publishes the number of cameras available to
 * open.
 */
final class CameraAvailabilityRegistry {
    private static final boolean DEBUG = false;
    private StringBuilder mDebugString = DEBUG ? new StringBuilder() : null;
    private static final String TAG = "AvailabilityRegistry";

    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    final int mMaxAllowedOpenedCameras;
    private final Executor mExecutor;
    private final LiveDataObservable<Integer> mAvailableCameras;

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final Map<CameraInternal, CameraInternal.State> mCameraStates = new HashMap<>();


    /**
     * Creates a new registry with a limit of {@code maxAllowedOpenCameras} allowed to be opened.
     *
     * @param maxAllowedOpenedCameras The limit of number of simultaneous open cameras.
     * @param executor                An executor used for state callbacks on
     */
    CameraAvailabilityRegistry(int maxAllowedOpenedCameras, @NonNull Executor executor) {
        mMaxAllowedOpenedCameras = maxAllowedOpenedCameras;
        mExecutor = Preconditions.checkNotNull(executor);
        mAvailableCameras = new LiveDataObservable<>();
        mAvailableCameras.postValue(maxAllowedOpenedCameras);
    }

    /**
     * Registers a camera with the registry.
     *
     * <p>Once registered, the state will be tracked until the camera is released. Once released,
     * the camera will be automatically unregistered.
     *
     * @param cameraInternal The camera to register.
     */
    void registerCamera(@NonNull final CameraInternal cameraInternal) {
        synchronized (mLock) {
            if (!mCameraStates.containsKey(cameraInternal)) {
                mCameraStates.put(cameraInternal, null);

                cameraInternal.getCameraState().addObserver(mExecutor,
                        new Observable.Observer<CameraInternal.State>() {
                            @Override
                            public void onNewData(@Nullable CameraInternal.State state) {
                                if (state == CameraInternal.State.RELEASED) {
                                    unregisterCamera(cameraInternal, this);
                                } else {
                                    updateState(cameraInternal, state);
                                }
                            }

                            @Override
                            public void onError(@NonNull Throwable t) {
                                // Ignore errors on state for now. Handle these in the future if
                                // needed.
                            }
                        });
            }
        }
    }

    /**
     * Returns an observable stream of the current available camera count.
     *
     * <p>This count is a best effort count of cameras available to be opened on this device.
     * This should only be used as a hint for when cameras can be opened. Due to the asynchronous
     * nature of notifications and when the camera device is opened, users should still expect
     * that attempting to open cameras may fail and should handle errors appropriately.
     */
    Observable<Integer> getAvailableCameraCount() {
        return mAvailableCameras;
    }

    @WorkerThread
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    void unregisterCamera(CameraInternal cameraInternal,
            Observable.Observer<CameraInternal.State> observer) {
        int availableCameras;
        synchronized (mLock) {
            cameraInternal.getCameraState().removeObserver(observer);
            if (mCameraStates.remove(cameraInternal) == null) {
                return;
            }

            availableCameras = recalculateAvailableCameras();
        }

        mAvailableCameras.postValue(availableCameras);
    }

    @WorkerThread
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    void updateState(CameraInternal cameraInternal, CameraInternal.State state) {
        int availableCameras;
        synchronized (mLock) {
            // If mCameraStates does not contain the camera, it may have been unregistered.
            // Or, if the state has not been updated, ignore this update.
            if (!mCameraStates.containsKey(cameraInternal) || mCameraStates.put(cameraInternal,
                    state) == state) {
                return;
            }

            availableCameras = recalculateAvailableCameras();
        }

        mAvailableCameras.postValue(availableCameras);
    }

    @WorkerThread
    @GuardedBy("mLock")
    private int recalculateAvailableCameras() {
        if (DEBUG) {
            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 state. Closed states are
        // considered to be CLOSED, PENDING_OPEN or OPENING, since we can't guarantee a camera
        // has actually be 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<CameraInternal, CameraInternal.State> entry : mCameraStates.entrySet()) {
            if (DEBUG) {
                String stateString =
                        entry.getValue() != null ? entry.getValue().toString() : "UNKNOWN";
                mDebugString.append(String.format(Locale.US, "%-45s%-22s\n",
                        entry.getKey().toString(),
                        stateString));
            }
            if (entry.getValue() != CameraInternal.State.CLOSED
                    && entry.getValue() != CameraInternal.State.OPENING
                    && entry.getValue() != CameraInternal.State.PENDING_OPEN) {
                openCount++;
            }
        }
        if (DEBUG) {
            mDebugString.append(
                    "-------------------------------------------------------------------\n");
            mDebugString.append(String.format(Locale.US, "Open count: %d (Max allowed: %d)",
                    openCount,
                    mMaxAllowedOpenedCameras));
            Log.d(TAG, mDebugString.toString());
        }

        // Calculate available cameras value (clamped to 0 or more)
        return Math.max(mMaxAllowedOpenedCameras - openCount, 0);
    }
}