blob: c8276b25c52d6229e5f990fa0b96f09ad5f6750f [file] [log] [blame]
/*
* Copyright (C) 2013 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 android.hardware.camera2;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.content.Context;
import android.hardware.CameraInfo;
import android.hardware.CameraStatus;
import android.hardware.ICameraService;
import android.hardware.ICameraServiceListener;
import android.hardware.camera2.impl.CameraDeviceImpl;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.legacy.CameraDeviceUserShim;
import android.hardware.camera2.legacy.LegacyMetadataMapper;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Size;
import android.view.Display;
import android.view.WindowManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* <p>A system service manager for detecting, characterizing, and connecting to
* {@link CameraDevice CameraDevices}.</p>
*
* <p>For more details about communicating with camera devices, read the Camera
* developer guide or the {@link android.hardware.camera2 camera2}
* package documentation.</p>
*/
@SystemService(Context.CAMERA_SERVICE)
public final class CameraManager {
private static final String TAG = "CameraManager";
private final boolean DEBUG = false;
private static final int USE_CALLING_UID = -1;
@SuppressWarnings("unused")
private static final int API_VERSION_1 = 1;
private static final int API_VERSION_2 = 2;
private static final int CAMERA_TYPE_BACKWARD_COMPATIBLE = 0;
private static final int CAMERA_TYPE_ALL = 1;
private ArrayList<String> mDeviceIdList;
private final Context mContext;
private final Object mLock = new Object();
/**
* @hide
*/
public CameraManager(Context context) {
synchronized(mLock) {
mContext = context;
}
}
/**
* Return the list of currently connected camera devices by identifier, including
* cameras that may be in use by other camera API clients.
*
* <p>Non-removable cameras use integers starting at 0 for their
* identifiers, while removable cameras have a unique identifier for each
* individual device, even if they are the same model.</p>
*
* <p>This list doesn't contain physical cameras that can only be used as part of a logical
* multi-camera device.</p>
*
* @return The list of currently connected camera devices.
*/
@NonNull
public String[] getCameraIdList() throws CameraAccessException {
return CameraManagerGlobal.get().getCameraIdList();
}
/**
* Register a callback to be notified about camera device availability.
*
* <p>Registering the same callback again will replace the handler with the
* new one provided.</p>
*
* <p>The first time a callback is registered, it is immediately called
* with the availability status of all currently known camera devices.</p>
*
* <p>{@link AvailabilityCallback#onCameraUnavailable(String)} will be called whenever a camera
* device is opened by any camera API client. As of API level 23, other camera API clients may
* still be able to open such a camera device, evicting the existing client if they have higher
* priority than the existing client of a camera device. See open() for more details.</p>
*
* <p>Since this callback will be registered with the camera service, remember to unregister it
* once it is no longer needed; otherwise the callback will continue to receive events
* indefinitely and it may prevent other resources from being released. Specifically, the
* callbacks will be invoked independently of the general activity lifecycle and independently
* of the state of individual CameraManager instances.</p>
*
* @param callback the new callback to send camera availability notices to
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*
* @throws IllegalArgumentException if the handler is {@code null} but the current thread has
* no looper.
*/
public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,
@Nullable Handler handler) {
CameraManagerGlobal.get().registerAvailabilityCallback(callback,
CameraDeviceImpl.checkAndWrapHandler(handler));
}
/**
* Register a callback to be notified about camera device availability.
*
* <p>The behavior of this method matches that of
* {@link #registerAvailabilityCallback(AvailabilityCallback, Handler)},
* except that it uses {@link java.util.concurrent.Executor} as an argument
* instead of {@link android.os.Handler}.</p>
*
* @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 AvailabilityCallback callback) {
if (executor == null) {
throw new IllegalArgumentException("executor was null");
}
CameraManagerGlobal.get().registerAvailabilityCallback(callback, executor);
}
/**
* Remove a previously-added callback; the callback will no longer receive connection and
* disconnection callbacks.
*
* <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 AvailabilityCallback callback) {
CameraManagerGlobal.get().unregisterAvailabilityCallback(callback);
}
/**
* Register a callback to be notified about torch mode status.
*
* <p>Registering the same callback again will replace the handler with the
* new one provided.</p>
*
* <p>The first time a callback is registered, it is immediately called
* with the torch mode status of all currently known camera devices with a flash unit.</p>
*
* <p>Since this callback will be registered with the camera service, remember to unregister it
* once it is no longer needed; otherwise the callback will continue to receive events
* indefinitely and it may prevent other resources from being released. Specifically, the
* callbacks will be invoked independently of the general activity lifecycle and independently
* of the state of individual CameraManager instances.</p>
*
* @param callback The new callback to send torch mode status to
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*
* @throws IllegalArgumentException if the handler is {@code null} but the current thread has
* no looper.
*/
public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler) {
CameraManagerGlobal.get().registerTorchCallback(callback,
CameraDeviceImpl.checkAndWrapHandler(handler));
}
/**
* Register a callback to be notified about torch mode status.
*
* <p>The behavior of this method matches that of
* {@link #registerTorchCallback(TorchCallback, Handler)},
* except that it uses {@link java.util.concurrent.Executor} as an argument
* instead of {@link android.os.Handler}.</p>
*
* @param executor The executor which will be used to invoke the callback
* @param callback The new callback to send torch mode status to
*
* @throws IllegalArgumentException if the executor is {@code null}.
*/
public void registerTorchCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull TorchCallback callback) {
if (executor == null) {
throw new IllegalArgumentException("executor was null");
}
CameraManagerGlobal.get().registerTorchCallback(callback, executor);
}
/**
* Remove a previously-added callback; the callback will no longer receive torch mode status
* callbacks.
*
* <p>Removing a callback that isn't registered has no effect.</p>
*
* @param callback The callback to remove from the notification list
*/
public void unregisterTorchCallback(@NonNull TorchCallback callback) {
CameraManagerGlobal.get().unregisterTorchCallback(callback);
}
private Size getDisplaySize() {
Size ret = new Size(0, 0);
try {
WindowManager windowManager =
(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
if (height > width) {
height = width;
width = display.getHeight();
}
ret = new Size(width, height);
} catch (Exception e) {
Log.e(TAG, "getDisplaySize Failed. " + e.toString());
}
return ret;
}
/**
* <p>Query the capabilities of a camera device. These capabilities are
* immutable for a given camera.</p>
*
* <p>From API level 29, this function can also be used to query the capabilities of physical
* cameras that can only be used as part of logical multi-camera. These cameras cannot be
* opened directly via {@link #openCamera}</p>
*
* @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 CameraAccessException if the camera device has been disconnected.
*
* @see #getCameraIdList
* @see android.app.admin.DevicePolicyManager#setCameraDisabled
*/
@NonNull
public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId)
throws CameraAccessException {
CameraCharacteristics characteristics = null;
if (CameraManagerGlobal.sCameraServiceDisabled) {
throw new IllegalArgumentException("No cameras available on device");
}
synchronized (mLock) {
/*
* Get the camera characteristics from the camera service directly if it supports it,
* otherwise get them from the legacy shim instead.
*/
ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
if (cameraService == null) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service is currently unavailable");
}
try {
Size displaySize = getDisplaySize();
// First check isHiddenPhysicalCamera to avoid supportsCamera2ApiLocked throwing
// exception in case cameraId is a hidden physical camera.
if (!isHiddenPhysicalCamera(cameraId) && !supportsCamera2ApiLocked(cameraId)) {
// Legacy backwards compatibility path; build static info from the camera
// parameters
int id = Integer.parseInt(cameraId);
String parameters = cameraService.getLegacyParameters(id);
CameraInfo info = cameraService.getCameraInfo(id);
characteristics = LegacyMetadataMapper.createCharacteristics(parameters, info,
id, displaySize);
} else {
// Normal path: Get the camera characteristics directly from the camera service
CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId);
try {
info.setCameraId(Integer.parseInt(cameraId));
} catch (NumberFormatException e) {
Log.e(TAG, "Failed to parse camera Id " + cameraId + " to integer");
}
info.setDisplaySize(displaySize);
characteristics = new CameraCharacteristics(info);
}
} catch (ServiceSpecificException e) {
throwAsPublicException(e);
} catch (RemoteException e) {
// Camera service died - act as if the camera was disconnected
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service is currently unavailable", e);
}
}
return characteristics;
}
/**
* Helper for opening a connection to a camera with the given ID.
*
* @param cameraId The unique identifier of the camera device to open
* @param callback The callback for the camera. Must not be null.
* @param executor The executor to invoke the callback with. Must not be null.
* @param uid The UID of the application actually opening the camera.
* Must be USE_CALLING_UID unless the caller is a service
* that is trusted to open the device on behalf of an
* application and to forward the real UID.
*
* @throws CameraAccessException if the camera is disabled by device policy,
* too many camera devices are already open, or the cameraId does not match
* any currently available camera device.
*
* @throws SecurityException if the application does not have permission to
* access the camera
* @throws IllegalArgumentException if callback or handler is null.
* @return A handle to the newly-created camera device.
*
* @see #getCameraIdList
* @see android.app.admin.DevicePolicyManager#setCameraDisabled
*/
private CameraDevice openCameraDeviceUserAsync(String cameraId,
CameraDevice.StateCallback callback, Executor executor, final int uid)
throws CameraAccessException {
CameraCharacteristics characteristics = getCameraCharacteristics(cameraId);
CameraDevice device = null;
synchronized (mLock) {
ICameraDeviceUser cameraUser = null;
android.hardware.camera2.impl.CameraDeviceImpl deviceImpl =
new android.hardware.camera2.impl.CameraDeviceImpl(
cameraId,
callback,
executor,
characteristics,
mContext.getApplicationInfo().targetSdkVersion);
ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks();
try {
if (supportsCamera2ApiLocked(cameraId)) {
// Use cameraservice's cameradeviceclient implementation for HAL3.2+ devices
ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
if (cameraService == null) {
throw new ServiceSpecificException(
ICameraService.ERROR_DISCONNECTED,
"Camera service is currently unavailable");
}
cameraUser = cameraService.connectDevice(callbacks, cameraId,
mContext.getOpPackageName(), uid);
} else {
// Use legacy camera implementation for HAL1 devices
int id;
try {
id = Integer.parseInt(cameraId);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Expected cameraId to be numeric, but it was: "
+ cameraId);
}
Log.i(TAG, "Using legacy camera HAL.");
cameraUser = CameraDeviceUserShim.connectBinderShim(callbacks, id,
getDisplaySize());
}
} catch (ServiceSpecificException e) {
if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) {
throw new AssertionError("Should've gone down the shim path");
} else if (e.errorCode == ICameraService.ERROR_CAMERA_IN_USE ||
e.errorCode == ICameraService.ERROR_MAX_CAMERAS_IN_USE ||
e.errorCode == ICameraService.ERROR_DISABLED ||
e.errorCode == ICameraService.ERROR_DISCONNECTED ||
e.errorCode == ICameraService.ERROR_INVALID_OPERATION) {
// Received one of the known connection errors
// The remote camera device cannot be connected to, so
// set the local camera to the startup error state
deviceImpl.setRemoteFailure(e);
if (e.errorCode == ICameraService.ERROR_DISABLED ||
e.errorCode == ICameraService.ERROR_DISCONNECTED ||
e.errorCode == ICameraService.ERROR_CAMERA_IN_USE) {
// Per API docs, these failures call onError and throw
throwAsPublicException(e);
}
} else {
// Unexpected failure - rethrow
throwAsPublicException(e);
}
} catch (RemoteException e) {
// Camera service died - act as if it's a CAMERA_DISCONNECTED case
ServiceSpecificException sse = new ServiceSpecificException(
ICameraService.ERROR_DISCONNECTED,
"Camera service is currently unavailable");
deviceImpl.setRemoteFailure(sse);
throwAsPublicException(sse);
}
// TODO: factor out callback to be non-nested, then move setter to constructor
// For now, calling setRemoteDevice will fire initial
// onOpened/onUnconfigured callbacks.
// This function call may post onDisconnected and throw CAMERA_DISCONNECTED if
// cameraUser dies during setup.
deviceImpl.setRemoteDevice(cameraUser);
device = deviceImpl;
}
return device;
}
/**
* Open a connection to a camera with the given ID.
*
* <p>Use {@link #getCameraIdList} to get the list of available camera
* devices. Note that even if an id is listed, open may fail if the device
* is disconnected between the calls to {@link #getCameraIdList} and
* {@link #openCamera}, or if a higher-priority camera API client begins using the
* camera device.</p>
*
* <p>As of API level 23, devices for which the
* {@link AvailabilityCallback#onCameraUnavailable(String)} callback has been called due to the
* device being in use by a lower-priority, background camera API client can still potentially
* be opened by calling this method when the calling camera API client has a higher priority
* than the current camera API client using this device. In general, if the top, foreground
* activity is running within your application process, your process will be given the highest
* priority when accessing the camera, and this method will succeed even if the camera device is
* in use by another camera API client. Any lower-priority application that loses control of the
* camera in this way will receive an
* {@link android.hardware.camera2.CameraDevice.StateCallback#onDisconnected} callback.</p>
*
* <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will
* be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up
* for operation by calling {@link CameraDevice#createCaptureSession} and
* {@link CameraDevice#createCaptureRequest}</p>
*
* <!--
* <p>Since the camera device will be opened asynchronously, any asynchronous operations done
* on the returned CameraDevice instance will be queued up until the device startup has
* completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is
* called. The pending operations are then processed in order.</p>
* -->
* <p>If the camera becomes disconnected during initialization
* after this function call returns,
* {@link CameraDevice.StateCallback#onDisconnected} with a
* {@link CameraDevice} in the disconnected state (and
* {@link CameraDevice.StateCallback#onOpened} will be skipped).</p>
*
* <p>If opening the camera device fails, then the device callback's
* {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent
* calls on the camera device will throw a {@link CameraAccessException}.</p>
*
* @param cameraId
* The unique identifier of the camera device to open
* @param callback
* The callback which is invoked once the camera is opened
* @param handler
* The handler on which the callback should be invoked, or
* {@code null} to use the current thread's {@link android.os.Looper looper}.
*
* @throws CameraAccessException if the camera is disabled by device policy,
* has been disconnected, or is being used by a higher-priority camera API client.
*
* @throws IllegalArgumentException if cameraId or the callback was null,
* or the cameraId does not match any currently or previously available
* camera device returned by {@link #getCameraIdList}.
*
* @throws SecurityException if the application does not have permission to
* access the camera
*
* @see #getCameraIdList
* @see android.app.admin.DevicePolicyManager#setCameraDisabled
*/
@RequiresPermission(android.Manifest.permission.CAMERA)
public void openCamera(@NonNull String cameraId,
@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)
throws CameraAccessException {
openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
USE_CALLING_UID);
}
/**
* Open a connection to a camera with the given ID.
*
* <p>The behavior of this method matches that of
* {@link #openCamera(String, StateCallback, Handler)}, except that it uses
* {@link java.util.concurrent.Executor} as an argument instead of
* {@link android.os.Handler}.</p>
*
* @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 CameraAccessException if the camera is disabled by device policy,
* has been disconnected, or is being used by a higher-priority camera API client.
*
* @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 #getCameraIdList
* @see android.app.admin.DevicePolicyManager#setCameraDisabled
*/
@RequiresPermission(android.Manifest.permission.CAMERA)
public void openCamera(@NonNull String cameraId,
@NonNull @CallbackExecutor Executor executor,
@NonNull final CameraDevice.StateCallback callback)
throws CameraAccessException {
if (executor == null) {
throw new IllegalArgumentException("executor was null");
}
openCameraForUid(cameraId, callback, executor, USE_CALLING_UID);
}
/**
* Open a connection to a camera with the given ID, on behalf of another application
* specified by clientUid.
*
* <p>The behavior of this method matches that of {@link #openCamera}, except that it allows
* the caller to specify the UID to use for permission/etc verification. This can only be
* done by services trusted by the camera subsystem to act on behalf of applications and
* to forward the real UID.</p>
*
* @param clientUid
* The UID of the application on whose behalf the camera is being opened.
* Must be USE_CALLING_UID unless the caller is a trusted service.
*
* @hide
*/
public void openCameraForUid(@NonNull String cameraId,
@NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor,
int clientUid)
throws CameraAccessException {
if (cameraId == null) {
throw new IllegalArgumentException("cameraId was null");
} else if (callback == null) {
throw new IllegalArgumentException("callback was null");
}
if (CameraManagerGlobal.sCameraServiceDisabled) {
throw new IllegalArgumentException("No cameras available on device");
}
openCameraDeviceUserAsync(cameraId, callback, executor, clientUid);
}
/**
* Set the flash unit's torch mode of the camera of the given ID without opening the camera
* device.
*
* <p>Use {@link #getCameraIdList} to get the list of available camera devices and use
* {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.
* Note that even if a camera device has a flash unit, turning on the torch mode may fail
* if the camera device or other camera resources needed to turn on the torch mode are in use.
* </p>
*
* <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,
* {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.
* However, even if turning on the torch mode is successful, the application does not have the
* exclusive ownership of the flash unit or the camera device. The torch mode will be turned
* off and becomes unavailable when the camera device that the flash unit belongs to becomes
* unavailable or when other camera resources to keep the torch on become unavailable (
* {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,
* other applications are free to call {@link #setTorchMode} to turn off the torch mode (
* {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest
* application that turned on the torch mode exits, the torch mode will be turned off.
*
* @param cameraId
* The unique identifier of the camera device that the flash unit belongs to.
* @param enabled
* The desired state of the torch mode for the target camera device. Set to
* {@code true} to turn on the torch mode. Set to {@code false} to turn off the
* torch mode.
*
* @throws CameraAccessException if it failed to access the flash unit.
* {@link CameraAccessException#CAMERA_IN_USE} will be thrown if the camera device
* is in use. {@link CameraAccessException#MAX_CAMERAS_IN_USE} will be thrown if
* other camera resources needed to turn on the torch mode are in use.
* {@link CameraAccessException#CAMERA_DISCONNECTED} will be thrown if camera
* service is not available.
*
* @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently
* or previously available camera device, or the camera device doesn't have a
* flash unit.
*/
public void setTorchMode(@NonNull String cameraId, boolean enabled)
throws CameraAccessException {
if (CameraManagerGlobal.sCameraServiceDisabled) {
throw new IllegalArgumentException("No cameras available on device");
}
CameraManagerGlobal.get().setTorchMode(cameraId, enabled);
}
/**
* A callback for camera devices becoming available or unavailable to open.
*
* <p>Cameras become available when they are no longer in use, or when a new
* removable camera is connected. They become unavailable when some
* application or service starts using a camera, or when a removable camera
* is disconnected.</p>
*
* <p>Extend this callback and pass an instance of the subclass to
* {@link CameraManager#registerAvailabilityCallback} to be notified of such availability
* changes.</p>
*
* @see #registerAvailabilityCallback
*/
public static abstract class AvailabilityCallback {
/**
* A new camera has become available to use.
*
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the new camera.
*/
public void onCameraAvailable(@NonNull String cameraId) {
// default empty implementation
}
/**
* A previously-available camera has become unavailable for use.
*
* <p>If an application had an active CameraDevice instance for the
* now-disconnected camera, that application will receive a
* {@link CameraDevice.StateCallback#onDisconnected disconnection error}.</p>
*
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the disconnected camera.
*/
public void onCameraUnavailable(@NonNull String cameraId) {
// default empty implementation
}
/**
* Called whenever camera access priorities change.
*
* <p>Notification that camera access priorities have changed and the camera may
* now be openable. An application that was previously denied camera access due to
* a higher-priority user already using the camera, or that was disconnected from an
* active camera session due to a higher-priority user trying to open the camera,
* should try to open the camera again if it still wants to use it. Note that
* multiple applications may receive this callback at the same time, and only one of
* them will succeed in opening the camera in practice, depending on exact access
* priority levels and timing. This method is useful in cases where multiple
* applications may be in the resumed state at the same time, and the user switches
* focus between them, or if the current camera-using application moves between
* full-screen and Picture-in-Picture (PiP) states. In such cases, the camera
* available/unavailable callbacks will not be invoked, but another application may
* now have higher priority for camera access than the current camera-using
* application.</p>
*
* <p>The default implementation of this method does nothing.</p>
*
*/
public void onCameraAccessPrioritiesChanged() {
// default empty implementation
}
}
/**
* A callback for camera flash torch modes becoming unavailable, disabled, or enabled.
*
* <p>The torch mode becomes unavailable when the camera device it belongs to becomes
* unavailable or other camera resources it needs become busy due to other higher priority
* camera activities. The torch mode becomes disabled when it was turned off or when the camera
* device it belongs to is no longer in use and other camera resources it needs are no longer
* busy. A camera's torch mode is turned off when an application calls {@link #setTorchMode} to
* turn off the camera's torch mode, or when an application turns on another camera's torch mode
* if keeping multiple torch modes on simultaneously is not supported. The torch mode becomes
* enabled when it is turned on via {@link #setTorchMode}.</p>
*
* <p>The torch mode is available to set via {@link #setTorchMode} only when it's in a disabled
* or enabled state.</p>
*
* <p>Extend this callback and pass an instance of the subclass to
* {@link CameraManager#registerTorchCallback} to be notified of such status changes.
* </p>
*
* @see #registerTorchCallback
*/
public static abstract class TorchCallback {
/**
* A camera's torch mode has become unavailable to set via {@link #setTorchMode}.
*
* <p>If torch mode was previously turned on by calling {@link #setTorchMode}, it will be
* turned off before {@link CameraManager.TorchCallback#onTorchModeUnavailable} is
* invoked. {@link #setTorchMode} will fail until the torch mode has entered a disabled or
* enabled state again.</p>
*
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the camera whose torch mode has become
* unavailable.
*/
public void onTorchModeUnavailable(@NonNull String cameraId) {
// default empty implementation
}
/**
* A camera's torch mode has become enabled or disabled and can be changed via
* {@link #setTorchMode}.
*
* <p>The default implementation of this method does nothing.</p>
*
* @param cameraId The unique identifier of the camera whose torch mode has been changed.
*
* @param enabled The state that the torch mode of the camera has been changed to.
* {@code true} when the torch mode has become on and available to be turned
* off. {@code false} when the torch mode has becomes off and available to
* be turned on.
*/
public void onTorchModeChanged(@NonNull String cameraId, boolean enabled) {
// default empty implementation
}
}
/**
* Convert ServiceSpecificExceptions and Binder RemoteExceptions from camera binder interfaces
* into the correct public exceptions.
*
* @hide
*/
public static void throwAsPublicException(Throwable t) throws CameraAccessException {
if (t instanceof ServiceSpecificException) {
ServiceSpecificException e = (ServiceSpecificException) t;
int reason = CameraAccessException.CAMERA_ERROR;
switch(e.errorCode) {
case ICameraService.ERROR_DISCONNECTED:
reason = CameraAccessException.CAMERA_DISCONNECTED;
break;
case ICameraService.ERROR_DISABLED:
reason = CameraAccessException.CAMERA_DISABLED;
break;
case ICameraService.ERROR_CAMERA_IN_USE:
reason = CameraAccessException.CAMERA_IN_USE;
break;
case ICameraService.ERROR_MAX_CAMERAS_IN_USE:
reason = CameraAccessException.MAX_CAMERAS_IN_USE;
break;
case ICameraService.ERROR_DEPRECATED_HAL:
reason = CameraAccessException.CAMERA_DEPRECATED_HAL;
break;
case ICameraService.ERROR_ILLEGAL_ARGUMENT:
case ICameraService.ERROR_ALREADY_EXISTS:
throw new IllegalArgumentException(e.getMessage(), e);
case ICameraService.ERROR_PERMISSION_DENIED:
throw new SecurityException(e.getMessage(), e);
case ICameraService.ERROR_TIMED_OUT:
case ICameraService.ERROR_INVALID_OPERATION:
default:
reason = CameraAccessException.CAMERA_ERROR;
}
throw new CameraAccessException(reason, e.getMessage(), e);
} else if (t instanceof DeadObjectException) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service has died unexpectedly",
t);
} else if (t instanceof RemoteException) {
throw new UnsupportedOperationException("An unknown RemoteException was thrown" +
" which should never happen.", t);
} else if (t instanceof RuntimeException) {
RuntimeException e = (RuntimeException) t;
throw e;
}
}
/**
* Queries the camera service if it supports the camera2 api directly, or needs a shim.
*
* @param cameraId a non-{@code null} camera identifier
* @return {@code false} if the legacy shim needs to be used, {@code true} otherwise.
*/
private boolean supportsCamera2ApiLocked(String cameraId) {
return supportsCameraApiLocked(cameraId, API_VERSION_2);
}
/**
* Queries the camera service if it supports a camera api directly, or needs a shim.
*
* @param cameraId a non-{@code null} camera identifier
* @param apiVersion the version, i.e. {@code API_VERSION_1} or {@code API_VERSION_2}
* @return {@code true} if connecting will work for that device version.
*/
private boolean supportsCameraApiLocked(String cameraId, int apiVersion) {
/*
* Possible return values:
* - NO_ERROR => CameraX API is supported
* - CAMERA_DEPRECATED_HAL => CameraX API is *not* supported (thrown as an exception)
* - Remote exception => If the camera service died
*
* Anything else is an unexpected error we don't want to recover from.
*/
try {
ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
// If no camera service, no support
if (cameraService == null) return false;
return cameraService.supportsCameraApi(cameraId, apiVersion);
} catch (RemoteException e) {
// Camera service is now down, no support for any API level
}
return false;
}
/**
* Queries the camera service if a cameraId is a hidden physical camera that belongs to a
* logical camera device.
*
* A hidden physical camera is a camera that cannot be opened by the application. But it
* can be used as part of a logical camera.
*
* @param cameraId a non-{@code null} camera identifier
* @return {@code true} if cameraId is a hidden physical camera device
*
* @hide
*/
public static boolean isHiddenPhysicalCamera(String cameraId) {
try {
ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
// If no camera service, no support
if (cameraService == null) return false;
return cameraService.isHiddenPhysicalCamera(cameraId);
} catch (RemoteException e) {
// Camera service is now down, no support for any API level
}
return false;
}
/**
* A per-process global camera manager instance, to retain a connection to the camera service,
* and to distribute camera availability notices to API-registered callbacks
*/
private static final class CameraManagerGlobal extends ICameraServiceListener.Stub
implements IBinder.DeathRecipient {
private static final String TAG = "CameraManagerGlobal";
private final boolean DEBUG = false;
private final int CAMERA_SERVICE_RECONNECT_DELAY_MS = 1000;
// Singleton instance
private static final CameraManagerGlobal gCameraManager =
new CameraManagerGlobal();
/**
* This must match the ICameraService definition
*/
private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
private final ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1);
// Camera ID -> Status map
private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();
// Registered availablility callbacks and their executors
private final ArrayMap<AvailabilityCallback, Executor> mCallbackMap =
new ArrayMap<AvailabilityCallback, Executor>();
// torch client binder to set the torch mode with.
private Binder mTorchClientBinder = new Binder();
// Camera ID -> Torch status map
private final ArrayMap<String, Integer> mTorchStatus = new ArrayMap<String, Integer>();
// Registered torch callbacks and their executors
private final ArrayMap<TorchCallback, Executor> mTorchCallbackMap =
new ArrayMap<TorchCallback, Executor>();
private final Object mLock = new Object();
// Access only through getCameraService to deal with binder death
private ICameraService mCameraService;
// Singleton, don't allow construction
private CameraManagerGlobal() {
}
public static final boolean sCameraServiceDisabled =
SystemProperties.getBoolean("config.disable_cameraservice", false);
public static CameraManagerGlobal get() {
return gCameraManager;
}
@Override
public IBinder asBinder() {
return this;
}
/**
* Return a best-effort ICameraService.
*
* <p>This will be null if the camera service is not currently available. If the camera
* service has died since the last use of the camera service, will try to reconnect to the
* service.</p>
*/
public ICameraService getCameraService() {
synchronized(mLock) {
connectCameraServiceLocked();
if (mCameraService == null && !sCameraServiceDisabled) {
Log.e(TAG, "Camera service is unavailable");
}
return mCameraService;
}
}
/**
* Connect to the camera service if it's available, and set up listeners.
* If the service is already connected, do nothing.
*
* <p>Sets mCameraService to a valid pointer or null if the connection does not succeed.</p>
*/
private void connectCameraServiceLocked() {
// Only reconnect if necessary
if (mCameraService != null || sCameraServiceDisabled) return;
Log.i(TAG, "Connecting to camera service");
IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
if (cameraServiceBinder == null) {
// Camera service is now down, leave mCameraService as null
return;
}
try {
cameraServiceBinder.linkToDeath(this, /*flags*/ 0);
} catch (RemoteException e) {
// Camera service is now down, leave mCameraService as null
return;
}
ICameraService cameraService = ICameraService.Stub.asInterface(cameraServiceBinder);
try {
CameraMetadataNative.setupGlobalVendorTagDescriptor();
} catch (ServiceSpecificException e) {
handleRecoverableSetupErrors(e);
}
try {
CameraStatus[] cameraStatuses = cameraService.addListener(this);
for (CameraStatus c : cameraStatuses) {
onStatusChangedLocked(c.status, c.cameraId);
}
mCameraService = cameraService;
} catch(ServiceSpecificException e) {
// Unexpected failure
throw new IllegalStateException("Failed to register a camera service listener", e);
} catch (RemoteException e) {
// Camera service is now down, leave mCameraService as null
}
}
/**
* Get a list of all camera IDs that are at least PRESENT; ignore devices that are
* NOT_PRESENT or ENUMERATING, since they cannot be used by anyone.
*/
public String[] getCameraIdList() {
String[] cameraIds = null;
synchronized(mLock) {
// Try to make sure we have an up-to-date list of camera devices.
connectCameraServiceLocked();
int idCount = 0;
for (int i = 0; i < mDeviceStatus.size(); i++) {
int status = mDeviceStatus.valueAt(i);
if (status == ICameraServiceListener.STATUS_NOT_PRESENT ||
status == ICameraServiceListener.STATUS_ENUMERATING) continue;
idCount++;
}
cameraIds = new String[idCount];
idCount = 0;
for (int i = 0; i < mDeviceStatus.size(); i++) {
int status = mDeviceStatus.valueAt(i);
if (status == ICameraServiceListener.STATUS_NOT_PRESENT ||
status == ICameraServiceListener.STATUS_ENUMERATING) continue;
cameraIds[idCount] = mDeviceStatus.keyAt(i);
idCount++;
}
}
// The sort logic must match the logic in
// libcameraservice/common/CameraProviderManager.cpp::getAPI1CompatibleCameraDeviceIds
Arrays.sort(cameraIds, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
int s1Int = 0, s2Int = 0;
try {
s1Int = Integer.parseInt(s1);
} catch (NumberFormatException e) {
s1Int = -1;
}
try {
s2Int = Integer.parseInt(s2);
} catch (NumberFormatException e) {
s2Int = -1;
}
// Uint device IDs first
if (s1Int >= 0 && s2Int >= 0) {
return s1Int - s2Int;
} else if (s1Int >= 0) {
return -1;
} else if (s2Int >= 0) {
return 1;
} else {
// Simple string compare if both id are not uint
return s1.compareTo(s2);
}
}});
return cameraIds;
}
public void setTorchMode(String cameraId, boolean enabled) throws CameraAccessException {
synchronized(mLock) {
if (cameraId == null) {
throw new IllegalArgumentException("cameraId was null");
}
ICameraService cameraService = getCameraService();
if (cameraService == null) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service is currently unavailable");
}
try {
cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder);
} catch(ServiceSpecificException e) {
throwAsPublicException(e);
} catch (RemoteException e) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service is currently unavailable");
}
}
}
private void handleRecoverableSetupErrors(ServiceSpecificException e) {
switch (e.errorCode) {
case ICameraService.ERROR_DISCONNECTED:
Log.w(TAG, e.getMessage());
break;
default:
throw new IllegalStateException(e);
}
}
private boolean isAvailable(int status) {
switch (status) {
case ICameraServiceListener.STATUS_PRESENT:
return true;
default:
return false;
}
}
private boolean validStatus(int status) {
switch (status) {
case ICameraServiceListener.STATUS_NOT_PRESENT:
case ICameraServiceListener.STATUS_PRESENT:
case ICameraServiceListener.STATUS_ENUMERATING:
case ICameraServiceListener.STATUS_NOT_AVAILABLE:
return true;
default:
return false;
}
}
private boolean validTorchStatus(int status) {
switch (status) {
case ICameraServiceListener.TORCH_STATUS_NOT_AVAILABLE:
case ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON:
case ICameraServiceListener.TORCH_STATUS_AVAILABLE_OFF:
return true;
default:
return false;
}
}
private void postSingleAccessPriorityChangeUpdate(final AvailabilityCallback callback,
final Executor executor) {
final long ident = Binder.clearCallingIdentity();
try {
executor.execute(
new Runnable() {
@Override
public void run() {
callback.onCameraAccessPrioritiesChanged();
}
});
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void postSingleUpdate(final AvailabilityCallback callback, final Executor executor,
final String id, final int status) {
if (isAvailable(status)) {
final long ident = Binder.clearCallingIdentity();
try {
executor.execute(
new Runnable() {
@Override
public void run() {
callback.onCameraAvailable(id);
}
});
} finally {
Binder.restoreCallingIdentity(ident);
}
} else {
final long ident = Binder.clearCallingIdentity();
try {
executor.execute(
new Runnable() {
@Override
public void run() {
callback.onCameraUnavailable(id);
}
});
} finally {
Binder.restoreCallingIdentity(ident);
}
}
}
private void postSingleTorchUpdate(final TorchCallback callback, final Executor executor,
final String id, final int status) {
switch(status) {
case ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON:
case ICameraServiceListener.TORCH_STATUS_AVAILABLE_OFF: {
final long ident = Binder.clearCallingIdentity();
try {
executor.execute(() -> {
callback.onTorchModeChanged(id, status ==
ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON);
});
} finally {
Binder.restoreCallingIdentity(ident);
}
}
break;
default: {
final long ident = Binder.clearCallingIdentity();
try {
executor.execute(() -> {
callback.onTorchModeUnavailable(id);
});
} finally {
Binder.restoreCallingIdentity(ident);
}
}
break;
}
}
/**
* Send the state of all known cameras to the provided listener, to initialize
* the listener's knowledge of camera state.
*/
private void updateCallbackLocked(AvailabilityCallback callback, Executor executor) {
for (int i = 0; i < mDeviceStatus.size(); i++) {
String id = mDeviceStatus.keyAt(i);
Integer status = mDeviceStatus.valueAt(i);
postSingleUpdate(callback, executor, id, status);
}
}
private void onStatusChangedLocked(int status, String id) {
if (DEBUG) {
Log.v(TAG,
String.format("Camera id %s has status changed to 0x%x", id, status));
}
if (!validStatus(status)) {
Log.e(TAG, String.format("Ignoring invalid device %s status 0x%x", id,
status));
return;
}
Integer oldStatus;
if (status == ICameraServiceListener.STATUS_NOT_PRESENT) {
oldStatus = mDeviceStatus.remove(id);
} else {
oldStatus = mDeviceStatus.put(id, status);
}
if (oldStatus != null && oldStatus == status) {
if (DEBUG) {
Log.v(TAG, String.format(
"Device status changed to 0x%x, which is what it already was",
status));
}
return;
}
// TODO: consider abstracting out this state minimization + transition
// into a separate
// more easily testable class
// i.e. (new State()).addState(STATE_AVAILABLE)
// .addState(STATE_NOT_AVAILABLE)
// .addTransition(STATUS_PRESENT, STATE_AVAILABLE),
// .addTransition(STATUS_NOT_PRESENT, STATE_NOT_AVAILABLE)
// .addTransition(STATUS_ENUMERATING, STATE_NOT_AVAILABLE);
// .addTransition(STATUS_NOT_AVAILABLE, STATE_NOT_AVAILABLE);
// Translate all the statuses to either 'available' or 'not available'
// available -> available => no new update
// not available -> not available => no new update
if (oldStatus != null && isAvailable(status) == isAvailable(oldStatus)) {
if (DEBUG) {
Log.v(TAG,
String.format(
"Device status was previously available (%b), " +
" and is now again available (%b)" +
"so no new client visible update will be sent",
isAvailable(oldStatus), isAvailable(status)));
}
return;
}
final int callbackCount = mCallbackMap.size();
for (int i = 0; i < callbackCount; i++) {
Executor executor = mCallbackMap.valueAt(i);
final AvailabilityCallback callback = mCallbackMap.keyAt(i);
postSingleUpdate(callback, executor, id, status);
}
} // onStatusChangedLocked
private void updateTorchCallbackLocked(TorchCallback callback, Executor executor) {
for (int i = 0; i < mTorchStatus.size(); i++) {
String id = mTorchStatus.keyAt(i);
Integer status = mTorchStatus.valueAt(i);
postSingleTorchUpdate(callback, executor, id, status);
}
}
private void onTorchStatusChangedLocked(int status, String id) {
if (DEBUG) {
Log.v(TAG,
String.format("Camera id %s has torch status changed to 0x%x", id, status));
}
if (!validTorchStatus(status)) {
Log.e(TAG, String.format("Ignoring invalid device %s torch status 0x%x", id,
status));
return;
}
Integer oldStatus = mTorchStatus.put(id, status);
if (oldStatus != null && oldStatus == status) {
if (DEBUG) {
Log.v(TAG, String.format(
"Torch status changed to 0x%x, which is what it already was",
status));
}
return;
}
final int callbackCount = mTorchCallbackMap.size();
for (int i = 0; i < callbackCount; i++) {
final Executor executor = mTorchCallbackMap.valueAt(i);
final TorchCallback callback = mTorchCallbackMap.keyAt(i);
postSingleTorchUpdate(callback, executor, id, status);
}
} // onTorchStatusChangedLocked
/**
* Register a callback to be notified about camera device availability with the
* global listener singleton.
*
* @param callback the new callback to send camera availability notices to
* @param executor The executor which should invoke the callback. May not be null.
*/
public void registerAvailabilityCallback(AvailabilityCallback callback, Executor executor) {
synchronized (mLock) {
connectCameraServiceLocked();
Executor oldExecutor = mCallbackMap.put(callback, executor);
// For new callbacks, provide initial availability information
if (oldExecutor == null) {
updateCallbackLocked(callback, executor);
}
// If not connected to camera service, schedule a reconnect to camera service.
if (mCameraService == null) {
scheduleCameraServiceReconnectionLocked();
}
}
}
/**
* Remove a previously-added callback; the callback will no longer receive connection and
* disconnection callbacks, and is no longer referenced by the global listener singleton.
*
* @param callback The callback to remove from the notification list
*/
public void unregisterAvailabilityCallback(AvailabilityCallback callback) {
synchronized (mLock) {
mCallbackMap.remove(callback);
}
}
public void registerTorchCallback(TorchCallback callback, Executor executor) {
synchronized(mLock) {
connectCameraServiceLocked();
Executor oldExecutor = mTorchCallbackMap.put(callback, executor);
// For new callbacks, provide initial torch information
if (oldExecutor == null) {
updateTorchCallbackLocked(callback, executor);
}
// If not connected to camera service, schedule a reconnect to camera service.
if (mCameraService == null) {
scheduleCameraServiceReconnectionLocked();
}
}
}
public void unregisterTorchCallback(TorchCallback callback) {
synchronized(mLock) {
mTorchCallbackMap.remove(callback);
}
}
/**
* Callback from camera service notifying the process about camera availability changes
*/
@Override
public void onStatusChanged(int status, String cameraId) throws RemoteException {
synchronized(mLock) {
onStatusChangedLocked(status, cameraId);
}
}
@Override
public void onTorchStatusChanged(int status, String cameraId) throws RemoteException {
synchronized (mLock) {
onTorchStatusChangedLocked(status, cameraId);
}
}
@Override
public void onCameraAccessPrioritiesChanged() {
synchronized (mLock) {
final int callbackCount = mCallbackMap.size();
for (int i = 0; i < callbackCount; i++) {
Executor executor = mCallbackMap.valueAt(i);
final AvailabilityCallback callback = mCallbackMap.keyAt(i);
postSingleAccessPriorityChangeUpdate(callback, executor);
}
}
}
/**
* Try to connect to camera service after some delay if any client registered camera
* availability callback or torch status callback.
*/
private void scheduleCameraServiceReconnectionLocked() {
if (mCallbackMap.isEmpty() && mTorchCallbackMap.isEmpty()) {
// Not necessary to reconnect camera service if no client registers a callback.
return;
}
if (DEBUG) {
Log.v(TAG, "Reconnecting Camera Service in " + CAMERA_SERVICE_RECONNECT_DELAY_MS +
" ms");
}
try {
mScheduler.schedule(() -> {
ICameraService cameraService = getCameraService();
if (cameraService == null) {
synchronized(mLock) {
if (DEBUG) {
Log.v(TAG, "Reconnecting Camera Service failed.");
}
scheduleCameraServiceReconnectionLocked();
}
}
}, CAMERA_SERVICE_RECONNECT_DELAY_MS, TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
Log.e(TAG, "Failed to schedule camera service re-connect: " + e);
}
}
/**
* Listener for camera service death.
*
* <p>The camera service isn't supposed to die under any normal circumstances, but can be
* turned off during debug, or crash due to bugs. So detect that and null out the interface
* object, so that the next calls to the manager can try to reconnect.</p>
*/
public void binderDied() {
synchronized(mLock) {
// Only do this once per service death
if (mCameraService == null) return;
mCameraService = null;
// Tell listeners that the cameras and torch modes are unavailable and schedule a
// reconnection to camera service. When camera service is reconnected, the camera
// and torch statuses will be updated.
for (int i = 0; i < mDeviceStatus.size(); i++) {
String cameraId = mDeviceStatus.keyAt(i);
onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, cameraId);
}
for (int i = 0; i < mTorchStatus.size(); i++) {
String cameraId = mTorchStatus.keyAt(i);
onTorchStatusChangedLocked(ICameraServiceListener.TORCH_STATUS_NOT_AVAILABLE,
cameraId);
}
scheduleCameraServiceReconnectionLocked();
}
}
} // CameraManagerGlobal
} // CameraManager