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
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 9a3d806..a4a1559 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -23,7 +23,7 @@
 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 @@
 public final class CameraManager {
 
     private static final String TAG = "CameraManager";
+    private final boolean DEBUG;
 
     /**
      * This should match the ICameraService definition
@@ -63,7 +64,9 @@
     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 @@
     private final Context mContext;
     private final Object mLock = new Object();
 
+    private final CameraServiceListener mServiceListener = new CameraServiceListener();
+
     /**
      * @hide
      */
     public CameraManager(Context context) {
-        mContext = 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 String[] getCameraIdList() throws CameraAccessException {
         synchronized (mLock) {
-            try {
-                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);
-            }
+            // 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]);
         }
     }
 
@@ -132,6 +113,9 @@
      * <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 @@
         }
 
         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,64 +161,67 @@
      * @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
+     *         access the camera
      *
      * @see #getCameraIdList
      * @see android.app.admin.DevicePolicyManager#setCameraDisabled
      */
     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);
+            int id = Integer.valueOf(cameraId);
 
-        /*
-         * Get the camera characteristics from the camera service directly if it supports it,
-         * otherwise get them from the legacy shim instead.
-         */
+            /*
+             * Get the camera characteristics from the camera service directly if it supports it,
+             * 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);
-                String parameters = outParameters[0];
+                if (!supportsCamera2ApiLocked(cameraId)) {
+                    // Legacy backwards compatibility path; build static info from the camera
+                    // parameters
+                    String[] outParameters = new String[1];
 
-                CameraInfo info = new CameraInfo();
-                mCameraService.getCameraInfo(id, /*out*/info);
+                    cameraService.getLegacyParameters(id, /*out*/outParameters);
+                    String parameters = outParameters[0];
 
-                return LegacyMetadataMapper.createCharacteristics(parameters, info);
-            } catch (RemoteException e) {
-                // Impossible
-                return null;
+                    CameraInfo info = new CameraInfo();
+                    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();
+
+                    cameraService.getCameraCharacteristics(id, info);
+
+                    characteristics = new CameraCharacteristics(info);
+                }
             } catch (CameraRuntimeException e) {
                 throw e.asChecked();
+            } 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);
             }
-
-        } else {
-            // Normal path: Get the camera characteristics directly from the camera service
-            CameraMetadataNative info = new CameraMetadataNative();
-
-            try {
-                mCameraService.getCameraCharacteristics(id, info);
-            } catch(CameraRuntimeException e) {
-                throw e.asChecked();
-            } catch(RemoteException e) {
-                // impossible
-                return null;
-            }
-
-            return new CameraCharacteristics(info);
         }
