CameraManagerCompat.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.compat;

import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.os.Build;
import android.os.Handler;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.camera.core.impl.utils.MainThreadAsyncHandler;

import java.util.concurrent.Executor;

/**
 * Helper for accessing features in {@link CameraManager} in a backwards compatible fashion.
 */
@RequiresApi(21)
public final class CameraManagerCompat {
    private final CameraManagerCompatImpl mImpl;

    private CameraManagerCompat(CameraManagerCompatImpl impl) {
        mImpl = impl;
    }


    /** Get a {@link CameraManagerCompat} instance for a provided context. */
    @NonNull
    public static CameraManagerCompat from(@NonNull Context context) {
        return CameraManagerCompat.from(context, MainThreadAsyncHandler.getInstance());
    }

    /**
     * Get a {@link CameraManagerCompat} instance for a provided context, and using the provided
     * compat handler for scheduling to {@link Executor} APIs.
     *
     * @param context       Context used to retrieve the {@link CameraManager}.
     * @param compatHandler {@link Handler} used for all APIs taking an {@link Executor} argument
     *                      on lower API levels. If the API level does not support directly
     *                      executing on an Executor, it will first be posted to this handler and
     *                      the executor will be called from there.
     */
    @NonNull
    public static CameraManagerCompat from(@NonNull Context context,
            @NonNull Handler compatHandler) {
        if (Build.VERSION.SDK_INT >= 29) {
            return new CameraManagerCompat(new CameraManagerCompatApi29Impl(context));
        } else if (Build.VERSION.SDK_INT >= 28) {
            // Can use Executor directly on API 28+
            return new CameraManagerCompat(CameraManagerCompatApi28Impl.create(context));
        }

        // Pass compat handler to implementation.
        return new CameraManagerCompat(CameraManagerCompatBaseImpl.create(context,
                compatHandler));
    }

    /**
     * Return the list of currently connected camera devices by identifier, including cameras that
     * may be in use by other camera API clients.
     *
     * <p>The behavior of this method matches that of {@link CameraManager#getCameraIdList()},
     * except that {@link CameraAccessExceptionCompat} is thrown in place of
     * {@link CameraAccessException} for convenience.
     *
     * @return The list of currently connected camera devices.
     */
    @NonNull
    public String[] getCameraIdList() throws CameraAccessExceptionCompat {
        return mImpl.getCameraIdList();
    }

    /**
     * Register a callback to be notified about camera device availability.
     *
     * <p>The behavior of this method matches that of {@link
     * CameraManager#registerAvailabilityCallback(CameraManager.AvailabilityCallback, Handler)},
     * except that it uses {@link Executor} as an argument instead of {@link Handler}.
     *
     * <p>When registering an availability callback with
     * {@link #registerAvailabilityCallback(Executor, CameraManager.AvailabilityCallback)}, it
     * should always be unregistered by calling
     * {@link #unregisterAvailabilityCallback(CameraManager.AvailabilityCallback)} on <b>the same
     * instance</b> of {@link CameraManagerCompat}. Unregistering through a difference instance
     * or directly through {@link CameraManager} may fail to unregister the callback with the
     * camera service.
     *
     * @param executor The executor which will be used to invoke the callback.
     * @param callback the new callback to send camera availability notices to
     * @throws IllegalArgumentException if the executor is {@code null}.
     */
    public void registerAvailabilityCallback(
            @NonNull /* @CallbackExecutor */ Executor executor,
            @NonNull CameraManager.AvailabilityCallback callback) {
        mImpl.registerAvailabilityCallback(executor, callback);
    }

    /**
     * Remove a previously-added callback; the callback will no longer receive connection and
     * disconnection callbacks.
     *
     * <p>All callbacks registered through an instance of {@link CameraManagerCompat} should be
     * unregistered through <b>the same instance</b>, otherwise the callback may fail to
     * unregister with the camera service.
     *
     * <p>Removing a callback that isn't registered has no effect.</p>
     *
     * @param callback The callback to remove from the notification list
     */
    public void unregisterAvailabilityCallback(
            @NonNull CameraManager.AvailabilityCallback callback) {
        mImpl.unregisterAvailabilityCallback(callback);
    }

