Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bd9b1068 authored by Eino-Ville Talvala's avatar Eino-Ville Talvala
Browse files

Camera2: CameraManager refurbishing and error management

- Invoke availability listeners when first added
- Handle camera service crashes
 - Listen to service death
 - Attempt to rebind on every call if service died
 - If service is still dead, act as if no cameras are connected
- Fix bug in DeviceImpl that prevented proper handling of errors on open
- For clarity, don't UncheckedThrow for exceptions that are already unchecked

Bug: 16514157
Bug: 16483815
Bug: 16483222
Bug: 16561237

Change-Id: I3d54de1204f5a863882cf675fcee6280d53c4039
parent c9c70a83
Loading
Loading
Loading
Loading
+293 −156
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import android.hardware.CameraInfo;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.legacy.CameraDeviceUserShim;
import android.hardware.camera2.legacy.LegacyMetadataMapper;
import android.hardware.camera2.utils.CameraBinderDecorator;
import android.hardware.camera2.utils.CameraServiceBinderDecorator;
import android.hardware.camera2.utils.CameraRuntimeException;
import android.hardware.camera2.utils.BinderHolder;
import android.os.IBinder;
@@ -52,6 +52,7 @@ import java.util.ArrayList;
public final class CameraManager {

    private static final String TAG = "CameraManager";
    private final boolean DEBUG;

