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

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

CameraManager: Separate service listener into a singleton

Currently, every CameraManager instance adds itself as a camera service
listener, which has the unfortunate side effect of keeping them all alive
indefinitely.

This is doubly unfortunate since every CameraManager keeps the Context it
was constructed with, and therefore may be leaking whole Activities along
with the CameraManager itself.

Break out a global per-process CameraManager which handles service
connection keepalive and availability listeners, so that local camera
manager instances can go out of scope as expected.

Bug: 18077200

Change-Id: I1be5fb8d3492131e98bb4a84121400d4abb2b9e1
parent 77e25331
Loading
Loading
Loading
Loading
+191 −144
Original line number Diff line number Diff line
@@ -54,29 +54,17 @@ public final class CameraManager {
    private static final String TAG = "CameraManager";
    private final boolean DEBUG;

    /**
     * This should match the ICameraService definition
     */
    private static final String CAMERA_SERVICE_BINDER_NAME = "media.camera";
    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;

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

    private ArrayList<String> mDeviceIdList;

    private final ArrayMap<AvailabilityCallback, Handler> mCallbackMap =
            new ArrayMap<AvailabilityCallback, Handler>();

    private final Context mContext;
    private final Object mLock = new Object();

    private final CameraServiceListener mServiceListener = new CameraServiceListener();

    /**
     * @hide
     */
@@ -84,8 +72,6 @@ public final class CameraManager {
        DEBUG = Log.isLoggable(TAG, Log.DEBUG);
        synchronized(mLock) {
            mContext = context;

            connectCameraServiceLocked();
        }
    }

@@ -116,6 +102,12 @@ public final class CameraManager {
     * <p>The first time a callback is registered, it is immediately called
     * with the availability status of all currently known camera devices.</p>
     *
     * <p>Since this callback will be registered with the camera service, remember to unregister it
     * once it is no longer needed; otherwise the callback will continue to receive events
     * indefinitely and it may prevent other resources from being released. Specifically, the
     * callbacks will be invoked independently of the general activity lifecycle and independently
     * of the state of individual CameraManager instances.</p>
     *
     * @param callback the new callback to send camera availability notices to
     * @param handler The handler on which the callback should be invoked, or
     * {@code null} to use the current thread's {@link android.os.Looper looper}.
@@ -130,13 +122,7 @@ public final class CameraManager {
            handler = new Handler(looper);
        }

        synchronized (mLock) {
            Handler oldHandler = mCallbackMap.put(callback, handler);
            // For new callbacks, provide initial availability information
            if (oldHandler == null) {
                mServiceListener.updateCallbackLocked(callback, handler);
            }
        }
        CameraManagerGlobal.get().registerAvailabilityCallback(callback, handler);
    }

    /**
@@ -148,9 +134,7 @@ public final class CameraManager {
     * @param callback The callback to remove from the notification list
     */
    public void unregisterAvailabilityCallback(AvailabilityCallback callback) {
        synchronized (mLock) {
            mCallbackMap.remove(callback);
        }
        CameraManagerGlobal.get().unregisterAvailabilityCallback(callback);
    }

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

            ICameraService cameraService = getCameraServiceLocked();
            ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
            if (cameraService == null) {
                throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
                        "Camera service is currently unavailable");
@@ -268,7 +252,7 @@ public final class CameraManager {
                try {
                    if (supportsCamera2ApiLocked(cameraId)) {
                        // Use cameraservice's cameradeviceclient implementation for HAL3.2+ devices
                        ICameraService cameraService = getCameraServiceLocked();
                        ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
                        if (cameraService == null) {
                            throw new CameraRuntimeException(
                                CameraAccessException.CAMERA_DISCONNECTED,
@@ -443,13 +427,6 @@ public final class CameraManager {
        }
    }

    /**
     * Temporary for migrating to Callback naming
     * @hide
     */
    public static abstract class AvailabilityListener extends AvailabilityCallback {
    }

    /**
     * Return or create the list of currently connected camera devices.
     *
@@ -458,7 +435,7 @@ public final class CameraManager {
    private ArrayList<String> getOrCreateDeviceIdListLocked() throws CameraAccessException {
        if (mDeviceIdList == null) {
            int numCameras = 0;
            ICameraService cameraService = getCameraServiceLocked();
            ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
            ArrayList<String> deviceIdList = new ArrayList<>();

            // If no camera service, then no devices
@@ -515,18 +492,6 @@ public final class CameraManager {
        return mDeviceIdList;
    }

    private void handleRecoverableSetupErrors(CameraRuntimeException e, String msg) {
        int problem = e.getReason();
        switch (problem) {
            case CameraAccessException.CAMERA_DISCONNECTED:
                String errorMsg = CameraAccessException.getDefaultMessage(problem);
                Log.w(TAG, msg + ": " + errorMsg);
                break;
            default:
                throw new IllegalStateException(msg, e.asChecked());
        }
    }

    /**
     * Queries the camera service if it supports the camera2 api directly, or needs a shim.
     *
@@ -556,7 +521,7 @@ public final class CameraManager {
         * Anything else is an unexpected error we don't want to recover from.
         */
        try {
            ICameraService cameraService = getCameraServiceLocked();
            ICameraService cameraService = CameraManagerGlobal.get().getCameraService();
            // If no camera service, no support
            if (cameraService == null) return false;

@@ -577,6 +542,85 @@ public final class CameraManager {
        return false;
    }

    /**
     * A per-process global camera manager instance, to retain a connection to the camera service,
     * and to distribute camera availability notices to API-registered callbacks
     */
    private static final class CameraManagerGlobal extends ICameraServiceListener.Stub
            implements IBinder.DeathRecipient {

        private static final String TAG = "CameraManagerGlobal";
        private final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

        // 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";

        // Keep up-to-date with ICameraServiceListener.h

        // Device physically unplugged
        public static final int STATUS_NOT_PRESENT = 0;
        // Device physically has been plugged in
        // and the camera can be used exclusively
        public static final int STATUS_PRESENT = 1;
        // Device physically has been plugged in
        // but it will not be connect-able until enumeration is complete
        public static final int STATUS_ENUMERATING = 2;
        // Camera is in use by another app and cannot be used exclusively
        public static final int STATUS_NOT_AVAILABLE = 0x80000000;

        // End enums shared with ICameraServiceListener.h

        // Camera ID -> Status map
        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();

        // Registered availablility callbacks and their handlers
        private final ArrayMap<AvailabilityCallback, Handler> mCallbackMap =
            new ArrayMap<AvailabilityCallback, Handler>();

        private final Object mLock = new Object();

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

        // Singleton, don't allow construction
        private CameraManagerGlobal() {
        }

        public static CameraManagerGlobal get() {
            return gCameraManager;
        }

        @Override
        public IBinder asBinder() {
            return this;
        }

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

        /**
         * Connect to the camera service if it's available, and set up listeners.
         *
@@ -590,7 +634,7 @@ public final class CameraManager {
                return;
            }
            try {
            cameraServiceBinder.linkToDeath(new CameraServiceDeathListener(), /*flags*/ 0);
                cameraServiceBinder.linkToDeath(this, /*flags*/ 0);
            } catch (RemoteException e) {
                // Camera service is now down, leave mCameraService as null
                return;
@@ -602,7 +646,8 @@ public final class CameraManager {
             * Wrap the camera service in a decorator which automatically translates return codes
             * into exceptions.
             */
        ICameraService cameraService = CameraServiceBinderDecorator.newInstance(cameraServiceRaw);
            ICameraService cameraService =
                CameraServiceBinderDecorator.newInstance(cameraServiceRaw);

            try {
                CameraServiceBinderDecorator.throwOnError(
@@ -612,7 +657,7 @@ public final class CameraManager {
            }

            try {
            cameraService.addListener(mServiceListener);
                cameraService.addListener(this);
                mCameraService = cameraService;
            } catch(CameraRuntimeException e) {
                // Unexpected failure
@@ -623,74 +668,16 @@ public final class CameraManager {
            }
        }

    /**
     * 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);
                }
            }
        }
        private void handleRecoverableSetupErrors(CameraRuntimeException e, String msg) {
            int problem = e.getReason();
            switch (problem) {
            case CameraAccessException.CAMERA_DISCONNECTED:
                String errorMsg = CameraAccessException.getDefaultMessage(problem);
                Log.w(TAG, msg + ": " + errorMsg);
                break;
            default:
                throw new IllegalStateException(msg, e.asChecked());
            }

    // TODO: this class needs unit tests
    // TODO: extract class into top level
    private class CameraServiceListener extends ICameraServiceListener.Stub {

        // Keep up-to-date with ICameraServiceListener.h

        // Device physically unplugged
        public static final int STATUS_NOT_PRESENT = 0;
        // Device physically has been plugged in
        // and the camera can be used exclusively
        public static final int STATUS_PRESENT = 1;
        // Device physically has been plugged in
        // but it will not be connect-able until enumeration is complete
        public static final int STATUS_ENUMERATING = 2;
        // Camera is in use by another app and cannot be used exclusively
        public static final int STATUS_NOT_AVAILABLE = 0x80000000;

        // Camera ID -> Status map
        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();

        private static final String TAG = "CameraServiceListener";

        @Override
        public IBinder asBinder() {
            return this;
        }

        private boolean isAvailable(int status) {
@@ -739,7 +726,7 @@ public final class CameraManager {
         * Send the state of all known cameras to the provided listener, to initialize
         * the listener's knowledge of camera state.
         */
        public void updateCallbackLocked(AvailabilityCallback callback, Handler handler) {
        private void updateCallbackLocked(AvailabilityCallback callback, Handler handler) {
            for (int i = 0; i < mDeviceStatus.size(); i++) {
                String id = mDeviceStatus.keyAt(i);
                Integer status = mDeviceStatus.valueAt(i);
@@ -747,14 +734,7 @@ public final class CameraManager {
            }
        }

        @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) {
        private void onStatusChangedLocked(int status, String id) {
            if (DEBUG) {
                Log.v(TAG,
                        String.format("Camera id %s has status changed to 0x%x", id, status));
@@ -811,5 +791,72 @@ public final class CameraManager {
            }
        } // onStatusChangedLocked

    } // CameraServiceListener
        /**
         * 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 handler The handler on which the callback should be invoked. May not be null.
         */
        public void registerAvailabilityCallback(AvailabilityCallback callback, Handler handler) {
            synchronized (mLock) {
                Handler oldHandler = mCallbackMap.put(callback, handler);
                // For new callbacks, provide initial availability information
                if (oldHandler == null) {
                    updateCallbackLocked(callback, handler);
                }
            }
        }

        /**
         * 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);
            }
        }

        /**
         * Callback from camera service notifying the process about camera availability changes
         */
        @Override
        public void onStatusChanged(int status, int cameraId) throws RemoteException {
            synchronized(mLock) {
                onStatusChangedLocked(status, String.valueOf(cameraId));
            }
        }

        /**
         * Listener for camera service death.
         *
         * <p>The camera service isn't supposed to die under any normal circumstances, but can be
         * turned off during debug, or crash due to bugs.  So detect that and null out the interface
         * object, so that the next calls to the manager can try to reconnect.</p>
         */
        public void binderDied() {
            synchronized(mLock) {
                // Only do this once per service death
                if (mCameraService == null) return;

                mCameraService = null;

                // Tell listeners that the cameras 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 (int i = 0; i < mDeviceStatus.size(); i++) {
                    String cameraId = mDeviceStatus.keyAt(i);
                    onStatusChangedLocked(STATUS_PRESENT, cameraId);
                }
            }
        }

    } // CameraManagerGlobal

} // CameraManager