    /**
     * Query the capabilities of a camera device. These capabilities are immutable for a given
     * camera.
     *
     * <p>The behavior of this method matches that of
     * {@link CameraManager#getCameraCharacteristics(String)}.
     *
     * @param cameraId The id of the camera device to query. This could be either a standalone
     * camera ID which can be directly opened by {@link #openCamera}, or a physical camera ID that
     * can only used as part of a logical multi-camera.
     * @return The properties of the given camera
     *
     * @throws IllegalArgumentException    if the cameraId does not match any known camera device.
     * @throws CameraAccessExceptionCompat if the camera device has been disconnected or the
     *                                     device is in Do Not Disturb mode with an early version
     *                                     of Android P.
     */
    @NonNull
    public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId)
            throws CameraAccessExceptionCompat {

        try {
            return mImpl.getCameraCharacteristics(cameraId);
        } catch (AssertionError e) {
            // Some devices may throw AssertionError when creating CameraCharacteristics and FPS
            // ranges are null. Catch the AssertionError and throw a CameraAccessExceptionCompat
            // to make the app be able to receive an exception to gracefully handle it.
            throw new CameraAccessExceptionCompat(
                    CameraAccessExceptionCompat.CAMERA_CHARACTERISTICS_CREATION_ERROR,
                    e.getMessage(), e);
        }
    }

    /**
     * Open a connection to a camera with the given ID.
     *
     * <p>The behavior of this method matches that of
     * {@link CameraManager#openCamera(String, CameraDevice.StateCallback, Handler)}, except that
     * it uses {@link Executor} as an argument instead of {@link Handler}.
     *
     * @param cameraId The unique identifier of the camera device to open
     * @param executor The executor which will be used when invoking the callback.
     * @param callback The callback which is invoked once the camera is opened
     * @throws CameraAccessExceptionCompat if the camera is disabled by device policy, has been
     *                                     disconnected, is being used by a higher-priority
     *                                     camera API client or the device is in Do Not Disturb
     *                                     mode with an early version of Android P.
     * @throws IllegalArgumentException    if cameraId, the callback or the executor was null,
     *                                     or the cameraId does not match any currently or
     *                                     previously available camera device.
     * @throws SecurityException           if the application does not have permission to access
     *                                     the camera
     * @see CameraManager#getCameraIdList
     * @see android.app.admin.DevicePolicyManager#setCameraDisabled
     */
    @RequiresPermission(android.Manifest.permission.CAMERA)
    public void openCamera(@NonNull String cameraId,
            @NonNull /*@CallbackExecutor*/ Executor executor,
            @NonNull CameraDevice.StateCallback callback)
            throws CameraAccessExceptionCompat {
        mImpl.openCamera(cameraId, executor, callback);
    }

    /**
     * Gets the underlying framework {@link CameraManager} object.
     *
     * <p>This method can be used gain access to {@link CameraManager} methods not exposed by
     * {@link CameraManagerCompat}.
     */
    @NonNull
    public CameraManager unwrap() {
        return mImpl.getCameraManager();
    }

    interface CameraManagerCompatImpl {

        String[] getCameraIdList() throws CameraAccessExceptionCompat;

        void registerAvailabilityCallback(
                @NonNull /* @CallbackExecutor */ Executor executor,
                @NonNull CameraManager.AvailabilityCallback callback);

        void unregisterAvailabilityCallback(@NonNull CameraManager.AvailabilityCallback callback);

        @NonNull
        CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId)
                throws CameraAccessExceptionCompat;

        @RequiresPermission(android.Manifest.permission.CAMERA)
        void openCamera(@NonNull String cameraId,
                @NonNull /* @CallbackExecutor */ Executor executor,
                @NonNull CameraDevice.StateCallback callback)
                throws CameraAccessExceptionCompat;

        @NonNull
        CameraManager getCameraManager();
    }

    static final class AvailabilityCallbackExecutorWrapper extends
            CameraManager.AvailabilityCallback {

        private final Executor mExecutor;
        final CameraManager.AvailabilityCallback mWrappedCallback;
        private final Object mLock = new Object();
        @GuardedBy("mLock")
        private boolean mDisabled = false;

        AvailabilityCallbackExecutorWrapper(@NonNull Executor executor,
                @NonNull CameraManager.AvailabilityCallback wrappedCallback) {
            mExecutor = executor;
            mWrappedCallback = wrappedCallback;
        }

        // Used to ensure that callbacks do not run after "unregisterAvailabilityCallback" has
        // returned. Once disabled, the wrapper can no longer be used.
        void setDisabled() {
            synchronized (mLock) {
                mDisabled = true;
            }
        }

        @RequiresApi(29)
        @Override
        public void onCameraAccessPrioritiesChanged() {
            synchronized (mLock) {
                if (!mDisabled) {
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            mWrappedCallback.onCameraAccessPrioritiesChanged();
                        }
                    });
                }
            }
        }

        @Override
        public void onCameraAvailable(@NonNull final String cameraId) {
            synchronized (mLock) {
                if (!mDisabled) {
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            mWrappedCallback.onCameraAvailable(cameraId);
                        }
                    });
                }
            }
        }

        @Override
        public void onCameraUnavailable(@NonNull final String cameraId) {
            synchronized (mLock) {
                if (!mDisabled) {
                    mExecutor.execute(new Runnable() {
                        @Override
                        public void run() {
                            mWrappedCallback.onCameraUnavailable(cameraId);
                        }
                    });
                }
            }
        }
    }
}