    /**
     * This should match the ICameraService definition
@@ -63,7 +64,9 @@ public final class CameraManager {
    private static final int API_VERSION_1 = 1;
    private static final int API_VERSION_2 = 2;

    private final ICameraService mCameraService;
    // Access only through getCameraServiceLocked to deal with binder death
    private ICameraService mCameraService;

    private ArrayList<String> mDeviceIdList;

    private final ArrayMap<AvailabilityListener, Handler> mListenerMap =
@@ -72,35 +75,17 @@ public final class CameraManager {
    private final Context mContext;
    private final Object mLock = new Object();

    private final CameraServiceListener mServiceListener = new CameraServiceListener();

    /**
     * @hide
     */
    public CameraManager(Context context) {
        DEBUG = Log.isLoggable(TAG, Log.DEBUG);
        synchronized(mLock) {
            mContext = context;

        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);

        /**
         * Wrap the camera service in a decorator which automatically translates return codes
         * into exceptions, and RemoteExceptions into other exceptions.
         */
        mCameraService = CameraBinderDecorator.newInstance(cameraServiceRaw);

        try {
            CameraBinderDecorator.throwOnError(
                    CameraMetadataNative.nativeSetupGlobalVendorTagDescriptor());
        } catch (CameraRuntimeException e) {
            handleRecoverableSetupErrors(e, "Failed to set up vendor tags");
        }

        try {
            mCameraService.addListener(new CameraServiceListener());
        } catch(CameraRuntimeException e) {
            throw new IllegalStateException("Failed to register a camera service listener",
                    e.asChecked());
        } catch (RemoteException e) {
            // impossible
            connectCameraServiceLocked();
        }
    }

@@ -116,13 +101,9 @@ public final class CameraManager {
     */
    public String[] getCameraIdList() throws CameraAccessException {
        synchronized (mLock) {
            try {
            // ID list creation handles various known failures in device enumeration, so only
            // exceptions it'll throw are unexpected, and should be propagated upward.
            return getOrCreateDeviceIdListLocked().toArray(new String[0]);
            } catch(CameraAccessException e) {
                // this should almost never happen, except if mediaserver crashes
                throw new IllegalStateException(
                        "Failed to query camera service for device ID list", e);
            }
        }
    }

@@ -132,6 +113,9 @@ public final class CameraManager {
     * <p>Registering the same listener again will replace the handler with the
     * new one provided.</p>
     *
     * <p>The first time a listener is registered, it is immediately called
     * with the availability status of all currently known camera devices.</p>
     *
     * @param listener The new listener to send camera availability notices to
     * @param handler The handler on which the listener should be invoked, or
     * {@code null} to use the current thread's {@link android.os.Looper looper}.
@@ -147,10 +131,11 @@ public final class CameraManager {
        }

        synchronized (mLock) {
            mListenerMap.put(listener, handler);

            // TODO: fire the current oldest known state when adding a new listener
            //    (must be done while holding lock)
            Handler oldHandler = mListenerMap.put(listener, handler);
            // For new listeners, provide initial availability information
            if (oldHandler == null) {
                mServiceListener.updateListenerLocked(listener, handler);
            }
        }
    }

@@ -176,8 +161,9 @@ public final class CameraManager {
     * @return The properties of the given camera
     *
     * @throws IllegalArgumentException if the cameraId does not match any
     * currently connected camera device.
     * @throws CameraAccessException if the camera is disabled by device policy.
     *         known camera device.
     * @throws CameraAccessException if the camera is disabled by device policy, or
     *         the camera device has been disconnected.
     * @throws SecurityException if the application does not have permission to
     *         access the camera
     *
@@ -186,13 +172,13 @@ public final class CameraManager {
     */
    public CameraCharacteristics getCameraCharacteristics(String cameraId)
            throws CameraAccessException {
        CameraCharacteristics characteristics = null;

        synchronized (mLock) {
            if (!getOrCreateDeviceIdListLocked().contains(cameraId)) {
                throw new IllegalArgumentException(String.format("Camera id %s does not match any" +
                        " currently connected camera device", cameraId));
            }
        }

            int id = Integer.valueOf(cameraId);

@@ -201,39 +187,41 @@ public final class CameraManager {
             * otherwise get them from the legacy shim instead.
             */

        if (!supportsCamera2Api(cameraId)) {
            // Legacy backwards compatibility path; build static info from the camera parameters
            String[] outParameters = new String[1];
            ICameraService cameraService = getCameraServiceLocked();
            if (cameraService == null) {
                throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
                        "Camera service is currently unavailable");
            }
            try {
                mCameraService.getLegacyParameters(id, /*out*/outParameters);
                if (!supportsCamera2ApiLocked(cameraId)) {
                    // Legacy backwards compatibility path; build static info from the camera
                    // parameters
                    String[] outParameters = new String[1];

                    cameraService.getLegacyParameters(id, /*out*/outParameters);
                    String parameters = outParameters[0];

                    CameraInfo info = new CameraInfo();
                mCameraService.getCameraInfo(id, /*out*/info);

                return LegacyMetadataMapper.createCharacteristics(parameters, info);
            } catch (RemoteException e) {
                // Impossible
                return null;
            } catch (CameraRuntimeException e) {
                throw e.asChecked();
            }
                    cameraService.getCameraInfo(id, /*out*/info);

                    characteristics = LegacyMetadataMapper.createCharacteristics(parameters, info);
                } else {
                    // Normal path: Get the camera characteristics directly from the camera service
                    CameraMetadataNative info = new CameraMetadataNative();

            try {
                mCameraService.getCameraCharacteristics(id, info);
                    cameraService.getCameraCharacteristics(id, info);

                    characteristics = new CameraCharacteristics(info);
                }
            } catch (CameraRuntimeException e) {
                throw e.asChecked();
            } catch (RemoteException e) {
                // impossible
                return null;
                // Camera service died - act as if the camera was disconnected
                throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
                        "Camera service is currently unavailable", e);
            }

            return new CameraCharacteristics(info);
        }
        return characteristics;
    }

    /**
@@ -278,10 +266,16 @@ public final class CameraManager {
                ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks();
                int id = Integer.parseInt(cameraId);
                try {
                    if (supportsCamera2Api(cameraId)) {
                    if (supportsCamera2ApiLocked(cameraId)) {
                        // Use cameraservice's cameradeviceclient implementation for HAL3.2+ devices
                        mCameraService.connectDevice(callbacks, id, mContext.getPackageName(),
                                USE_CALLING_UID, holder);
                        ICameraService cameraService = getCameraServiceLocked();
                        if (cameraService == null) {
                            throw new CameraRuntimeException(
                                CameraAccessException.CAMERA_DISCONNECTED,
                                "Camera service is currently unavailable");
                        }
                        cameraService.connectDevice(callbacks, id,
                                mContext.getPackageName(), USE_CALLING_UID, holder);
                        cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder());
                    } else {
                        // Use legacy camera implementation for HAL1 devices
@@ -304,12 +298,19 @@ public final class CameraManager {
                        if (e.getReason() == CameraAccessException.CAMERA_DISABLED ||
                                e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) {
                            // Per API docs, these failures call onError and throw
                            throw e;
                            throw e.asChecked();
                        }
                    } else {
                        // Unexpected failure - rethrow
                        throw e;
                    }
                } catch (RemoteException e) {
                    // Camera service died - act as if it's a CAMERA_DISCONNECTED case
                    CameraRuntimeException ce = new CameraRuntimeException(
                        CameraAccessException.CAMERA_DISCONNECTED,
                        "Camera service is currently unavailable", e);
                    deviceImpl.setRemoteFailure(ce);
                    throw ce.asChecked();
                }

                // TODO: factor out listener to be non-nested, then move setter to constructor
@@ -324,8 +325,6 @@ public final class CameraManager {
                    + cameraId);
        } catch (CameraRuntimeException e) {
            throw e.asChecked();
        } catch (RemoteException e) {
            // impossible
        }
        return device;
    }
@@ -444,27 +443,38 @@ public final class CameraManager {
        }
    }

    /**
     * Return or create the list of currently connected camera devices.
     *
     * <p>In case of errors connecting to the camera service, will return an empty list.</p>
     */
    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
        if (mDeviceIdList == null) {
            int numCameras = 0;
            ICameraService cameraService = getCameraServiceLocked();
            ArrayList<String> deviceIdList = new ArrayList<>();

            // If no camera service, then no devices
            if (cameraService == null) {
                return deviceIdList;
            }

            try {
                numCameras = mCameraService.getNumberOfCameras();
                numCameras = cameraService.getNumberOfCameras();
            } catch(CameraRuntimeException e) {
                throw e.asChecked();
            } catch (RemoteException e) {
                // impossible
                return null;
                // camera service just died - if no camera service, then no devices
                return deviceIdList;
            }

            mDeviceIdList = new ArrayList<String>();
            CameraMetadataNative info = new CameraMetadataNative();
            for (int i = 0; i < numCameras; ++i) {
                // Non-removable cameras use integers starting at 0 for their
                // identifiers
                boolean isDeviceSupported = false;
                try {
                    mCameraService.getCameraCharacteristics(i, info);
                    cameraService.getCameraCharacteristics(i, info);
                    if (!info.isEmpty()) {
                        isDeviceSupported = true;
                    } else {
@@ -474,17 +484,27 @@ public final class CameraManager {
                    // Got a BAD_VALUE from service, meaning that this
                    // device is not supported.
                } catch(CameraRuntimeException e) {
                    // DISCONNECTED means that the HAL reported an low-level error getting the
                    // device info; skip listing the device.  Other errors,
                    // propagate exception onward
                    if (e.getReason() != CameraAccessException.CAMERA_DISCONNECTED) {
                        throw e.asChecked();
                    }
                } catch(RemoteException e) {
                    // impossible
                    // Camera service died - no devices to list
                    deviceIdList.clear();
                    return deviceIdList;
                }

                if (isDeviceSupported) {
                    mDeviceIdList.add(String.valueOf(i));
                }
                    deviceIdList.add(String.valueOf(i));
                } else {
                    Log.w(TAG, "Error querying camera device " + i + " for listing.");
                }

            }
            mDeviceIdList = deviceIdList;
        }
        return mDeviceIdList;
    }

@@ -506,8 +526,8 @@ public final class CameraManager {
     * @param cameraId a non-{@code null} camera identifier
     * @return {@code false} if the legacy shim needs to be used, {@code true} otherwise.
     */
    private boolean supportsCamera2Api(String cameraId) {
        return supportsCameraApi(cameraId, API_VERSION_2);
    private boolean supportsCamera2ApiLocked(String cameraId) {
        return supportsCameraApiLocked(cameraId, API_VERSION_2);
    }

    /**
@@ -517,33 +537,125 @@ public final class CameraManager {
     * @param apiVersion the version, i.e. {@code API_VERSION_1} or {@code API_VERSION_2}
     * @return {@code true} if connecting will work for that device version.
     */
    private boolean supportsCameraApi(String cameraId, int apiVersion) {
    private boolean supportsCameraApiLocked(String cameraId, int apiVersion) {
        int id = Integer.parseInt(cameraId);

        /*
         * Possible return values:
         * - NO_ERROR => Camera2 API is supported
         * - CAMERA_DEPRECATED_HAL => Camera2 API is *not* supported (thrown as an exception)
         * - NO_ERROR => CameraX API is supported
         * - CAMERA_DEPRECATED_HAL => CameraX API is *not* supported (thrown as an exception)
         * - Remote exception => If the camera service died
         *
         * Anything else is an unexpected error we don't want to recover from.
         */

        try {
            int res = mCameraService.supportsCameraApi(id, apiVersion);
            ICameraService cameraService = getCameraServiceLocked();
            // If no camera service, no support
            if (cameraService == null) return false;

            if (res != CameraBinderDecorator.NO_ERROR) {
            int res = cameraService.supportsCameraApi(id, apiVersion);

            if (res != CameraServiceBinderDecorator.NO_ERROR) {
                throw new AssertionError("Unexpected value " + res);
            }

            return true;
        } catch (CameraRuntimeException e) {
            if (e.getReason() == CameraAccessException.CAMERA_DEPRECATED_HAL) {
                return false;
            } else {
            if (e.getReason() != CameraAccessException.CAMERA_DEPRECATED_HAL) {
                throw e;
            }
            // API level is not supported
        } catch (RemoteException e) {
            // Camera service is now down, no support for any API level
        }
        return false;
    }

    /**
     * Connect to the camera service if it's available, and set up listeners.
     *
     * <p>Sets mCameraService to a valid pointer or null if the connection does not succeed.</p>
     */
    private void connectCameraServiceLocked() {
        mCameraService = null;
        IBinder cameraServiceBinder = ServiceManager.getService(CAMERA_SERVICE_BINDER_NAME);
        if (cameraServiceBinder == null) {
            // Camera service is now down, leave mCameraService as null
            return;
        }
        try {
            cameraServiceBinder.linkToDeath(new CameraServiceDeathListener(), /*flags*/ 0);
        } catch (RemoteException e) {
            // Camera service is now down, leave mCameraService as null
            return;
        }

        ICameraService cameraServiceRaw = ICameraService.Stub.asInterface(cameraServiceBinder);

        /**
         * Wrap the camera service in a decorator which automatically translates return codes
         * into exceptions.
         */
        ICameraService cameraService = CameraServiceBinderDecorator.newInstance(cameraServiceRaw);

        try {
            CameraServiceBinderDecorator.throwOnError(
                    CameraMetadataNative.nativeSetupGlobalVendorTagDescriptor());
        } catch (CameraRuntimeException e) {
            handleRecoverableSetupErrors(e, "Failed to set up vendor tags");
        }

        try {
            cameraService.addListener(mServiceListener);
            mCameraService = cameraService;
        } catch(CameraRuntimeException e) {
            // Unexpected failure
            throw new IllegalStateException("Failed to register a camera service listener",
                    e.asChecked());
        } catch (RemoteException e) {
            throw new AssertionError("Camera service unreachable", e);
            // Camera service is now down, leave mCameraService as null
        }
    }

    /**
     * Return a best-effort ICameraService.
     *
     * <p>This will be null if the camera service
     * is not currently available. If the camera service has died since the last
     * use of the camera service, will try to reconnect to the service.</p>
     */
    private ICameraService getCameraServiceLocked() {
        if (mCameraService == null) {
            Log.i(TAG, "getCameraServiceLocked: Reconnecting to camera service");
            connectCameraServiceLocked();
            if (mCameraService == null) {
                Log.e(TAG, "Camera service is unavailable");
            }
        }
        return mCameraService;
    }

    /**
     * Listener for camera service death.
     *
     * <p>The camera service isn't supposed to die under any normal circumstances, but can be turned
     * off during debug, or crash due to bugs.  So detect that and null out the interface object, so
     * that the next calls to the manager can try to reconnect.</p>
     */
    private class CameraServiceDeathListener implements IBinder.DeathRecipient {
        public void binderDied() {
            synchronized(mLock) {
                mCameraService = null;
                // Tell listeners that the cameras are _available_, because any existing clients
                // will have gotten disconnected. This is optimistic under the assumption that the
                // service will be back shortly.
                //
                // Without this, a camera service crash while a camera is open will never signal to
                // listeners that previously in-use cameras are now available.
                for (String cameraId : mDeviceIdList) {
                    mServiceListener.onStatusChangedLocked(CameraServiceListener.STATUS_PRESENT,
                            cameraId);
                }
            }
        }
    }

@@ -595,17 +707,54 @@ public final class CameraManager {
            }
        }

        private void postSingleUpdate(final AvailabilityListener listener, final Handler handler,
                final String id, final int status) {
            if (isAvailable(status)) {
                handler.post(
                    new Runnable() {
                        @Override
                        public void run() {
                            listener.onCameraAvailable(id);
                        }
                    });
            } else {
                handler.post(
                    new Runnable() {
                        @Override
                        public void run() {
                            listener.onCameraUnavailable(id);
                        }
                    });
            }
        }

        /**
         * Send the state of all known cameras to the provided listener, to initialize
         * the listener's knowledge of camera state.
         */
        public void updateListenerLocked(AvailabilityListener listener, Handler handler) {
            for (int i = 0; i < mDeviceStatus.size(); i++) {
                String id = mDeviceStatus.keyAt(i);
                Integer status = mDeviceStatus.valueAt(i);
                postSingleUpdate(listener, handler, id, status);
            }
        }

        @Override
        public void onStatusChanged(int status, int cameraId) throws RemoteException {
            synchronized(CameraManager.this.mLock) {
                onStatusChangedLocked(status, String.valueOf(cameraId));
            }
        }

        public void onStatusChangedLocked(int status, String id) {
            if (DEBUG) {
                Log.v(TAG,
                        String.format("Camera id %d has status changed to 0x%x", cameraId, status));

                final String id = String.valueOf(cameraId);
                        String.format("Camera id %s has status changed to 0x%x", id, status));
            }

            if (!validStatus(status)) {
                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
                Log.e(TAG, String.format("Ignoring invalid device %s status 0x%x", id,
                                status));
                return;
            }
@@ -613,9 +762,11 @@ public final class CameraManager {
            Integer oldStatus = mDeviceStatus.put(id, status);

            if (oldStatus != null && oldStatus == status) {
                if (DEBUG) {
                    Log.v(TAG, String.format(
                        "Device status changed to 0x%x, which is what it already was",
                        status));
                }
                return;
            }

@@ -633,13 +784,14 @@ public final class CameraManager {
            //  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 (%d), " +
                                " and is now again available (%d)" +
                                "so no new client visible update will be sent",
                                isAvailable(status), isAvailable(status)));
                }
                return;
            }

@@ -647,25 +799,10 @@ public final class CameraManager {
            for (int i = 0; i < listenerCount; i++) {
                Handler handler = mListenerMap.valueAt(i);
                final AvailabilityListener listener = mListenerMap.keyAt(i);
                    if (isAvailable(status)) {
                        handler.post(
                            new Runnable() {
                                @Override
                                public void run() {
                                    listener.onCameraAvailable(id);
                                }
                            });
                    } else {
                        handler.post(
                            new Runnable() {
                                @Override
                                public void run() {
                                    listener.onCameraUnavailable(id);
                                }
                            });

                postSingleUpdate(listener, handler, id, status);
            }
                } // for
            } // synchronized
        } // onStatusChanged
        } // onStatusChangedLocked

    } // CameraServiceListener
} // CameraManager
+0 −2
Original line number Diff line number Diff line
@@ -294,8 +294,6 @@ public class CameraDeviceImpl extends android.hardware.camera2.CameraDevice {
        final int code = failureCode;
        final boolean isError = failureIsError;
        synchronized(mInterfaceLock) {
            if (mRemoteDevice == null) return; // Camera already closed, can't go to error state

            mInError = true;
            mDeviceHandler.post(new Runnable() {
                @Override
+12 −19

File changed.

Preview size limit exceeded, changes collapsed.

+70 −0

File added.

Preview size limit exceeded, changes collapsed.