/* * 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 static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.content.Context.DEVICE_ID_DEFAULT; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.app.ActivityManager; import android.app.TaskInfo; import android.app.compat.CompatChanges; import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; import android.hardware.CameraExtensionSessionStats; import android.hardware.CameraStatus; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; import android.hardware.camera2.CameraDevice.StateCallback; import android.hardware.camera2.impl.CameraDeviceImpl; import android.hardware.camera2.impl.CameraDeviceSetupImpl; import android.hardware.camera2.impl.CameraInjectionSessionImpl; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfiguration; import android.hardware.camera2.utils.CameraIdAndSessionConfiguration; import android.hardware.camera2.utils.ConcurrentCameraIdCombination; import android.hardware.camera2.utils.ExceptionUtils; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Handler; import android.os.HandlerExecutor; import android.os.HandlerThread; 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.ArraySet; import android.util.Log; import android.util.Pair; import android.util.Size; import android.view.Display; import com.android.internal.camera.flags.Flags; import com.android.internal.util.ArrayUtils; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; 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; /** *

A system service manager for detecting, characterizing, and connecting to * {@link CameraDevice CameraDevices}.

* *

For more details about communicating with camera devices, read the Camera * developer guide or the {@link android.hardware.camera2 camera2} * package documentation.

*/ @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; /** * Caches the mapping between a logical camera ID and 'MultiResolutionStreamConfigurationMap' * that is calculated by {@link #getPhysicalCameraMultiResolutionConfigs} as the calculation * might take many binder calls. *

* Note, this is a map of maps. The structure is: *

     * {
     *     logicalCameraId_1 -> {
     *         physicalCameraId_1 -> [
     *             streamConfiguration_1,
     *             streamConfiguration_2,
     *             ...
     *         ],
     *         physicalCameraId_2 -> [...],
     *         ...
     *     },
     *     logicalCameraId_2 -> {
     *         ...
     *     },
     *     ...
     * }
     * 
*

*/ private final Map> mCameraIdToMultiResolutionStreamConfigurationMap = new HashMap<>(); private final Context mContext; private final Object mLock = new Object(); private static final String CAMERA_OPEN_CLOSE_LISTENER_PERMISSION = "android.permission.CAMERA_OPEN_CLOSE_LISTENER"; private final boolean mHasOpenCloseListenerPermission; private VirtualDeviceManager mVirtualDeviceManager; /** * Force camera output to be rotated to portrait orientation on landscape cameras. * Many apps do not handle this situation and display stretched images otherwise. * @hide */ @ChangeId @Overridable @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @TestApi public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; /** * System property for allowing the above * @hide */ @TestApi public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait"; /** * Does not override landscape feed to portrait. * * @hide */ @TestApi @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE; /** * Crops and rotates landscape camera feed to portrait, and changes sensor orientation to * portrait. * * @hide */ @TestApi @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT; /** * Crops and rotates landscape camera feed to portrait, but doesn't change sensor orientation. * * @hide */ @TestApi @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) public static final int ROTATION_OVERRIDE_ROTATION_ONLY = ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY; /** * Enable physical camera availability callbacks when the logical camera is unavailable * *