+        return characteristics;
     }
 
     /**
@@ -278,10 +266,16 @@
                 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 @@
                         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 @@
                     + cameraId);
         } catch (CameraRuntimeException e) {
             throw e.asChecked();
-        } catch (RemoteException e) {
-            // impossible
         }
         return device;
     }
@@ -444,27 +443,38 @@
         }
     }
 
+    /**
+     * 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,16 +484,26 @@
                     // Got a BAD_VALUE from service, meaning that this
                     // device is not supported.
                 } catch(CameraRuntimeException e) {
-                    throw e.asChecked();
+                    // 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 @@
      * @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 @@
      * @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) {
-            throw new AssertionError("Camera service unreachable", 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) {
+            // 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,77 +707,102 @@
             }
         }
 
+        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));
+                        String.format("Camera id %s has status changed to 0x%x", id, status));
+            }
 
-                final String id = String.valueOf(cameraId);
+            if (!validStatus(status)) {
+                Log.e(TAG, String.format("Ignoring invalid device %s status 0x%x", id,
+                                status));
+                return;
+            }
 
-                if (!validStatus(status)) {
-                    Log.e(TAG, String.format("Ignoring invalid device %d status 0x%x", cameraId,
-                            status));
-                    return;
-                }
+            Integer oldStatus = mDeviceStatus.put(id, status);
 
-                Integer oldStatus = mDeviceStatus.put(id, status);
-
-                if (oldStatus != null && oldStatus == 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;
+                        "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);
+            // 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)) {
-
+            // 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 (%d), " +
-                                            " and is now again available (%d)" +
-                                            "so no new client visible update will be sent",
-                                    isAvailable(status), isAvailable(status)));
-                    return;
+                                "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;
+            }
 
-                final int listenerCount = mListenerMap.size();
-                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);
-                                }
-                            });
-                    }
-                } // for
-            } // synchronized
-        } // onStatusChanged
+            final int listenerCount = mListenerMap.size();
+            for (int i = 0; i < listenerCount; i++) {
+                Handler handler = mListenerMap.valueAt(i);
+                final AvailabilityListener listener = mListenerMap.keyAt(i);
+
+                postSingleUpdate(listener, handler, id, status);
+            }
+        } // onStatusChangedLocked
+
     } // CameraServiceListener
 } // CameraManager
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index fb1bc15..ed4e457 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -294,8 +294,6 @@
         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
diff --git a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java
index 898c746..83ebadd 100644
--- a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java
+++ b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java
@@ -28,7 +28,7 @@
 import java.lang.reflect.Method;
 
 /**
- * Translate camera service status_t return values into exceptions.
+ * Translate camera device status_t return values into exceptions.
  *
  * @see android.hardware.camera2.utils.CameraBinderDecorator#newInstance
  * @hide
@@ -57,7 +57,7 @@
     public static final int EUSERS = -87;
 
 
-    private static class CameraBinderDecoratorListener implements Decorator.DecoratorListener {
+    static class CameraBinderDecoratorListener implements Decorator.DecoratorListener {
 
         @Override
         public void onBeforeInvocation(Method m, Object[] args) {
@@ -76,10 +76,9 @@
         public boolean onCatchException(Method m, Object[] args, Throwable t) {
 
             if (t instanceof DeadObjectException) {
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_DISCONNECTED,
+                throw new CameraRuntimeException(CAMERA_DISCONNECTED,
                         "Process hosting the camera service has died unexpectedly",
-                        t));
+                        t);
             } else if (t instanceof RemoteException) {
                 throw new UnsupportedOperationException("An unknown RemoteException was thrown" +
                         " which should never happen.", t);
@@ -112,26 +111,20 @@
             case BAD_VALUE:
                 throw new IllegalArgumentException("Bad argument passed to camera service");
             case DEAD_OBJECT:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_DISCONNECTED));
+                throw new CameraRuntimeException(CAMERA_DISCONNECTED);
             case EACCES:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_DISABLED));
+                throw new CameraRuntimeException(CAMERA_DISABLED);
             case EBUSY:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_IN_USE));
+                throw new CameraRuntimeException(CAMERA_IN_USE);
             case EUSERS:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        MAX_CAMERAS_IN_USE));
+                throw new CameraRuntimeException(MAX_CAMERAS_IN_USE);
             case ENODEV:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_DISCONNECTED));
+                throw new CameraRuntimeException(CAMERA_DISCONNECTED);
             case EOPNOTSUPP:
-                UncheckedThrow.throwAnyException(new CameraRuntimeException(
-                        CAMERA_DEPRECATED_HAL));
+                throw new CameraRuntimeException(CAMERA_DEPRECATED_HAL);
             case INVALID_OPERATION:
-                UncheckedThrow.throwAnyException(new IllegalStateException(
-                        "Illegal state encountered in camera service."));
+                throw new IllegalStateException(
+                        "Illegal state encountered in camera service.");
         }
 
         /**
diff --git a/core/java/android/hardware/camera2/utils/CameraServiceBinderDecorator.java b/core/java/android/hardware/camera2/utils/CameraServiceBinderDecorator.java
new file mode 100644
index 0000000..c1fb6b1
--- /dev/null
+++ b/core/java/android/hardware/camera2/utils/CameraServiceBinderDecorator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 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.utils;
+
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Translate camera service status_t return values into exceptions.
+ *
+ * @see android.hardware.camera2.utils.CameraBinderDecorator#newInstance
+ * @hide
+ */
+public class CameraServiceBinderDecorator extends CameraBinderDecorator {
+
+    private static final String TAG = "CameraServiceBinderDecorator";
+
+    static class CameraServiceBinderDecoratorListener
+            extends CameraBinderDecorator.CameraBinderDecoratorListener {
+
+        // Pass through remote exceptions, unlike CameraBinderDecorator
+        @Override
+        public boolean onCatchException(Method m, Object[] args, Throwable t) {
+
+            if (t instanceof DeadObjectException) {
+                // Can sometimes happen (camera service died)
+                // Pass on silently
+            } else if (t instanceof RemoteException) {
+                // Some other kind of remote exception - this is not normal, so let's at least
+                // note it before moving on
+                Log.e(TAG, "Unexpected RemoteException from camera service call.", t);
+            }
+            // All other exceptions also get sent onward
+            return false;
+        }
+
+    }
+
+    /**
+     * <p>
+     * Wraps the type T with a proxy that will check 'status_t' return codes
+     * from the native side of the camera service, and throw Java exceptions
+     * automatically based on the code.
+     * </p>
+     *
+     * @param obj object that will serve as the target for all method calls
+     * @param <T> the type of the element you want to wrap. This must be an interface.
+     * @return a proxy that will intercept all invocations to obj
+     */
+    public static <T> T newInstance(T obj) {
+        return Decorator.<T> newInstance(obj, new CameraServiceBinderDecoratorListener());
+    }
+}