Previously once a logical camera becomes unavailable, no * {@link AvailabilityCallback#onPhysicalCameraAvailable} or * {@link AvailabilityCallback#onPhysicalCameraUnavailable} will * be called until the logical camera becomes available again. The * results in the app opening the logical camera not able to * receive physical camera availability change.

* *

With this change, the {@link * AvailabilityCallback#onPhysicalCameraAvailable} and {@link * AvailabilityCallback#onPhysicalCameraUnavailable} can still be * called while the logical camera is unavailable.

*/ @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) private static final long ENABLE_PHYSICAL_CAMERA_CALLBACK_FOR_UNAVAILABLE_LOGICAL_CAMERA = 244358506L; /** * @hide */ public CameraManager(Context context) { synchronized(mLock) { mContext = context; mHasOpenCloseListenerPermission = mContext.checkSelfPermission(CAMERA_OPEN_CLOSE_LISTENER_PERMISSION) == PackageManager.PERMISSION_GRANTED; } } /** * @hide */ public interface DeviceStateListener { void onDeviceStateChanged(boolean folded); } private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { private final int[] mFoldedDeviceStates; private ArrayList> mDeviceStateListeners = new ArrayList<>(); private boolean mFoldedDeviceState; public FoldStateListener(Context context) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); } private synchronized void handleStateChange(int state) { boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); mFoldedDeviceState = folded; Iterator> it = mDeviceStateListeners.iterator(); while(it.hasNext()) { DeviceStateListener callback = it.next().get(); if (callback != null) { callback.onDeviceStateChanged(folded); } else { it.remove(); } } } public synchronized void addDeviceStateListener(DeviceStateListener listener) { listener.onDeviceStateChanged(mFoldedDeviceState); mDeviceStateListeners.removeIf(l -> l.get() == null); mDeviceStateListeners.add(new WeakReference<>(listener)); } @SuppressWarnings("FlaggedApi") @Override public void onDeviceStateChanged(DeviceState state) { // Suppressing the FlaggedAPI warning as this specific API isn't new, just moved to // system API which requires it to be flagged. handleStateChange(state.getIdentifier()); } } /** * Register a {@link CameraCharacteristics} device state listener * * @param chars Camera characteristics that need to receive device state updates * * @hide */ public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) { CameraManagerGlobal.get().registerDeviceStateListener(chars, mContext); } /** * Return the list of currently connected camera devices by identifier, including * cameras that may be in use by other camera API clients. * *

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.

* *

This list doesn't contain physical cameras that can only be used as part of a logical * multi-camera device.

* * @return The list of currently connected camera devices. */ @NonNull public String[] getCameraIdList() throws CameraAccessException { return CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Similar to getCameraIdList(). However, getCamerIdListNoLazy() necessarily communicates with * cameraserver in order to get the list of camera ids. This is to facilitate testing since some * camera ids may go 'offline' without callbacks from cameraserver because of changes in * SYSTEM_CAMERA permissions (though this is not a changeable permission, tests may call * adopt(drop)ShellPermissionIdentity() and effectively change their permissions). This call * affects the camera ids returned by getCameraIdList() as well. Tests which do adopt shell * permission identity should not mix getCameraIdList() and getCameraListNoLazyCalls(). * * @hide */ @TestApi public String[] getCameraIdListNoLazy() throws CameraAccessException { return CameraManagerGlobal.get().getCameraIdListNoLazy(mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Return the set of combinations of currently connected camera device identifiers, which * support configuring camera device sessions concurrently. * *

The devices in these combinations can be concurrently configured by the same * client camera application. Using these camera devices concurrently by two different * applications is not guaranteed to be supported, however.

* *

For concurrent operation, in chronological order : *

    *
  • Applications must first close any open cameras that have sessions configured, using * {@link CameraDevice#close}.
  • *
  • All camera devices intended to be operated concurrently, must be opened using * {@link #openCamera}, before configuring sessions on any of the camera devices.
  • *
*

*

Each device in a combination, is guaranteed to support stream combinations which may be * obtained by querying {@link #getCameraCharacteristics} for the key * {@link android.hardware.camera2.CameraCharacteristics#SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS}.

* *

For concurrent operation, if a camera device has a non null zoom ratio range as specified * by * {@link android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE}, * its complete zoom ratio range may not apply. Applications can use * {@link android.hardware.camera2.CaptureRequest#CONTROL_ZOOM_RATIO} >=1 and <= * {@link android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM} * during concurrent operation. *

* *

The set of combinations may include camera devices that may be in use by other camera API * clients.

* *

Concurrent camera extension sessions {@link CameraExtensionSession} are not currently * supported.

* *

The set of combinations doesn't contain physical cameras that can only be used as * part of a logical multi-camera device.

* *

If a new camera id becomes available through * {@link AvailabilityCallback#onCameraUnavailable(String)}, clients can call * this method to check if new combinations of camera ids which can stream concurrently are * available. * * @return The set of combinations of currently connected camera devices, that may have * sessions configured concurrently. The set of combinations will be empty if no such * combinations are supported by the camera subsystem. * * @throws CameraAccessException if the camera device has been disconnected. */ @NonNull public Set> getConcurrentCameraIds() throws CameraAccessException { return CameraManagerGlobal.get().getConcurrentCameraIds(mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Checks whether the provided set of camera devices and their corresponding * {@link SessionConfiguration} can be configured concurrently. * *

This method performs a runtime check of the given {@link SessionConfiguration} and camera * id combinations. The result confirms whether or not the passed session configurations can be * successfully used to create camera capture sessions concurrently, on the given camera * devices using {@link CameraDevice#createCaptureSession(SessionConfiguration)}. *

* *

The method can be called at any point before, during and after active capture sessions. * It will not impact normal camera behavior in any way and must complete significantly * faster than creating a regular or constrained capture session.

* *

Although this method is faster than creating a new capture session, it is not intended * to be used for exploring the entire space of supported concurrent stream combinations. The * available mandatory concurrent stream combinations may be obtained by querying * {@link #getCameraCharacteristics} for the key * {@link android.hardware.camera2.CameraCharacteristics#SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS}.

* *

Note that session parameters will be ignored and calls to * {@link SessionConfiguration#setSessionParameters} are not required.

* * @return {@code true} if the given combination of session configurations and corresponding * camera ids are concurrently supported by the camera sub-system, * {@code false} otherwise OR if the set of camera devices provided is not a subset of * those returned by {@link #getConcurrentCameraIds}. * * @throws CameraAccessException if one of the camera devices queried is no longer connected. * */ @RequiresPermission(android.Manifest.permission.CAMERA) public boolean isConcurrentSessionConfigurationSupported( @NonNull Map cameraIdAndSessionConfig) throws CameraAccessException { return CameraManagerGlobal.get().isConcurrentSessionConfigurationSupported( cameraIdAndSessionConfig, mContext.getApplicationInfo().targetSdkVersion, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Register a callback to be notified about camera device availability. * *

Registering the same callback again will replace the handler with the * new one provided.

* *

The first time a callback is registered, it is immediately called * with the availability status of all currently known camera devices.

* *

{@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.

* *

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.

* * @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), mHasOpenCloseListenerPermission, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Register a callback to be notified about camera device availability. * *

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}.

* *

Note: If the order between some availability callbacks matters, the implementation of the * executor should handle those callbacks in the same thread to maintain the callbacks' order. * Some examples are:

* *
    * *
  • {@link AvailabilityCallback#onCameraAvailable} and * {@link AvailabilityCallback#onCameraUnavailable} of the same camera ID.
  • * *
  • {@link AvailabilityCallback#onCameraAvailable} or * {@link AvailabilityCallback#onCameraUnavailable} of a logical multi-camera, and {@link * AvailabilityCallback#onPhysicalCameraUnavailable} or * {@link AvailabilityCallback#onPhysicalCameraAvailable} of its physical * cameras.
  • * *
* * @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, mHasOpenCloseListenerPermission, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Remove a previously-added callback; the callback will no longer receive connection and * disconnection callbacks. * *

Removing a callback that isn't registered has no effect.

* * @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. * *

Registering the same callback again will replace the handler with the * new one provided.

* *

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.

* *

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.

* * @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), mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Register a callback to be notified about torch mode status. * *

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}.

* * @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, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Remove a previously-added callback; the callback will no longer receive torch mode status * callbacks. * *

Removing a callback that isn't registered has no effect.

* * @param callback The callback to remove from the notification list */ public void unregisterTorchCallback(@NonNull TorchCallback callback) { CameraManagerGlobal.get().unregisterTorchCallback(callback); } /** @hide */ public int getDevicePolicyFromContext(@NonNull Context context) { if (context.getDeviceId() == DEVICE_ID_DEFAULT || !android.companion.virtual.flags.Flags.virtualCamera()) { return DEVICE_POLICY_DEFAULT; } if (mVirtualDeviceManager == null) { mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class); } return mVirtualDeviceManager.getDevicePolicy(context.getDeviceId(), POLICY_TYPE_CAMERA); } // TODO(b/147726300): Investigate how to support foldables/multi-display devices. private Size getDisplaySize() { Size ret = new Size(0, 0); try { DisplayManager displayManager = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); if (display != null) { Point sz = new Point(); display.getRealSize(sz); int width = sz.x; int height = sz.y; if (height > width) { height = width; width = sz.y; } ret = new Size(width, height); } else { Log.e(TAG, "Invalid default display!"); } } catch (Exception e) { Log.e(TAG, "getDisplaySize Failed. " + e); } return ret; } /** * Get all physical cameras' multi-resolution stream configuration map * *

For a logical multi-camera, query the map between physical camera id and * the physical camera's multi-resolution stream configuration. This map is in turn * combined to form the logical camera's multi-resolution stream configuration map.

* *

For an ultra high resolution camera, directly use * android.scaler.physicalCameraMultiResolutionStreamConfigurations as the camera device's * multi-resolution stream configuration map.

*/ private Map getPhysicalCameraMultiResolutionConfigs( String cameraId, CameraMetadataNative info, ICameraService cameraService) throws CameraAccessException { if (mCameraIdToMultiResolutionStreamConfigurationMap.containsKey(cameraId)) { return mCameraIdToMultiResolutionStreamConfigurationMap.get(cameraId); } HashMap multiResolutionStreamConfigurations = new HashMap<>(); mCameraIdToMultiResolutionStreamConfigurationMap.put(cameraId, multiResolutionStreamConfigurations); Boolean multiResolutionStreamSupported = info.get( CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_SUPPORTED); if (multiResolutionStreamSupported == null || !multiResolutionStreamSupported) { return multiResolutionStreamConfigurations; } // Query the characteristics of all physical sub-cameras, and combine the multi-resolution // stream configurations. Alternatively, for ultra-high resolution camera, directly use // its multi-resolution stream configurations. Note that framework derived formats such as // HEIC and DEPTH_JPEG aren't supported as multi-resolution input or output formats. Set physicalCameraIds = info.getPhysicalCameraIds(); if (physicalCameraIds.size() == 0 && info.isUltraHighResolutionSensor()) { StreamConfiguration[] configs = info.get(CameraCharacteristics. SCALER_PHYSICAL_CAMERA_MULTI_RESOLUTION_STREAM_CONFIGURATIONS); if (configs != null) { multiResolutionStreamConfigurations.put(cameraId, configs); } return multiResolutionStreamConfigurations; } try { for (String physicalCameraId : physicalCameraIds) { CameraMetadataNative physicalCameraInfo = cameraService.getCameraCharacteristics(physicalCameraId, mContext.getApplicationInfo().targetSdkVersion, /*rotationOverride*/ ICameraService.ROTATION_OVERRIDE_NONE, DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT); StreamConfiguration[] configs = physicalCameraInfo.get( CameraCharacteristics. SCALER_PHYSICAL_CAMERA_MULTI_RESOLUTION_STREAM_CONFIGURATIONS); if (configs != null) { multiResolutionStreamConfigurations.put(physicalCameraId, configs); } } } catch (RemoteException e) { ServiceSpecificException sse = new ServiceSpecificException( ICameraService.ERROR_DISCONNECTED, "Camera service is currently unavailable"); throw ExceptionUtils.throwAsPublicException(sse); } return multiResolutionStreamConfigurations; } /** *

Query the capabilities of a camera device. These capabilities are * immutable for a given camera.

* *

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}

* *

Also starting with API level 29, while most basic camera information is still available * even without the CAMERA permission, some values are not available to apps that do not hold * that permission. The keys not available are listed by * {@link CameraCharacteristics#getKeysNeedingPermission}.

* * @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 { return getCameraCharacteristics(cameraId, getRotationOverride(mContext)); } /** *

Query the capabilities of a camera device. These capabilities are * immutable for a given camera.

* *

The value of {@link CameraCharacteristics.SENSOR_ORIENTATION} will change for landscape * cameras depending on whether overrideToPortrait is enabled. If enabled, these cameras will * appear to be portrait orientation instead, provided that the override is supported by the * camera device. Only devices that can be opened by {@link #openCamera} will report a changed * {@link CameraCharacteristics.SENSOR_ORIENTATION}.

* * @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. * @param overrideToPortrait Whether to apply the landscape to portrait override. * @return The properties of the given camera * * @hide */ @TestApi @NonNull public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId, boolean overrideToPortrait) throws CameraAccessException { return getCameraCharacteristics(cameraId, overrideToPortrait ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT : ICameraService.ROTATION_OVERRIDE_NONE); } @NonNull private CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId, int rotationOverride) throws CameraAccessException { CameraCharacteristics characteristics = null; if (CameraManagerGlobal.sCameraServiceDisabled) { throw new IllegalArgumentException("No cameras available on device"); } synchronized (mLock) { ICameraService cameraService = CameraManagerGlobal.get().getCameraService(); if (cameraService == null) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable"); } try { CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, mContext.getApplicationInfo().targetSdkVersion, rotationOverride, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); characteristics = prepareCameraCharacteristics(cameraId, info, cameraService); } catch (ServiceSpecificException e) { throw ExceptionUtils.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); } } registerDeviceStateListener(characteristics); return characteristics; } /** * Utility method to take a {@link CameraMetadataNative} object and wrap it into a * {@link CameraCharacteristics} object that has all required fields and keys set and is fit * for apps to consume. * * @param cameraId camera Id that the CameraMetadataNative was fetched for. * @param metadata base CameraMetadataNative to be wrapped * @param cameraService remote cameraservice instance to be used if binder calls need * to be made. * @return A CameraCharacteristics object that can be used by the apps. * @hide */ @NonNull public CameraCharacteristics prepareCameraCharacteristics( @NonNull String cameraId, CameraMetadataNative metadata, ICameraService cameraService) throws CameraAccessException { synchronized (mLock) { try { metadata.setCameraId(Integer.parseInt(cameraId)); } catch (NumberFormatException e) { Log.v(TAG, "Failed to parse camera Id " + cameraId + " to integer"); } boolean hasConcurrentStreams = CameraManagerGlobal.get().cameraIdHasConcurrentStreamsLocked(cameraId, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); metadata.setHasMandatoryConcurrentStreams(hasConcurrentStreams); Size displaySize = getDisplaySize(); metadata.setDisplaySize(displaySize); Map multiResolutionSizeMap = getPhysicalCameraMultiResolutionConfigs(cameraId, metadata, cameraService); if (!multiResolutionSizeMap.isEmpty()) { metadata.setMultiResolutionStreamConfigurationMap(multiResolutionSizeMap); } return new CameraCharacteristics(metadata); } } /** *

Query the camera extension capabilities of a camera device.

* * @param cameraId The id of the camera device to query. This must be a standalone * camera ID which can be directly opened by {@link #openCamera}. * @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 CameraExtensionCharacteristics * @see CameraDevice#createExtensionSession(ExtensionSessionConfiguration) * @see CameraExtensionSession */ @NonNull public CameraExtensionCharacteristics getCameraExtensionCharacteristics( @NonNull String cameraId) throws CameraAccessException { CameraCharacteristics chars = getCameraCharacteristics(cameraId); Map characteristicsMap = getPhysicalIdToCharsMap(chars); characteristicsMap.put(cameraId, chars); return new CameraExtensionCharacteristics(mContext, cameraId, characteristicsMap); } /** * @hide */ public Map getPhysicalIdToCharsMap( CameraCharacteristics chars) throws CameraAccessException { HashMap physicalIdsToChars = new HashMap(); Set physicalCameraIds = chars.getPhysicalCameraIds(); for (String physicalCameraId : physicalCameraIds) { CameraCharacteristics physicalChars = getCameraCharacteristics(physicalCameraId); physicalIdsToChars.put(physicalCameraId, physicalChars); } return physicalIdsToChars; } /** * Returns a {@link CameraDevice.CameraDeviceSetup} object for the given {@code cameraId}, * which provides limited access to CameraDevice setup and query functionality without * requiring an {@link #openCamera} call. The {@link CameraDevice} can later be obtained either * by calling {@link #openCamera}, or {@link CameraDevice.CameraDeviceSetup#openCamera}. * *

Support for {@link CameraDevice.CameraDeviceSetup} for a given {@code cameraId} must be * checked with {@link #isCameraDeviceSetupSupported}. If {@code isCameraDeviceSetupSupported} * returns {@code false} for a {@code cameraId}, this method will throw an * {@link UnsupportedOperationException}

* * @param cameraId The unique identifier of the camera device for which * {@link CameraDevice.CameraDeviceSetup} object must be constructed. This * identifier must be present in {@link #getCameraIdList()} * * @return {@link CameraDevice.CameraDeviceSetup} object corresponding to the provided * {@code cameraId} * * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not * match any device in {@link #getCameraIdList()}. * @throws CameraAccessException if the camera device is not accessible * @throws UnsupportedOperationException if {@link CameraDevice.CameraDeviceSetup} instance * cannot be constructed for the given {@code cameraId}, i.e. * {@link #isCameraDeviceSetupSupported} returns false. * * @see CameraDevice.CameraDeviceSetup * @see #getCameraIdList() * @see #openCamera */ @NonNull @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) public CameraDevice.CameraDeviceSetup getCameraDeviceSetup(@NonNull String cameraId) throws CameraAccessException { // isCameraDeviceSetup does all the error checking we need. if (!isCameraDeviceSetupSupported(cameraId)) { throw new UnsupportedOperationException( "CameraDeviceSetup is not supported for Camera ID: " + cameraId); } return getCameraDeviceSetupUnsafe(cameraId); } /** * Creates and returns a {@link CameraDeviceSetup} instance without any error checking. To * be used (carefully) by callers who are sure that CameraDeviceSetup instance can be legally * created and don't want to pay the latency cost of calling {@link #getCameraDeviceSetup}. */ private CameraDevice.CameraDeviceSetup getCameraDeviceSetupUnsafe(@NonNull String cameraId) { return new CameraDeviceSetupImpl(cameraId, /*cameraManager=*/ this, mContext); } /** * Checks a Camera Device's characteristics to ensure that a * {@link CameraDevice.CameraDeviceSetup} instance can be constructed for a given * {@code cameraId}. If this method returns false for a {@code cameraId}, calling * {@link #getCameraDeviceSetup} for that {@code cameraId} will throw an * {@link UnsupportedOperationException}. * *

{@link CameraDevice.CameraDeviceSetup} is supported for all devices that report * {@link CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION} > * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}

* * @param cameraId The unique identifier of the camera device for which * {@link CameraDevice.CameraDeviceSetup} support is being queried. This * identifier must be present in {@link #getCameraIdList()}. * * @return {@code true} if {@link CameraDevice.CameraDeviceSetup} object can be constructed * for the provided {@code cameraId}; {@code false} otherwise. * * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not * match any device in {@link #getCameraIdList()}. * @throws CameraAccessException if the camera device is not accessible * * @see CameraCharacteristics#INFO_SESSION_CONFIGURATION_QUERY_VERSION * @see CameraDevice.CameraDeviceSetup * @see #getCameraDeviceSetup(String) * @see #getCameraIdList() */ @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) public boolean isCameraDeviceSetupSupported(@NonNull String cameraId) throws CameraAccessException { if (cameraId == null) { throw new IllegalArgumentException("Camera ID was null"); } if (CameraManagerGlobal.sCameraServiceDisabled || !Arrays.asList(CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(), getDevicePolicyFromContext(mContext))).contains(cameraId)) { throw new IllegalArgumentException( "Camera ID '" + cameraId + "' not available on device."); } CameraCharacteristics chars = getCameraCharacteristics(cameraId); return CameraDeviceSetupImpl.isCameraDeviceSetupSupported(chars); } /** * 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, final int oomScoreOffset, int rotationOverride) throws CameraAccessException { CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); CameraDevice device = null; synchronized (mLock) { ICameraDeviceUser cameraUser = null; CameraDevice.CameraDeviceSetup cameraDeviceSetup = null; if (Flags.cameraDeviceSetup() && CameraDeviceSetupImpl.isCameraDeviceSetupSupported(characteristics)) { cameraDeviceSetup = getCameraDeviceSetupUnsafe(cameraId); } android.hardware.camera2.impl.CameraDeviceImpl deviceImpl = new CameraDeviceImpl( cameraId, callback, executor, characteristics, this, mContext.getApplicationInfo().targetSdkVersion, mContext, cameraDeviceSetup); ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks(); try { 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(), mContext.getAttributionTag(), uid, oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, rotationOverride, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } 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 throw ExceptionUtils.throwAsPublicException(e); } } else { // Unexpected failure - rethrow throw ExceptionUtils.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); throw ExceptionUtils.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. * *

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.

* *

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. * Opening the same camera ID twice in the same application will similarly cause the * {@link android.hardware.camera2.CameraDevice.StateCallback#onDisconnected} callback * being fired for the {@link CameraDevice} from the first open call and all ongoing tasks * being dropped.

* *

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}

* *

Before API level 30, when the application tries to open multiple {@link CameraDevice} of * different IDs and the device does not support opening such combination, either the * {@link #openCamera} will fail and throw a {@link CameraAccessException} or one or more of * already opened {@link CameraDevice} will be disconnected and receive * {@link android.hardware.camera2.CameraDevice.StateCallback#onDisconnected} callback. Which * behavior will happen depends on the device implementation and can vary on different devices. * Starting in API level 30, if the device does not support the combination of cameras being * opened, it is guaranteed the {@link #openCamera} call will fail and none of existing * {@link CameraDevice} will be disconnected.

* * *

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).

* *

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}.

* * @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, is being used by a higher-priority camera API client, or the device * has reached its maximal resource and cannot open this camera device. * * @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. Also specify overrideToPortrait for testing. * * @param cameraId * The unique identifier of the camera device to open * @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}. * @param callback * The callback which is invoked once the camera is opened * @param overrideToPortrait * Whether to apply the landscape to portrait override, using rotate and crop. * * @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 * * @hide */ @TestApi @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String cameraId, boolean overrideToPortrait, @Nullable Handler handler, @NonNull final CameraDevice.StateCallback callback) throws CameraAccessException { openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler), USE_CALLING_UID, /*oomScoreOffset*/0, overrideToPortrait ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT : ICameraService.ROTATION_OVERRIDE_NONE); } /** * Open a connection to a camera with the given ID. * *

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}.

* *

Do note that typically callbacks are expected to be dispatched * by the executor in a single thread. If the executor uses two or * more threads to dispatch callbacks, then clients must ensure correct * synchronization and must also be able to handle potentially different * ordering of the incoming callbacks.

* * @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. Also specify what oom score must be offset * by cameraserver for this client. This api can be useful for system * components which want to assume a lower priority (for camera arbitration) than other clients * which it might contend for camera devices with. Increasing the oom score of a client reduces * its priority when the camera framework manages camera arbitration. * Considering typical use cases: * * 1) oom score(apps hosting activities visible to the user) - oom score(of a foreground app) * is approximately 100. * * 2) The oom score (process which hosts components which that are perceptible to the user / * native vendor camera clients) - oom (foreground app) is approximately 200. * * 3) The oom score (process which is cached hosting activities not visible) - oom (foreground * app) is approximately 999. * *

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}.

* * @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 * @param oomScoreOffset * The value by which the oom score of this client must be offset by the camera * framework in order to assist it with camera arbitration. This value must be > 0. * A positive value lowers the priority of this camera client compared to what the * camera framework would have originally seen. * * @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 * * @hide */ @SystemApi @TestApi @RequiresPermission(allOf = { android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA, }) public void openCamera(@NonNull String cameraId, int oomScoreOffset, @NonNull @CallbackExecutor Executor executor, @NonNull final CameraDevice.StateCallback callback) throws CameraAccessException { if (executor == null) { throw new IllegalArgumentException("executor was null"); } if (oomScoreOffset < 0) { throw new IllegalArgumentException( "oomScoreOffset < 0, cannot increase priority of camera client"); } openCameraForUid(cameraId, callback, executor, USE_CALLING_UID, oomScoreOffset, getRotationOverride(mContext)); } /** * Open a connection to a camera with the given ID, on behalf of another application * specified by clientUid. Also specify the minimum oom score and process state the application * should have, as seen by the cameraserver. * *

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.

* * @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. * @param oomScoreOffset * The minimum oom score that cameraservice must see for this client. * @param rotationOverride * The type of rotation override (none, override_to_portrait, rotation_only) * that should be followed for this camera id connection * @hide */ public void openCameraForUid(@NonNull String cameraId, @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor, int clientUid, int oomScoreOffset, int rotationOverride) 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, oomScoreOffset, rotationOverride); } /** * Open a connection to a camera with the given ID, on behalf of another application * specified by clientUid. * *

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.

* * @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 { openCameraForUid(cameraId, callback, executor, clientUid, /*oomScoreOffset*/0, getRotationOverride(mContext)); } /** * Set the flash unit's torch mode of the camera of the given ID without opening the camera * device. * *

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. *

* *

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, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Set the brightness level of the flashlight associated with the given cameraId in torch * mode. If the torch is OFF and torchStrength is >= 1, torch will turn ON with the * strength level specified in torchStrength. * *

Use * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL} * to check whether the camera device supports flash unit strength control or not. If this value * is greater than 1, applications can call this API to control the flashlight brightness level. *

* *

If {@link #turnOnTorchWithStrengthLevel} is called to change the brightness level of the * flash unit {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will be invoked. * If the new desired strength level is same as previously set level, then this callback will * not be invoked. * If the torch is OFF and {@link #turnOnTorchWithStrengthLevel} is called with level >= 1, * the torch will be turned ON with that brightness level. In this case * {@link CameraManager.TorchCallback#onTorchModeChanged} will also be invoked. *

* *

When the torch is turned OFF via {@link #setTorchMode}, the flashlight brightness level * will reset to default value * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL} * In this case the {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will not be * invoked. *

* *

If torch is enabled via {@link #setTorchMode} after calling * {@link #turnOnTorchWithStrengthLevel} with level N then the flash unit will have the * brightness level N. * Since multiple applications are free to call {@link #setTorchMode}, when the latest * application that turned ON the torch mode exits, the torch mode will be turned OFF * and in this case the brightness level will reset to default level. *

* * @param cameraId * The unique identifier of the camera device that the flash unit belongs to. * @param torchStrength * The desired brightness level to be set for the flash unit in the range 1 to * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}. * * @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, the camera device doesn't have a * flash unit or if torchStrength is not within the range i.e. is greater than * the maximum level * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL} * or <= 0. * */ public void turnOnTorchWithStrengthLevel(@NonNull String cameraId, int torchStrength) throws CameraAccessException { if (CameraManagerGlobal.sCameraServiceDisabled) { throw new IllegalArgumentException("No camera available on device"); } CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * Returns the brightness level of the flash unit associated with the cameraId. * * @param cameraId * The unique identifier of the camera device that the flash unit belongs to. * @return The brightness level of the flash unit associated with cameraId. * When the torch is turned OFF, the strength level will reset to a default level * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}. * In this case the return value will be * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL} * rather than 0. * * @throws CameraAccessException if it failed to access the flash unit. * @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 int getTorchStrengthLevel(@NonNull String cameraId) throws CameraAccessException { if (CameraManagerGlobal.sCameraServiceDisabled) { throw new IllegalArgumentException("No camera available on device."); } return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId, mContext.getDeviceId(), getDevicePolicyFromContext(mContext)); } /** * @hide */ public static int getRotationOverride(@Nullable Context context) { PackageManager packageManager = null; String packageName = null; if (context != null) { packageManager = context.getPackageManager(); packageName = context.getOpPackageName(); } return getRotationOverride(context, packageManager, packageName); } /** * @hide */ public static int getRotationOverride(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { if (com.android.window.flags.Flags.cameraCompatForFreeform()) { return getRotationOverrideInternal(context, packageManager, packageName); } else { return shouldOverrideToPortrait(packageManager, packageName) ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT : ICameraService.ROTATION_OVERRIDE_NONE; } } /** * @hide */ @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) @TestApi public static int getRotationOverrideInternal(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { if (!CameraManagerGlobal.sLandscapeToPortrait) { return ICameraService.ROTATION_OVERRIDE_NONE; } if (context != null) { final ActivityManager activityManager = context.getSystemService(ActivityManager.class); for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) { final TaskInfo taskInfo = appTask.getTaskInfo(); if (taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode != 0 && taskInfo.topActivity != null && taskInfo.topActivity.getPackageName().equals(packageName)) { // WindowManager has requested rotation override. return ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY; } } } if (packageManager != null && packageName != null) { try { return packageManager.getProperty( PackageManager.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT, packageName).getBoolean() ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT : ICameraService.ROTATION_OVERRIDE_NONE; } catch (PackageManager.NameNotFoundException e) { // No such property } } return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT) ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT : ICameraService.ROTATION_OVERRIDE_NONE; } /** * @hide */ @TestApi public static boolean shouldOverrideToPortrait(@Nullable PackageManager packageManager, @Nullable String packageName) { if (!CameraManagerGlobal.sLandscapeToPortrait) { return false; } if (packageManager != null && packageName != null) { try { return packageManager.getProperty( PackageManager.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT, packageName).getBoolean(); } catch (PackageManager.NameNotFoundException e) { // No such property } } return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT); } /** * @hide */ public static boolean physicalCallbacksAreEnabledForUnavailableCamera() { return CompatChanges.isChangeEnabled( ENABLE_PHYSICAL_CAMERA_CALLBACK_FOR_UNAVAILABLE_LOGICAL_CAMERA); } /** * A callback for camera devices becoming available or unavailable to open. * *

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.

* *

Extend this callback and pass an instance of the subclass to * {@link CameraManager#registerAvailabilityCallback} to be notified of such availability * changes.

* * @see #registerAvailabilityCallback */ public static abstract class AvailabilityCallback { private int mDeviceId; private int mDevicePolicy; /** * A new camera has become available to use. * *

The default implementation of this method does nothing.

* * @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. * *

If an application had an active CameraDevice instance for the * now-disconnected camera, that application will receive a * {@link CameraDevice.StateCallback#onDisconnected disconnection error}.

* *

The default implementation of this method does nothing.

* * @param cameraId The unique identifier of the disconnected camera. */ public void onCameraUnavailable(@NonNull String cameraId) { // default empty implementation } /** * Called whenever camera access priorities change. * *

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.

* *

The default implementation of this method does nothing.

* */ public void onCameraAccessPrioritiesChanged() { // default empty implementation } /** * A physical camera has become available for use again. * *

By default, all of the physical cameras of a logical multi-camera are * available, so {@link #onPhysicalCameraAvailable} is not called for any of the physical * cameras of a logical multi-camera, when {@link #onCameraAvailable} for the logical * multi-camera is invoked. However, if some specific physical cameras are unavailable * to begin with, {@link #onPhysicalCameraUnavailable} may be invoked after * {@link #onCameraAvailable}.

* *

If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion} * < {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, opening a logical camera * disables the {@link #onPhysicalCameraAvailable} and {@link #onPhysicalCameraUnavailable} * callbacks for its physical cameras. For example, if app A opens the camera device:

* *
    * *
  • All apps subscribing to ActivityCallback get {@link #onCameraUnavailable}.
  • * *
  • No app (including app A) subscribing to ActivityCallback gets * {@link #onPhysicalCameraAvailable} or {@link #onPhysicalCameraUnavailable}, because * the logical camera is unavailable (some app is using it).
  • * *
* *

If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion} * ≥ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}:

* *
    * *
  • A physical camera status change will trigger {@link #onPhysicalCameraAvailable} * or {@link #onPhysicalCameraUnavailable} even after the logical camera becomes * unavailable. A {@link #onCameraUnavailable} call for a logical camera doesn't reset the * physical cameras' availability status. This makes it possible for an application opening * the logical camera device to know which physical camera becomes unavailable or available * to use.
  • * *
  • Similar to {@link android.os.Build.VERSION_CODES#TIRAMISU Android 13} and earlier, * the logical camera's {@link #onCameraAvailable} callback implies all of its physical * cameras' status become available. {@link #onPhysicalCameraUnavailable} will be called * for any unavailable physical cameras upon the logical camera becoming available.
  • * *
* *

Given the pipeline nature of the camera capture through {@link * android.hardware.camera2.CaptureRequest}, there may be frame drops if the application * requests images from a physical camera of a logical multi-camera and that physical camera * becomes unavailable. The application should stop requesting directly from an unavailable * physical camera as soon as {@link #onPhysicalCameraUnavailable} is received, and also be * ready to robustly handle frame drop errors for requests targeting physical cameras, * since those errors may arrive before the unavailability callback.

* *

The default implementation of this method does nothing.

* * @param cameraId The unique identifier of the logical multi-camera. * @param physicalCameraId The unique identifier of the physical camera. * * @see #onCameraAvailable * @see #onPhysicalCameraUnavailable */ public void onPhysicalCameraAvailable(@NonNull String cameraId, @NonNull String physicalCameraId) { // default empty implementation } /** * A previously-available physical camera has become unavailable for use. * *

By default, all of the physical cameras of a logical multi-camera are * unavailable if the logical camera itself is unavailable. * No availability callbacks will be called for any of the physical * cameras of its parent logical multi-camera, when {@link #onCameraUnavailable} for * the logical multi-camera is invoked.

* *

If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion} * < {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, opening a logical camera * disables the {@link #onPhysicalCameraAvailable} and {@link #onPhysicalCameraUnavailable} * callbacks for its physical cameras. For example, if app A opens the camera device:

* *
    * *
  • All apps subscribing to ActivityCallback get {@link #onCameraUnavailable}.
  • * *
  • No app (including app A) subscribing to ActivityCallback gets * {@link #onPhysicalCameraAvailable} or {@link #onPhysicalCameraUnavailable}, because * the logical camera is unavailable (some app is using it).
  • * *
* *

If {@link android.content.pm.ApplicationInfo#targetSdkVersion targetSdkVersion} * ≥ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}:

* *
    * *
  • A physical camera status change will trigger {@link #onPhysicalCameraAvailable} * or {@link #onPhysicalCameraUnavailable} even after the logical camera becomes * unavailable. A {@link #onCameraUnavailable} call for a logical camera doesn't reset the * physical cameras' availability status. This makes it possible for an application opening * the logical camera device to know which physical camera becomes unavailable or available * to use.
  • * *
  • Similar to {@link android.os.Build.VERSION_CODES#TIRAMISU Android 13} and earlier, * the logical camera's {@link #onCameraAvailable} callback implies all of its physical * cameras' status become available. {@link #onPhysicalCameraUnavailable} will be called * for any unavailable physical cameras upon the logical camera becoming available.
  • * *
* *

Given the pipeline nature of the camera capture through {@link * android.hardware.camera2.CaptureRequest}, there may be frame drops if the application * requests images from a physical camera of a logical multi-camera and that physical camera * becomes unavailable. The application should stop requesting directly from an unavailable * physical camera as soon as {@link #onPhysicalCameraUnavailable} is received, and also be * ready to robustly handle frame drop errors for requests targeting physical cameras, * since those errors may arrive before the unavailability callback.

* *

The default implementation of this method does nothing.

* * @param cameraId The unique identifier of the logical multi-camera. * @param physicalCameraId The unique identifier of the physical camera. * * @see #onCameraAvailable * @see #onPhysicalCameraAvailable */ public void onPhysicalCameraUnavailable(@NonNull String cameraId, @NonNull String physicalCameraId) { // default empty implementation } /** * A camera device has been opened by an application. * *

The default implementation of this method does nothing.

* android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER is required to receive this * callback * @param cameraId The unique identifier of the camera opened. * @param packageId The package Id of the application opening the camera. * * @see #onCameraClosed * @hide */ @SystemApi @TestApi @RequiresPermission(android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER) public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { // default empty implementation } /** * A previously-opened camera has been closed. * *

The default implementation of this method does nothing.

* android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER is required to receive this * callback. * @param cameraId The unique identifier of the closed camera. * @hide */ @SystemApi @TestApi @RequiresPermission(android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER) public void onCameraClosed(@NonNull String cameraId) { // default empty implementation } } /** * A callback for camera flash torch modes becoming unavailable, disabled, or enabled. * *

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}.

* *

The torch mode is available to set via {@link #setTorchMode} only when it's in a disabled * or enabled state.

* *

Extend this callback and pass an instance of the subclass to * {@link CameraManager#registerTorchCallback} to be notified of such status changes. *

* * @see #registerTorchCallback */ public static abstract class TorchCallback { private int mDeviceId; private int mDevicePolicy; /** * A camera's torch mode has become unavailable to set via {@link #setTorchMode}. * *

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.

* *

The default implementation of this method does nothing.

* * @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}. * *

The default implementation of this method does nothing.

* * @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 } /** * A camera's flash unit brightness level has been changed in torch mode via * {@link #turnOnTorchWithStrengthLevel}. When the torch is turned OFF, this * callback will not be triggered even though the torch strength level resets to * default value * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL} * *

The default implementation of this method does nothing.

* * @param cameraId The unique identifier of the camera whose flash unit brightness level has * been changed. * * @param newStrengthLevel The brightness level of the flash unit that has been changed to. */ public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) { // default empty implementation } } /** * 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; } /** * Inject the external camera to replace the internal camera session. * *

If injecting the external camera device fails, then the injection callback's * {@link CameraInjectionSession.InjectionStatusCallback#onInjectionError * onInjectionError} method will be called.

* * @param packageName It scopes the injection to a particular app. * @param internalCamId The id of one of the physical or logical cameras on the phone. * @param externalCamId The id of one of the remote cameras that are provided by the dynamic * camera HAL. * @param executor The executor which will be used when invoking the callback. * @param callback The callback which is invoked once the external camera is injected. * * @throws CameraAccessException If the camera device has been disconnected. * {@link CameraAccessException#CAMERA_DISCONNECTED} will be * thrown if camera service is not available. * @throws SecurityException If the specific application that can cast to external * devices does not have permission to inject the external * camera. * @throws IllegalArgumentException If cameraId doesn't match any currently or previously * available camera device or some camera functions might not * work properly or the injection camera runs into a fatal * error. * @hide */ @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) public void injectCamera(@NonNull String packageName, @NonNull String internalCamId, @NonNull String externalCamId, @NonNull @CallbackExecutor Executor executor, @NonNull CameraInjectionSession.InjectionStatusCallback callback) throws CameraAccessException, SecurityException, IllegalArgumentException { if (CameraManagerGlobal.sCameraServiceDisabled) { throw new IllegalArgumentException("No cameras available on device"); } ICameraService cameraService = CameraManagerGlobal.get().getCameraService(); if (cameraService == null) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable"); } synchronized (mLock) { try { CameraInjectionSessionImpl injectionSessionImpl = new CameraInjectionSessionImpl(callback, executor); ICameraInjectionCallback cameraInjectionCallback = injectionSessionImpl.getCallback(); ICameraInjectionSession injectionSession = cameraService.injectCamera(packageName, internalCamId, externalCamId, cameraInjectionCallback); injectionSessionImpl.setRemoteInjectionSession(injectionSession); } catch (ServiceSpecificException e) { throw ExceptionUtils.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"); throw ExceptionUtils.throwAsPublicException(sse); } } } /** * Injects session params into existing clients in the CameraService. * * @param cameraId The camera id of client to inject session params into. * If no such client exists for cameraId, no injection will * take place. * @param sessionParams A {@link CaptureRequest} object containing the * the sessionParams to inject into the existing client. * * @throws CameraAccessException {@link CameraAccessException#CAMERA_DISCONNECTED} will be * thrown if camera service is not available. Further, if * if no such client exists for cameraId, * {@link CameraAccessException#CAMERA_ERROR} will be thrown. * @throws SecurityException If the caller does not have permission to inject session * params * @hide */ @RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA) public void injectSessionParams(@NonNull String cameraId, @NonNull CaptureRequest sessionParams) throws CameraAccessException, SecurityException { CameraManagerGlobal.get().injectSessionParams(cameraId, sessionParams); } /** * Returns the current CameraService instance connected to Global * @hide */ public ICameraService getCameraService() { return CameraManagerGlobal.get().getCameraService(); } /** * Returns true if cameraservice is currently disabled. If true, {@link #getCameraService()} * will definitely return null. * @hide */ public boolean isCameraServiceDisabled() { return CameraManagerGlobal.sCameraServiceDisabled; } /** * Reports {@link CameraExtensionSessionStats} to the {@link ICameraService} to be logged for * currently active session. Validation is done downstream. * * @param extStats Extension Session stats to be logged by cameraservice * * @return the key to be used with the next call. * See {@link ICameraService#reportExtensionSessionStats}. * @hide */ public static String reportExtensionSessionStats(CameraExtensionSessionStats extStats) { ICameraService cameraService = CameraManagerGlobal.get().getCameraService(); if (cameraService == null) { Log.e(TAG, "CameraService not available. Not reporting extension stats."); return ""; } try { return cameraService.reportExtensionSessionStats(extStats); } catch (RemoteException e) { Log.e(TAG, "Failed to report extension session stats to cameraservice.", e); } return ""; } /** * 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 mDeviceStatus = new ArrayMap<>(); // Camera ID -> (physical camera ID -> Status map) private final ArrayMap> mUnavailablePhysicalDevices = new ArrayMap<>(); // Opened Camera ID -> apk name map private final ArrayMap mOpenedDevices = new ArrayMap<>(); private final Set> mConcurrentCameraIdCombinations = new ArraySet<>(); // Registered availability callbacks and their executors private final ArrayMap mCallbackMap = new ArrayMap<>(); // torch client binder to set the torch mode with. private final Binder mTorchClientBinder = new Binder(); // Camera ID -> Torch status map private final ArrayMap mTorchStatus = new ArrayMap<>(); // Registered torch callbacks and their executors private final ArrayMap mTorchCallbackMap = new ArrayMap<>(); private final Object mLock = new Object(); // Access only through getCameraService to deal with binder death private ICameraService mCameraService; private boolean mHasOpenCloseListenerPermission = false; private HandlerThread mDeviceStateHandlerThread; private Handler mDeviceStateHandler; private FoldStateListener mFoldStateListener; // Singleton, don't allow construction private CameraManagerGlobal() { } public static final boolean sCameraServiceDisabled = SystemProperties.getBoolean("config.disable_cameraservice", false); public static final boolean sLandscapeToPortrait = SystemProperties.getBoolean(LANDSCAPE_TO_PORTRAIT_PROP, false); public static CameraManagerGlobal get() { return gCameraManager; } public void registerDeviceStateListener(@NonNull CameraCharacteristics chars, @NonNull Context ctx) { synchronized(mLock) { if (mDeviceStateHandlerThread == null) { mDeviceStateHandlerThread = new HandlerThread(TAG); mDeviceStateHandlerThread.start(); mDeviceStateHandler = new Handler(mDeviceStateHandlerThread.getLooper()); } if (mFoldStateListener == null) { mFoldStateListener = new FoldStateListener(ctx); try { ctx.getSystemService(DeviceStateManager.class).registerCallback( new HandlerExecutor(mDeviceStateHandler), mFoldStateListener); } catch (IllegalStateException e) { mFoldStateListener = null; Log.v(TAG, "Failed to register device state listener!"); Log.v(TAG, "Device state dependent characteristics updates will not be" + "functional!"); return; } } mFoldStateListener.addDeviceStateListener(chars.getDeviceStateListener()); } } @Override public IBinder asBinder() { return this; } /** * Return a best-effort ICameraService. * *

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.

*/ 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. * *

Sets mCameraService to a valid pointer or null if the connection does not succeed.

*/ 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 cameraStatus : cameraStatuses) { DeviceCameraInfo info = new DeviceCameraInfo(cameraStatus.cameraId, cameraStatus.deviceId); onStatusChangedLocked(cameraStatus.status, info); if (cameraStatus.unavailablePhysicalCameras != null) { for (String unavailablePhysicalCamera : cameraStatus.unavailablePhysicalCameras) { onPhysicalCameraStatusChangedLocked( ICameraServiceListener.STATUS_NOT_PRESENT, info, unavailablePhysicalCamera); } } if (mHasOpenCloseListenerPermission && cameraStatus.status == ICameraServiceListener.STATUS_NOT_AVAILABLE && !cameraStatus.clientPackage.isEmpty()) { onCameraOpenedLocked(info, cameraStatus.clientPackage); } } 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 } try { ConcurrentCameraIdCombination[] cameraIdCombinations = cameraService.getConcurrentCameraIds(); for (ConcurrentCameraIdCombination comb : cameraIdCombinations) { Set> combination = comb.getConcurrentCameraIdCombination(); Set deviceCameraInfoSet = new ArraySet<>(); for (Pair entry : combination) { deviceCameraInfoSet.add(new DeviceCameraInfo(entry.first, entry.second)); } mConcurrentCameraIdCombinations.add(deviceCameraInfoSet); } } catch (ServiceSpecificException e) { // Unexpected failure throw new IllegalStateException("Failed to get concurrent camera id combinations", e); } catch (RemoteException e) { // Camera service died in all probability } } /** Injects session params into an existing client for cameraid. */ public void injectSessionParams(@NonNull String cameraId, @NonNull CaptureRequest sessionParams) throws CameraAccessException, SecurityException { synchronized (mLock) { ICameraService cameraService = getCameraService(); if (cameraService == null) { throw new CameraAccessException( CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); } try { cameraService.injectSessionParams(cameraId, sessionParams.getNativeMetadata()); } catch (ServiceSpecificException e) { throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException( CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); } } } private String[] extractCameraIdListLocked(int deviceId, int devicePolicy) { List cameraIds = new ArrayList<>(); for (int i = 0; i < mDeviceStatus.size(); i++) { int status = mDeviceStatus.valueAt(i); DeviceCameraInfo info = mDeviceStatus.keyAt(i); if (status == ICameraServiceListener.STATUS_NOT_PRESENT || status == ICameraServiceListener.STATUS_ENUMERATING || shouldHideCamera(deviceId, devicePolicy, info)) { continue; } cameraIds.add(info.mCameraId); } return cameraIds.toArray(new String[0]); } private Set> extractConcurrentCameraIdListLocked(int deviceId, int devicePolicy) { Set> concurrentCameraIds = new ArraySet<>(); for (Set deviceCameraInfos : mConcurrentCameraIdCombinations) { Set extractedCameraIds = new ArraySet<>(); for (DeviceCameraInfo info : deviceCameraInfos) { // if the camera id status is NOT_PRESENT or ENUMERATING; skip the device. // TODO: Would a device status NOT_PRESENT ever be in the map ? it gets removed // in the callback anyway. Integer status = mDeviceStatus.get(info); if (status == null) { // camera id not present continue; } if (status == ICameraServiceListener.STATUS_ENUMERATING || status == ICameraServiceListener.STATUS_NOT_PRESENT) { continue; } if (shouldHideCamera(deviceId, devicePolicy, info)) { continue; } extractedCameraIds.add(info.mCameraId); } if (!extractedCameraIds.isEmpty()) { concurrentCameraIds.add(extractedCameraIds); } } return concurrentCameraIds; } private static void sortCameraIds(String[] cameraIds) { // The sort logic must match the logic in // libcameraservice/common/CameraProviderManager.cpp::getAPI1CompatibleCameraDeviceIds Arrays.sort(cameraIds, new Comparator() { @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); } }}); } private boolean shouldHideCamera(int currentDeviceId, int devicePolicy, DeviceCameraInfo info) { if (!android.companion.virtualdevice.flags.Flags.cameraDeviceAwareness()) { // Don't hide any cameras if the device-awareness feature flag is disabled. return false; } if (devicePolicy == DEVICE_POLICY_DEFAULT && info.mDeviceId == DEVICE_ID_DEFAULT) { // Don't hide default-device cameras for a default-policy virtual device. return false; } return currentDeviceId != info.mDeviceId; } private static boolean cameraStatusesContains(CameraStatus[] cameraStatuses, DeviceCameraInfo info) { for (CameraStatus c : cameraStatuses) { if (c.cameraId.equals(info.mCameraId) && c.deviceId == info.mDeviceId) { return true; } } return false; } public String[] getCameraIdListNoLazy(int deviceId, int devicePolicy) { if (sCameraServiceDisabled) { return new String[] {}; } CameraStatus[] cameraStatuses; ICameraServiceListener.Stub testListener = new ICameraServiceListener.Stub() { @Override public void onStatusChanged(int status, String id, int deviceId) throws RemoteException { } @Override public void onPhysicalCameraStatusChanged(int status, String id, String physicalId, int deviceId) throws RemoteException { } @Override public void onTorchStatusChanged(int status, String id, int deviceId) throws RemoteException { } @Override public void onTorchStrengthLevelChanged(String id, int newStrengthLevel, int deviceId) throws RemoteException { } @Override public void onCameraAccessPrioritiesChanged() { } @Override public void onCameraOpened(String id, String clientPackageId, int deviceId) { } @Override public void onCameraClosed(String id, int deviceId) { }}; String[] cameraIds; synchronized (mLock) { connectCameraServiceLocked(); try { // The purpose of the addListener, removeListener pair here is to get a fresh // list of camera ids from cameraserver. We do this since for in test processes, // changes can happen w.r.t non-changeable permissions (eg: SYSTEM_CAMERA // permissions can be effectively changed by calling // adopt(drop)ShellPermissionIdentity()). // Camera devices, which have their discovery affected by these permission // changes, will not have clients get callbacks informing them about these // devices going offline (in real world scenarios, these permissions aren't // changeable). Future calls to getCameraIdList() will reflect the changes in // the camera id list after getCameraIdListNoLazy() is called. // We need to remove the torch ids which may have been associated with the // devices removed as well. This is the same situation. cameraStatuses = mCameraService.addListener(testListener); mCameraService.removeListener(testListener); for (CameraStatus cameraStatus : cameraStatuses) { onStatusChangedLocked(cameraStatus.status, new DeviceCameraInfo(cameraStatus.cameraId, cameraStatus.deviceId)); } Set deviceCameraInfos = mDeviceStatus.keySet(); List deviceInfosToRemove = new ArrayList<>(); for (DeviceCameraInfo info : deviceCameraInfos) { // Its possible that a device id was removed without a callback notifying // us. This may happen in case a process 'drops' system camera permissions // (even though the permission isn't a changeable one, tests may call // adoptShellPermissionIdentity() and then dropShellPermissionIdentity(). if (!cameraStatusesContains(cameraStatuses, info)) { deviceInfosToRemove.add(info); } } for (DeviceCameraInfo info : deviceInfosToRemove) { onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info); mTorchStatus.remove(info); } } 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 } cameraIds = extractCameraIdListLocked(deviceId, devicePolicy); } sortCameraIds(cameraIds); return cameraIds; } /** * 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(int deviceId, int devicePolicy) { String[] cameraIds; synchronized (mLock) { // Try to make sure we have an up-to-date list of camera devices. connectCameraServiceLocked(); cameraIds = extractCameraIdListLocked(deviceId, devicePolicy); } sortCameraIds(cameraIds); return cameraIds; } public @NonNull Set> getConcurrentCameraIds(int deviceId, int devicePolicy) { Set> concurrentStreamingCameraIds; synchronized (mLock) { // Try to make sure we have an up-to-date list of concurrent camera devices. connectCameraServiceLocked(); concurrentStreamingCameraIds = extractConcurrentCameraIdListLocked(deviceId, devicePolicy); } // TODO: Some sort of sorting ? return concurrentStreamingCameraIds; } public boolean isConcurrentSessionConfigurationSupported( @NonNull Map cameraIdsAndSessionConfigurations, int targetSdkVersion, int deviceId, int devicePolicy) throws CameraAccessException { if (cameraIdsAndSessionConfigurations == null) { throw new IllegalArgumentException("cameraIdsAndSessionConfigurations was null"); } int size = cameraIdsAndSessionConfigurations.size(); if (size == 0) { throw new IllegalArgumentException("camera id and session combination is empty"); } synchronized (mLock) { // Go through all the elements and check if the camera ids are valid at least / // belong to one of the combinations returned by getConcurrentCameraIds() boolean subsetFound = false; for (Set combination : mConcurrentCameraIdCombinations) { Set infos = new ArraySet<>(); for (String cameraId : cameraIdsAndSessionConfigurations.keySet()) { infos.add(new DeviceCameraInfo(cameraId, devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId)); } if (combination.containsAll(infos)) { subsetFound = true; } } if (!subsetFound) { Log.v(TAG, "isConcurrentSessionConfigurationSupported called with a subset of" + " camera ids not returned by getConcurrentCameraIds"); return false; } CameraIdAndSessionConfiguration [] cameraIdsAndConfigs = new CameraIdAndSessionConfiguration[size]; int i = 0; for (Map.Entry pair : cameraIdsAndSessionConfigurations.entrySet()) { cameraIdsAndConfigs[i] = new CameraIdAndSessionConfiguration(pair.getKey(), pair.getValue()); i++; } try { return mCameraService.isConcurrentSessionConfigurationSupported( cameraIdsAndConfigs, targetSdkVersion, deviceId, devicePolicy); } catch (ServiceSpecificException e) { throw ExceptionUtils.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); } } } /** * Helper function to find out if a camera id is in the set of combinations returned by * getConcurrentCameraIds() * @param cameraId the unique identifier of the camera device to query * @param deviceId the device id of the context * @return Whether the camera device was found in the set of combinations returned by * getConcurrentCameraIds */ public boolean cameraIdHasConcurrentStreamsLocked(String cameraId, int deviceId, int devicePolicy) { DeviceCameraInfo info = new DeviceCameraInfo(cameraId, devicePolicy == DEVICE_POLICY_DEFAULT ? DEVICE_ID_DEFAULT : deviceId); if (!mDeviceStatus.containsKey(info)) { // physical camera ids aren't advertised in concurrent camera id combinations. if (DEBUG) { Log.v(TAG, " physical camera id " + cameraId + " is hidden." + " Available logical camera ids : " + mDeviceStatus); } return false; } for (Set comb : mConcurrentCameraIdCombinations) { if (comb.contains(info)) { return true; } } return false; } public void setTorchMode(String cameraId, boolean enabled, int deviceId, int devicePolicy) 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, deviceId, devicePolicy); } catch(ServiceSpecificException e) { throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable"); } } } public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength, int deviceId, int devicePolicy) 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.turnOnTorchWithStrengthLevel(cameraId, torchStrength, mTorchClientBinder, deviceId, devicePolicy); } catch(ServiceSpecificException e) { throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); } } } public int getTorchStrengthLevel(String cameraId, int deviceId, int devicePolicy) throws CameraAccessException { int torchStrength; 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 { torchStrength = cameraService.getTorchStrengthLevel(cameraId, deviceId, devicePolicy); } catch(ServiceSpecificException e) { throw ExceptionUtils.throwAsPublicException(e); } catch (RemoteException e) { throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED, "Camera service is currently unavailable."); } } return torchStrength; } 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(callback::onCameraAccessPrioritiesChanged); } finally { Binder.restoreCallingIdentity(ident); } } private void postSingleCameraOpenedUpdate(final AvailabilityCallback callback, final Executor executor, final String id, final String packageId) { final long ident = Binder.clearCallingIdentity(); try { executor.execute(() -> callback.onCameraOpened(id, packageId)); } finally { Binder.restoreCallingIdentity(ident); } } private void postSingleCameraClosedUpdate(final AvailabilityCallback callback, final Executor executor, final String id) { final long ident = Binder.clearCallingIdentity(); try { executor.execute(() -> callback.onCameraClosed(id)); } finally { Binder.restoreCallingIdentity(ident); } } private void postSingleUpdate(final AvailabilityCallback callback, final Executor executor, final String id, final String physicalId, final int status) { if (isAvailable(status)) { final long ident = Binder.clearCallingIdentity(); try { executor.execute( () -> { if (physicalId == null) { callback.onCameraAvailable(id); } else { callback.onPhysicalCameraAvailable(id, physicalId); } }); } finally { Binder.restoreCallingIdentity(ident); } } else { final long ident = Binder.clearCallingIdentity(); try { executor.execute( () -> { if (physicalId == null) { callback.onCameraUnavailable(id); } else { callback.onPhysicalCameraUnavailable(id, physicalId); } }); } 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; } } } private void postSingleTorchStrengthLevelUpdate(final TorchCallback callback, final Executor executor, final String id, final int newStrengthLevel) { final long ident = Binder.clearCallingIdentity(); try { executor.execute(() -> callback.onTorchStrengthLevelChanged(id, newStrengthLevel)); } finally { Binder.restoreCallingIdentity(ident); } } /** * 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++) { DeviceCameraInfo info = mDeviceStatus.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } Integer status = mDeviceStatus.valueAt(i); postSingleUpdate(callback, executor, info.mCameraId, null /* physicalId */, status); // Send the NOT_PRESENT state for unavailable physical cameras if ((isAvailable(status) || physicalCallbacksAreEnabledForUnavailableCamera()) && mUnavailablePhysicalDevices.containsKey(info)) { List unavailableIds = mUnavailablePhysicalDevices.get(info); for (String unavailableId : unavailableIds) { postSingleUpdate(callback, executor, info.mCameraId, unavailableId, ICameraServiceListener.STATUS_NOT_PRESENT); } } } for (int i = 0; i < mOpenedDevices.size(); i++) { DeviceCameraInfo info = mOpenedDevices.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } String clientPackageId = mOpenedDevices.valueAt(i); postSingleCameraOpenedUpdate(callback, executor, info.mCameraId, clientPackageId); } } private void onStatusChangedLocked(int status, DeviceCameraInfo info) { if (DEBUG) { Log.v(TAG, String.format("Camera id %s has status changed to 0x%x for device %d", info.mCameraId, status, info.mDeviceId)); } if (!validStatus(status)) { Log.e(TAG, String.format("Ignoring invalid camera %s status 0x%x for device %d", info.mCameraId, status, info.mDeviceId)); return; } Integer oldStatus; if (status == ICameraServiceListener.STATUS_NOT_PRESENT) { oldStatus = mDeviceStatus.remove(info); mUnavailablePhysicalDevices.remove(info); } else { oldStatus = mDeviceStatus.put(info, status); if (oldStatus == null) { mUnavailablePhysicalDevices.put(info, new ArrayList<>()); } } 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++) { final AvailabilityCallback callback = mCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mCallbackMap.valueAt(i); postSingleUpdate(callback, executor, info.mCameraId, null /* physicalId */, status); // Send the NOT_PRESENT state for unavailable physical cameras if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(info)) { List unavailableIds = mUnavailablePhysicalDevices.get(info); for (String unavailableId : unavailableIds) { postSingleUpdate(callback, executor, info.mCameraId, unavailableId, ICameraServiceListener.STATUS_NOT_PRESENT); } } } } // onStatusChangedLocked private void onPhysicalCameraStatusChangedLocked(int status, DeviceCameraInfo info, String physicalId) { if (DEBUG) { Log.v(TAG, String.format("Camera id %s physical camera id %s has status changed " + "to 0x%x for device %d", info.mCameraId, physicalId, status, info.mDeviceId)); } if (!validStatus(status)) { Log.e(TAG, String.format( "Ignoring invalid device %s physical device %s status 0x%x for device %d", info.mCameraId, physicalId, status, info.mDeviceId)); return; } //TODO: Do we need to treat this as error? if (!mDeviceStatus.containsKey(info) || !mUnavailablePhysicalDevices.containsKey(info)) { Log.e(TAG, String.format("Camera %s is not present. Ignore physical camera " + "status change", info.mCameraId)); return; } List unavailablePhysicalDevices = mUnavailablePhysicalDevices.get(info); if (!isAvailable(status) && !unavailablePhysicalDevices.contains(physicalId)) { unavailablePhysicalDevices.add(physicalId); } else if (isAvailable(status) && unavailablePhysicalDevices.contains(physicalId)) { unavailablePhysicalDevices.remove(physicalId); } else { if (DEBUG) { Log.v(TAG, String.format( "Physical camera device status was previously available (%b), " + " and is now again available (%b)" + "so no new client visible update will be sent", !unavailablePhysicalDevices.contains(physicalId), isAvailable(status))); } return; } if (!physicalCallbacksAreEnabledForUnavailableCamera() && !isAvailable(mDeviceStatus.get(info))) { Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera " + "status change callback(s)", info.mCameraId)); return; } final int callbackCount = mCallbackMap.size(); for (int i = 0; i < callbackCount; i++) { final AvailabilityCallback callback = mCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mCallbackMap.valueAt(i); postSingleUpdate(callback, executor, info.mCameraId, physicalId, status); } } // onPhysicalCameraStatusChangedLocked private void updateTorchCallbackLocked(TorchCallback callback, Executor executor) { for (int i = 0; i < mTorchStatus.size(); i++) { DeviceCameraInfo info = mTorchStatus.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } Integer status = mTorchStatus.valueAt(i); postSingleTorchUpdate(callback, executor, info.mCameraId, status); } } private void onTorchStatusChangedLocked(int status, DeviceCameraInfo info) { if (DEBUG) { Log.v(TAG, String.format( "Camera id %s has torch status changed to 0x%x for device %d", info.mCameraId, status, info.mDeviceId)); } if (!validTorchStatus(status)) { Log.e(TAG, String.format( "Ignoring invalid camera %s torch status 0x%x for device %d", info.mCameraId, status, info.mDeviceId)); return; } Integer oldStatus = mTorchStatus.put(info, 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 TorchCallback callback = mTorchCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mTorchCallbackMap.valueAt(i); postSingleTorchUpdate(callback, executor, info.mCameraId, status); } } // onTorchStatusChangedLocked private void onTorchStrengthLevelChangedLocked(DeviceCameraInfo info, int newStrengthLevel) { if (DEBUG) { Log.v(TAG, String.format( "Camera id %s has torch strength level changed to %d for device %d", info.mCameraId, newStrengthLevel, info.mDeviceId)); } final int callbackCount = mTorchCallbackMap.size(); for (int i = 0; i < callbackCount; i++) { final TorchCallback callback = mTorchCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mTorchCallbackMap.valueAt(i); postSingleTorchStrengthLevelUpdate(callback, executor, info.mCameraId, newStrengthLevel); } } // onTorchStrengthLevelChanged /** * 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. * @param hasOpenCloseListenerPermission whether the client has permission for * onCameraOpened/onCameraClosed callback */ public void registerAvailabilityCallback(AvailabilityCallback callback, Executor executor, boolean hasOpenCloseListenerPermission, int deviceId, int devicePolicy) { synchronized (mLock) { // In practice, this permission doesn't change. So we don't need one flag for each // callback object. mHasOpenCloseListenerPermission = hasOpenCloseListenerPermission; connectCameraServiceLocked(); callback.mDeviceId = deviceId; callback.mDevicePolicy = devicePolicy; 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, int deviceId, int devicePolicy) { synchronized(mLock) { connectCameraServiceLocked(); callback.mDeviceId = deviceId; callback.mDevicePolicy = devicePolicy; 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, int deviceId) throws RemoteException { synchronized(mLock) { onStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId)); } } @Override public void onPhysicalCameraStatusChanged(int status, String cameraId, String physicalCameraId, int deviceId) throws RemoteException { synchronized (mLock) { onPhysicalCameraStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId), physicalCameraId); } } @Override public void onTorchStatusChanged(int status, String cameraId, int deviceId) throws RemoteException { synchronized (mLock) { onTorchStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId)); } } @Override public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel, int deviceId) throws RemoteException { synchronized (mLock) { onTorchStrengthLevelChangedLocked(new DeviceCameraInfo(cameraId, deviceId), newStrengthLevel); } } @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); } } } @Override public void onCameraOpened(String cameraId, String clientPackageId, int deviceId) { synchronized (mLock) { onCameraOpenedLocked(new DeviceCameraInfo(cameraId, deviceId), clientPackageId); } } private void onCameraOpenedLocked(DeviceCameraInfo info, String clientPackageId) { String oldApk = mOpenedDevices.put(info, clientPackageId); if (oldApk != null) { if (oldApk.equals(clientPackageId)) { Log.w(TAG, "onCameraOpened was previously called for " + oldApk + " and is now again called for the same package name, " + "so no new client visible update will be sent"); return; } else { Log.w(TAG, "onCameraOpened was previously called for " + oldApk + " and is now called for " + clientPackageId + " without onCameraClosed being called first"); } } final int callbackCount = mCallbackMap.size(); for (int i = 0; i < callbackCount; i++) { final AvailabilityCallback callback = mCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mCallbackMap.valueAt(i); postSingleCameraOpenedUpdate(callback, executor, info.mCameraId, clientPackageId); } } @Override public void onCameraClosed(String cameraId, int deviceId) { synchronized (mLock) { onCameraClosedLocked(new DeviceCameraInfo(cameraId, deviceId)); } } private void onCameraClosedLocked(DeviceCameraInfo info) { mOpenedDevices.remove(info); final int callbackCount = mCallbackMap.size(); for (int i = 0; i < callbackCount; i++) { final AvailabilityCallback callback = mCallbackMap.keyAt(i); if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) { continue; } final Executor executor = mCallbackMap.valueAt(i); postSingleCameraClosedUpdate(callback, executor, info.mCameraId); } } /** * 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. * *

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.

*/ 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. // Iterate from the end to the beginning because onStatusChangedLocked removes // entries from the ArrayMap. for (int i = mDeviceStatus.size() - 1; i >= 0; i--) { DeviceCameraInfo info = mDeviceStatus.keyAt(i); onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info); if (mHasOpenCloseListenerPermission) { onCameraClosedLocked(info); } } for (int i = 0; i < mTorchStatus.size(); i++) { DeviceCameraInfo info = mTorchStatus.keyAt(i); onTorchStatusChangedLocked(ICameraServiceListener.TORCH_STATUS_NOT_AVAILABLE, info); } mConcurrentCameraIdCombinations.clear(); scheduleCameraServiceReconnectionLocked(); } } private static final class DeviceCameraInfo { private final String mCameraId; private final int mDeviceId; DeviceCameraInfo(String cameraId, int deviceId) { mCameraId = cameraId; mDeviceId = deviceId; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DeviceCameraInfo that = (DeviceCameraInfo) o; return mDeviceId == that.mDeviceId && Objects.equals(mCameraId, that.mCameraId); } @Override public int hashCode() { return Objects.hash(mCameraId, mDeviceId); } } } // CameraManagerGlobal } // CameraManager