Merge tag android-5.1.0_r1 into AOSP_5.1_MERGE

Change-Id: I6773b6beb0a3b37410c39a16bfcd203d5a13c435
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
index a746634..2fc4ad3 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2AgentImpl.java
@@ -65,6 +65,7 @@
     private final DispatchThread mDispatchThread;
     private final CameraManager mCameraManager;
     private final MediaActionSound mNoisemaker;
+    private CameraExceptionHandler mExceptionHandler;
 
     /**
      * Number of camera devices.  The length of {@code mCameraDevices} does not reveal this
@@ -86,6 +87,7 @@
         mCameraHandlerThread = new HandlerThread("Camera2 Handler Thread");
         mCameraHandlerThread.start();
         mCameraHandler = new Camera2Handler(mCameraHandlerThread.getLooper());
+        mExceptionHandler = new CameraExceptionHandler(mCameraHandler);
         mCameraState = new AndroidCamera2StateHolder();
         mDispatchThread = new DispatchThread(mCameraHandler, mCameraHandlerThread);
         mDispatchThread.start();
@@ -134,11 +136,6 @@
 
     // TODO: Implement
     @Override
-    public void setCameraDefaultExceptionCallback(CameraExceptionCallback callback,
-            Handler handler) {}
-
-    // TODO: Implement
-    @Override
     public void recycle() {}
 
     // TODO: Some indices may now be invalid; ensure everyone can handle that and update the docs
@@ -159,6 +156,21 @@
         return mDispatchThread;
     }
 
+    @Override
+    protected CameraStateHolder getCameraState() {
+        return mCameraState;
+    }
+
+    @Override
+    protected CameraExceptionHandler getCameraExceptionHandler() {
+        return mExceptionHandler;
+    }
+
+    @Override
+    public void setCameraExceptionHandler(CameraExceptionHandler exceptionHandler) {
+        mExceptionHandler = exceptionHandler;
+    }
+
     private static abstract class CaptureAvailableListener
             extends CameraCaptureSession.CaptureCallback
             implements ImageReader.OnImageAvailableListener {};
@@ -210,8 +222,9 @@
         public void handleMessage(final Message msg) {
             super.handleMessage(msg);
             Log.v(TAG, "handleMessage - action = '" + CameraActions.stringify(msg.what) + "'");
+            int cameraAction = msg.what;
             try {
-                switch(msg.what) {
+                switch (cameraAction) {
                     case CameraActions.OPEN_CAMERA:
                     case CameraActions.RECONNECT: {
                         CameraOpenCallback openCallback = (CameraOpenCallback) msg.obj;
@@ -628,12 +641,12 @@
                     }
                 }
             } catch (final Exception ex) {
-                if (msg.what != CameraActions.RELEASE && mCamera != null) {
+                if (cameraAction != CameraActions.RELEASE && mCamera != null) {
                     // TODO: Handle this better
                     mCamera.close();
                     mCamera = null;
                 } else if (mCamera == null) {
-                    if (msg.what == CameraActions.OPEN_CAMERA) {
+                    if (cameraAction == CameraActions.OPEN_CAMERA) {
                         if (mOpenCallback != null) {
                             mOpenCallback.onDeviceOpenFailure(mCameraIndex,
                                     generateHistoryString(mCameraIndex));
@@ -645,11 +658,9 @@
                 }
 
                 if (ex instanceof RuntimeException) {
-                    post(new Runnable() {
-                        @Override
-                        public void run() {
-                            sCameraExceptionCallback.onCameraException((RuntimeException) ex);
-                        }});
+                    String commandHistory = generateHistoryString(Integer.parseInt(mCameraId));
+                    mExceptionHandler.onCameraException((RuntimeException) ex, commandHistory,
+                            cameraAction, mCameraState.getState());
                 }
             } finally {
                 WaitDoneBundle.unblockSyncWaiters(msg);
@@ -771,8 +782,10 @@
                     try {
                         CameraCharacteristics props =
                                 mCameraManager.getCameraCharacteristics(mCameraId);
-                        mCameraProxy = new AndroidCamera2ProxyImpl(mCameraIndex, mCamera,
-                                    getCameraDeviceInfo().getCharacteristics(mCameraIndex), props);
+                        CameraDeviceInfo.Characteristics characteristics =
+                                getCameraDeviceInfo().getCharacteristics(mCameraIndex);
+                        mCameraProxy = new AndroidCamera2ProxyImpl(AndroidCamera2AgentImpl.this,
+                                mCameraIndex, mCamera, characteristics, props);
                         mPersistentSettings = new Camera2RequestSettingsSet();
                         mActiveArray =
                                 props.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
@@ -960,6 +973,7 @@
     }
 
     private class AndroidCamera2ProxyImpl extends CameraAgent.CameraProxy {
+        private final AndroidCamera2AgentImpl mCameraAgent;
         private final int mCameraIndex;
         private final CameraDevice mCamera;
         private final CameraDeviceInfo.Characteristics mCharacteristics;
@@ -967,9 +981,13 @@
         private CameraSettings mLastSettings;
         private boolean mShutterSoundEnabled;
 
-        public AndroidCamera2ProxyImpl(int cameraIndex, CameraDevice camera,
+        public AndroidCamera2ProxyImpl(
+                AndroidCamera2AgentImpl agent,
+                int cameraIndex,
+                CameraDevice camera,
                 CameraDeviceInfo.Characteristics characteristics,
                 CameraCharacteristics properties) {
+            mCameraAgent = agent;
             mCameraIndex = cameraIndex;
             mCamera = camera;
             mCharacteristics = characteristics;
@@ -997,6 +1015,10 @@
             return mCapabilities;
         }
 
+        public CameraAgent getAgent() {
+            return mCameraAgent;
+        }
+
         private AndroidCamera2Capabilities getSpecializedCapabilities() {
             return mCapabilities;
         }
@@ -1039,53 +1061,67 @@
 
         @Override
         public void autoFocus(final Handler handler, final CameraAFCallback cb) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    CameraAFCallback cbForward = null;
-                    if (cb != null) {
-                        cbForward = new CameraAFCallback() {
-                            @Override
-                            public void onAutoFocus(final boolean focused,
-                                                    final CameraProxy camera) {
-                                handler.post(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        cb.onAutoFocus(focused, camera);
-                                    }});
-                            }};
-                    }
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        CameraAFCallback cbForward = null;
+                        if (cb != null) {
+                            cbForward = new CameraAFCallback() {
+                                @Override
+                                public void onAutoFocus(final boolean focused,
+                                                        final CameraProxy camera) {
+                                    handler.post(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            cb.onAutoFocus(focused, camera);
+                                        }
+                                    });
+                                }
+                            };
+                        }
 
-                    mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE |
-                            AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
-                    mCameraHandler.obtainMessage(CameraActions.AUTO_FOCUS, cbForward)
-                            .sendToTarget();
-                }});
+                        mCameraState.waitForStates(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE |
+                                AndroidCamera2StateHolder.CAMERA_FOCUS_LOCKED);
+                        mCameraHandler.obtainMessage(CameraActions.AUTO_FOCUS, cbForward)
+                                .sendToTarget();
+                    }
+                });
+            } catch (RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
         @Override
         public void setAutoFocusMoveCallback(final Handler handler, final CameraAFMoveCallback cb) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    CameraAFMoveCallback cbForward = null;
-                    if (cb != null) {
-                        cbForward = new CameraAFMoveCallback() {
-                            @Override
-                            public void onAutoFocusMoving(final boolean moving,
-                                                          final CameraProxy camera) {
-                                handler.post(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        cb.onAutoFocusMoving(moving, camera);
-                                    }});
-                                }};
-                    }
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        CameraAFMoveCallback cbForward = null;
+                        if (cb != null) {
+                            cbForward = new CameraAFMoveCallback() {
+                                @Override
+                                public void onAutoFocusMoving(final boolean moving,
+                                                              final CameraProxy camera) {
+                                    handler.post(new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            cb.onAutoFocusMoving(moving, camera);
+                                        }
+                                    });
+                                }
+                            };
+                        }
 
-                    mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK,
-                            cbForward).sendToTarget();
-                }});
+                        mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK,
+                                cbForward).sendToTarget();
+                    }
+                });
+            } catch (RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Override
@@ -1127,15 +1163,20 @@
                         }
                     }
                 }};
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    // Wait until PREVIEW_ACTIVE or better
-                    mCameraState.waitForStates(
-                            ~(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE - 1));
-                    mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener)
-                            .sendToTarget();
-                }});
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Wait until PREVIEW_ACTIVE or better
+                        mCameraState.waitForStates(
+                                ~(AndroidCamera2StateHolder.CAMERA_PREVIEW_ACTIVE - 1));
+                        mCameraHandler.obtainMessage(CameraActions.CAPTURE_PHOTO, picListener)
+                                .sendToTarget();
+                    }
+                });
+            } catch (RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         // TODO: Implement
@@ -1157,10 +1198,6 @@
 
         // TODO: Implement
         @Override
-        public void setErrorCallback(Handler handler, CameraErrorCallback cb) {}
-
-        // TODO: Implement
-        @Override
         public void setParameters(android.hardware.Camera.Parameters params) {}
 
         // TODO: Implement
@@ -1380,12 +1417,4 @@
             }
         }
     }
-
-    private static final CameraExceptionCallback sCameraExceptionCallback =
-            new CameraExceptionCallback() {
-                @Override
-                public synchronized void onCameraException(RuntimeException e) {
-                    throw e;
-                }
-            };
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
index 540d8df..0062097 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCamera2Settings.java
@@ -228,6 +228,11 @@
         } else if (setting == CONTROL_AWB_LOCK) {
             return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK));
         } else if (setting == JPEG_THUMBNAIL_SIZE) {
+            if (mExifThumbnailSize == null) {
+                // It doesn't matter if this is true or false since setting this
+                // to null in the request settings will use the default anyway.
+                return false;
+            }
             android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
             return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) ||
                     (defaultThumbnailSize != null &&
@@ -273,9 +278,13 @@
         updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked);
         // TODO: mRecordingHintEnabled
         updateRequestGpsData();
-        updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE,
-                new android.util.Size(
-                        mExifThumbnailSize.width(), mExifThumbnailSize.height()));
+        if (mExifThumbnailSize != null) {
+            updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE,
+                    new android.util.Size(
+                            mExifThumbnailSize.width(), mExifThumbnailSize.height()));
+        } else {
+            updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, null);
+        }
 
         return mRequestSettings;
     }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
index 992fcec..201a905 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraAgentImpl.java
@@ -56,38 +56,42 @@
     private final CameraStateHolder mCameraState;
     private final DispatchThread mDispatchThread;
 
-    private Handler mCameraExceptionCallbackHandler;
-    private CameraExceptionCallback mCameraExceptionCallback =
-        new CameraExceptionCallback() {
-            @Override
-            public void onCameraException(RuntimeException e) {
-                throw e;
-            }
-        };
+    private static final CameraExceptionHandler sDefaultExceptionHandler =
+            new CameraExceptionHandler(null) {
+        @Override
+        public void onCameraError(int errorCode) {
+            Log.w(TAG, "onCameraError called with no handler set: " + errorCode);
+        }
+
+        @Override
+        public void onCameraException(RuntimeException ex, String commandHistory, int action,
+                int state) {
+            Log.w(TAG, "onCameraException called with no handler set", ex);
+        }
+
+        @Override
+        public void onDispatchThreadException(RuntimeException ex) {
+            Log.w(TAG, "onDispatchThreadException called with no handler set", ex);
+        }
+    };
+
+    private CameraExceptionHandler mExceptionHandler = sDefaultExceptionHandler;
 
     AndroidCameraAgentImpl() {
         mCameraHandlerThread = new HandlerThread("Camera Handler Thread");
         mCameraHandlerThread.start();
-        mCameraHandler = new CameraHandler(mCameraHandlerThread.getLooper());
-        mCameraExceptionCallbackHandler = mCameraHandler;
+        mCameraHandler = new CameraHandler(this, mCameraHandlerThread.getLooper());
+        mExceptionHandler = new CameraExceptionHandler(mCameraHandler);
         mCameraState = new AndroidCameraStateHolder();
         mDispatchThread = new DispatchThread(mCameraHandler, mCameraHandlerThread);
         mDispatchThread.start();
     }
 
     @Override
-    public void setCameraDefaultExceptionCallback(CameraExceptionCallback callback,
-            Handler handler) {
-        synchronized (mCameraExceptionCallback) {
-            mCameraExceptionCallback = callback;
-            mCameraExceptionCallbackHandler = handler;
-        }
-    }
-
-    @Override
     public void recycle() {
         closeCamera(null, true);
         mDispatchThread.end();
+        mCameraState.invalidate();
     }
 
     @Override
@@ -105,6 +109,22 @@
         return mDispatchThread;
     }
 
+    @Override
+    protected CameraStateHolder getCameraState() {
+        return mCameraState;
+    }
+
+    @Override
+    protected CameraExceptionHandler getCameraExceptionHandler() {
+        return mExceptionHandler;
+    }
+
+    @Override
+    public void setCameraExceptionHandler(CameraExceptionHandler exceptionHandler) {
+        // In case of null set the default handler to route exceptions to logs
+        mExceptionHandler = exceptionHandler != null ? exceptionHandler : sDefaultExceptionHandler;
+    }
+
     private static class AndroidCameraDeviceInfo implements CameraDeviceInfo {
         private final Camera.CameraInfo[] mCameraInfos;
         private final int mNumberOfCameras;
@@ -237,10 +257,10 @@
     /**
      * The handler on which the actual camera operations happen.
      */
-    private class CameraHandler extends HistoryHandler {
-
+    private class CameraHandler extends HistoryHandler implements Camera.ErrorCallback {
+        private CameraAgent mAgent;
         private Camera mCamera;
-        private int mCameraId;
+        private int mCameraId = -1;
         private ParametersCache mParameterCache;
         private int mCancelAfPending = 0;
 
@@ -259,8 +279,9 @@
             }
         }
 
-        CameraHandler(Looper looper) {
+        CameraHandler(CameraAgent agent, Looper looper) {
             super(looper);
+            mAgent = agent;
         }
 
         private void startFaceDetection() {
@@ -298,16 +319,6 @@
             }
         }
 
-        private void capture(final CaptureCallbacks cb) {
-            try {
-                mCamera.takePicture(cb.mShutter, cb.mRaw, cb.mPostView, cb.mJpeg);
-            } catch (RuntimeException e) {
-                // TODO: output camera state and focus state for debugging.
-                Log.e(TAG, "take picture failed.");
-                throw e;
-            }
-        }
-
         public void requestTakePicture(
                 final ShutterCallback shutter,
                 final PictureCallback raw,
@@ -317,6 +328,19 @@
             obtainMessage(CameraActions.CAPTURE_PHOTO, callbacks).sendToTarget();
         }
 
+        @Override
+        public void onError(final int errorCode, Camera camera) {
+            mExceptionHandler.onCameraError(errorCode);
+            if (errorCode == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) {
+                int lastCameraAction = getCurrentMessage();
+                mExceptionHandler.onCameraException(
+                        new RuntimeException("Media server died."),
+                        generateHistoryString(mCameraId),
+                        lastCameraAction,
+                        mCameraState.getState());
+            }
+        }
+
         /**
          * This method does not deal with the API level check.  Everyone should
          * check first for supported operations before sending message to this handler.
@@ -324,9 +348,16 @@
         @Override
         public void handleMessage(final Message msg) {
             super.handleMessage(msg);
+
+            if (getCameraState().isInvalid()) {
+                Log.v(TAG, "Skip handleMessage - action = '" + CameraActions.stringify(msg.what) + "'");
+                return;
+            }
             Log.v(TAG, "handleMessage - action = '" + CameraActions.stringify(msg.what) + "'");
+
+            int cameraAction = msg.what;
             try {
-                switch (msg.what) {
+                switch (cameraAction) {
                     case CameraActions.OPEN_CAMERA: {
                         final CameraOpenCallback openCallback = (CameraOpenCallback) msg.obj;
                         final int cameraId = msg.arg1;
@@ -346,11 +377,13 @@
                             mCapabilities = new AndroidCameraCapabilities(
                                     mParameterCache.getBlocking());
 
+                            mCamera.setErrorCallback(this);
+
                             mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE);
                             if (openCallback != null) {
-                                openCallback.onCameraOpened(
-                                        new AndroidCameraProxyImpl(cameraId, mCamera,
-                                                mCharacteristics, mCapabilities));
+                                CameraProxy cameraProxy = new AndroidCameraProxyImpl(
+                                        mAgent, cameraId, mCamera, mCharacteristics, mCapabilities);
+                                openCallback.onCameraOpened(cameraProxy);
                             }
                         } else {
                             if (openCallback != null) {
@@ -365,6 +398,7 @@
                             mCamera.release();
                             mCameraState.setState(AndroidCameraStateHolder.CAMERA_UNOPENED);
                             mCamera = null;
+                            mCameraId = -1;
                         } else {
                             Log.w(TAG, "Releasing camera without any camera opened.");
                         }
@@ -379,8 +413,7 @@
                             mCamera.reconnect();
                         } catch (IOException ex) {
                             if (cbForward != null) {
-                                cbForward.onReconnectionFailure(AndroidCameraAgentImpl.this,
-                                        generateHistoryString(mCameraId));
+                                cbForward.onReconnectionFailure(mAgent, generateHistoryString(mCameraId));
                             }
                             break;
                         }
@@ -388,8 +421,8 @@
                         mCameraState.setState(AndroidCameraStateHolder.CAMERA_IDLE);
                         if (cbForward != null) {
                             cbForward.onCameraOpened(
-                                    new AndroidCameraProxyImpl(cameraId, mCamera, mCharacteristics,
-                                            mCapabilities));
+                                    new AndroidCameraProxyImpl(AndroidCameraAgentImpl.this,
+                                            cameraId, mCamera, mCharacteristics, mCapabilities));
                         }
                         break;
                     }
@@ -527,11 +560,6 @@
                         break;
                     }
 
-                    case CameraActions.SET_ERROR_CALLBACK: {
-                        mCamera.setErrorCallback((ErrorCallback) msg.obj);
-                        break;
-                    }
-
                     case CameraActions.APPLY_SETTINGS: {
                         Parameters parameters = mParameterCache.getBlocking();
                         CameraSettings settings = (CameraSettings) msg.obj;
@@ -573,45 +601,50 @@
 
                     case CameraActions.CAPTURE_PHOTO: {
                         mCameraState.setState(AndroidCameraStateHolder.CAMERA_CAPTURING);
-                        capture((CaptureCallbacks) msg.obj);
+                        CaptureCallbacks captureCallbacks = (CaptureCallbacks) msg.obj;
+                        mCamera.takePicture(
+                                captureCallbacks.mShutter,
+                                captureCallbacks.mRaw,
+                                captureCallbacks.mPostView,
+                                captureCallbacks.mJpeg);
                         break;
                     }
 
                     default: {
-                        throw new RuntimeException("Invalid CameraProxy message=" + msg.what);
+                        Log.e(TAG, "Invalid CameraProxy message=" + msg.what);
                     }
                 }
-            } catch (final RuntimeException e) {
-                Log.e(TAG, "Exception during camera operation " + msg.what, e);
-                if (msg.what != CameraActions.RELEASE && mCamera != null) {
+            } catch (final RuntimeException ex) {
+                int cameraState = mCameraState.getState();
+                String errorContext = "CameraAction[" + CameraActions.stringify(cameraAction) +
+                        "] at CameraState[" + cameraState + "]";
+                Log.e(TAG, "RuntimeException during " + errorContext, ex);
+
+                // Be conservative by invalidating both CameraAgent and CameraProxy objects.
+                mCameraState.invalidate();
+
+                if (mCamera != null) {
+                    Log.i(TAG, "Release camera since mCamera is not null.");
                     try {
                         mCamera.release();
-                        mCameraState.setState(AndroidCameraStateHolder.CAMERA_UNOPENED);
-                    } catch (Exception ex) {
-                        Log.e(TAG, "Fail to release the camera.");
-                    }
-                    mCamera = null;
-                } else {
-                    if (mCamera == null) {
-                        if (msg.what == CameraActions.OPEN_CAMERA) {
-                            final int cameraId = msg.arg1;
-                            if (msg.obj != null) {
-                                ((CameraOpenCallback) msg.obj).onDeviceOpenFailure(
-                                        msg.arg1, generateHistoryString(cameraId));
-                            }
-                        } else {
-                            Log.w(TAG, "Cannot handle message " + msg.what + ", mCamera is null.");
-                        }
-                        return;
+                    } catch (Exception e) {
+                        Log.e(TAG, "Fail when calling Camera.release().", e);
+                    } finally {
+                        mCamera = null;
                     }
                 }
-                synchronized (mCameraExceptionCallback) {
-                    mCameraExceptionCallbackHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                                mCameraExceptionCallback.onCameraException(e);
-                            }
-                        });
+
+                // Invoke error callback.
+                if (msg.what == CameraActions.OPEN_CAMERA && mCamera == null) {
+                    final int cameraId = msg.arg1;
+                    if (msg.obj != null) {
+                        ((CameraOpenCallback) msg.obj).onDeviceOpenFailure(
+                                msg.arg1, generateHistoryString(cameraId));
+                    }
+                } else {
+                    CameraExceptionHandler exceptionHandler = mAgent.getCameraExceptionHandler();
+                    exceptionHandler.onCameraException(
+                            ex, generateHistoryString(mCameraId), cameraAction, cameraState);
                 }
             } finally {
                 WaitDoneBundle.unblockSyncWaiters(msg);
@@ -670,7 +703,9 @@
             }
             parameters.setRecordingHint(settings.isRecordingHintEnabled());
             Size jpegThumbSize = settings.getExifThumbnailSize();
-            parameters.setJpegThumbnailSize(jpegThumbSize.width(), jpegThumbSize.height());
+            if (jpegThumbSize != null) {
+                parameters.setJpegThumbnailSize(jpegThumbSize.width(), jpegThumbSize.height());
+            }
             parameters.setPictureFormat(settings.getCurrentPhotoFormat());
 
             CameraSettings.GpsData gpsData = settings.getGpsData();
@@ -720,15 +755,20 @@
      * camera handler thread.
      */
     private class AndroidCameraProxyImpl extends CameraAgent.CameraProxy {
+        private final CameraAgent mCameraAgent;
         private final int mCameraId;
         /* TODO: remove this Camera instance. */
         private final Camera mCamera;
         private final CameraDeviceInfo.Characteristics mCharacteristics;
         private final AndroidCameraCapabilities mCapabilities;
 
-        private AndroidCameraProxyImpl(int cameraId, Camera camera,
+        private AndroidCameraProxyImpl(
+                CameraAgent cameraAgent,
+                int cameraId,
+                Camera camera,
                 CameraDeviceInfo.Characteristics characteristics,
                 AndroidCameraCapabilities capabilities) {
+            mCameraAgent = cameraAgent;
             mCamera = camera;
             mCameraId = cameraId;
             mCharacteristics = characteristics;
@@ -738,6 +778,9 @@
         @Deprecated
         @Override
         public android.hardware.Camera getCamera() {
+            if (getCameraState().isInvalid()) {
+                return null;
+            }
             return mCamera;
         }
 
@@ -757,6 +800,11 @@
         }
 
         @Override
+        public CameraAgent getAgent() {
+            return mCameraAgent;
+        }
+
+        @Override
         public void setPreviewDataCallback(
                 final Handler handler, final CameraPreviewDataCallback cb) {
             mDispatchThread.runJob(new Runnable() {
@@ -819,6 +867,10 @@
             mDispatchThread.runJob(new Runnable() {
                 @Override
                 public void run() {
+                    // Don't bother to wait since camera is in bad state.
+                    if (getCameraState().isInvalid()) {
+                        return;
+                    }
                     mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE);
                     mCameraHandler.obtainMessage(CameraActions.AUTO_FOCUS, afCallback)
                             .sendToTarget();
@@ -830,15 +882,19 @@
         @Override
         public void setAutoFocusMoveCallback(
                 final Handler handler, final CameraAFMoveCallback cb) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK,
-                            AFMoveCallbackForward.getNewInstance(
-                                    handler, AndroidCameraProxyImpl.this, cb))
-                            .sendToTarget();
-                }
-            });
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraHandler.obtainMessage(CameraActions.SET_AUTO_FOCUS_MOVE_CALLBACK,
+                                AFMoveCallbackForward.getNewInstance(
+                                        handler, AndroidCameraProxyImpl.this, cb))
+                                .sendToTarget();
+                    }
+                });
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Override
@@ -863,59 +919,62 @@
                 }
             };
 
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE |
-                            AndroidCameraStateHolder.CAMERA_UNLOCKED);
-                    mCameraHandler.requestTakePicture(ShutterCallbackForward
-                                    .getNewInstance(handler, AndroidCameraProxyImpl.this, shutter),
-                            PictureCallbackForward
-                                    .getNewInstance(handler, AndroidCameraProxyImpl.this, raw),
-                            PictureCallbackForward
-                                    .getNewInstance(handler, AndroidCameraProxyImpl.this, post),
-                            jpegCallback
-                    );
-                }
-            });
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Don't bother to wait since camera is in bad state.
+                        if (getCameraState().isInvalid()) {
+                            return;
+                        }
+                        mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE |
+                                AndroidCameraStateHolder.CAMERA_UNLOCKED);
+                        mCameraHandler.requestTakePicture(ShutterCallbackForward
+                                        .getNewInstance(handler, AndroidCameraProxyImpl.this, shutter),
+                                PictureCallbackForward
+                                        .getNewInstance(handler, AndroidCameraProxyImpl.this, raw),
+                                PictureCallbackForward
+                                        .getNewInstance(handler, AndroidCameraProxyImpl.this, post),
+                                jpegCallback
+                        );
+                    }
+                });
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Override
         public void setZoomChangeListener(final OnZoomChangeListener listener) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraHandler.obtainMessage(CameraActions.SET_ZOOM_CHANGE_LISTENER, listener)
-                            .sendToTarget();
-                }
-            });
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraHandler.obtainMessage(CameraActions.SET_ZOOM_CHANGE_LISTENER, listener)
+                                .sendToTarget();
+                    }
+                });
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Override
         public void setFaceDetectionCallback(final Handler handler,
                 final CameraFaceDetectionCallback cb) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraHandler.obtainMessage(CameraActions.SET_FACE_DETECTION_LISTENER,
-                            FaceDetectionCallbackForward
-                                    .getNewInstance(handler, AndroidCameraProxyImpl.this, cb))
-                            .sendToTarget();
-                }
-            });
-        }
-
-        @Override
-        public void setErrorCallback(final Handler handler, final CameraErrorCallback cb) {
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraHandler.obtainMessage(CameraActions.SET_ERROR_CALLBACK,
-                            ErrorCallbackForward.getNewInstance(
-                                    handler, AndroidCameraProxyImpl.this, cb))
-                            .sendToTarget();
-                }
-            });
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraHandler.obtainMessage(CameraActions.SET_FACE_DETECTION_LISTENER,
+                                FaceDetectionCallbackForward
+                                        .getNewInstance(handler, AndroidCameraProxyImpl.this, cb))
+                                .sendToTarget();
+                    }
+                });
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Deprecated
@@ -926,15 +985,19 @@
                 return;
             }
             final String flattenedParameters = params.flatten();
-            mDispatchThread.runJob(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE |
-                            AndroidCameraStateHolder.CAMERA_UNLOCKED);
-                    mCameraHandler.obtainMessage(CameraActions.SET_PARAMETERS, flattenedParameters)
-                            .sendToTarget();
-                }
-            });
+            try {
+                mDispatchThread.runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraState.waitForStates(AndroidCameraStateHolder.CAMERA_IDLE |
+                                AndroidCameraStateHolder.CAMERA_UNLOCKED);
+                        mCameraHandler.obtainMessage(CameraActions.SET_PARAMETERS, flattenedParameters)
+                                .sendToTarget();
+                    }
+                });
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         @Deprecated
@@ -942,14 +1005,18 @@
         public Parameters getParameters() {
             final WaitDoneBundle bundle = new WaitDoneBundle();
             final Parameters[] parametersHolder = new Parameters[1];
-            mDispatchThread.runJobSync(new Runnable() {
-                @Override
-                public void run() {
-                    mCameraHandler.obtainMessage(
-                            CameraActions.GET_PARAMETERS, parametersHolder).sendToTarget();
-                    mCameraHandler.post(bundle.mUnlockRunnable);
-                }
-            }, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "get parameters");
+            try {
+                mDispatchThread.runJobSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCameraHandler.obtainMessage(
+                                CameraActions.GET_PARAMETERS, parametersHolder).sendToTarget();
+                        mCameraHandler.post(bundle.mUnlockRunnable);
+                    }
+                }, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "get parameters");
+            } catch (final RuntimeException ex) {
+                mCameraAgent.getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
             return parametersHolder[0];
         }
 
@@ -1059,49 +1126,6 @@
         }
     }
 
-    /**
-     * A helper class to forward ErrorCallback to another thread.
-     */
-    private static class ErrorCallbackForward implements Camera.ErrorCallback {
-        private final Handler mHandler;
-        private final CameraProxy mCamera;
-        private final CameraErrorCallback mCallback;
-
-        /**
-         * Returns a new instance of {@link AFCallbackForward}.
-         *
-         * @param handler The handler in which the callback will be invoked in.
-         * @param camera  The {@link CameraProxy} which the callback is from.
-         * @param cb      The callback to be invoked.
-         * @return        The instance of the {@link AFCallbackForward},
-         *                or null if any parameter is null.
-         */
-        public static ErrorCallbackForward getNewInstance(
-                Handler handler, CameraProxy camera, CameraErrorCallback cb) {
-            if (handler == null || camera == null || cb == null) {
-                return null;
-            }
-            return new ErrorCallbackForward(handler, camera, cb);
-        }
-
-        private ErrorCallbackForward(
-                Handler h, CameraProxy camera, CameraErrorCallback cb) {
-            mHandler = h;
-            mCamera = camera;
-            mCallback = cb;
-        }
-
-        @Override
-        public void onError(final int error, Camera camera) {
-            mHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    mCallback.onError(error, mCamera);
-                }
-            });
-        }
-    }
-
     /** A helper class to forward AutoFocusMoveCallback to another thread. */
     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private static class AFMoveCallbackForward implements AutoFocusMoveCallback {
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java
index 84b44e6..9892d4a 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/AndroidCameraCapabilities.java
@@ -47,7 +47,6 @@
         mPreferredPreviewSizeForVideo = new Size(p.getPreferredPreviewSizeForVideo());
         mSupportedPreviewFormats.addAll(p.getSupportedPreviewFormats());
         mSupportedPhotoFormats.addAll(p.getSupportedPictureFormats());
-        mMaxZoomRatio = p.getZoomRatios().get(p.getMaxZoom()) / ZOOM_MULTIPLIER;
         mHorizontalViewAngle = p.getHorizontalViewAngle();
         mVerticalViewAngle = p.getVerticalViewAngle();
         buildPreviewFpsRange(p);
@@ -60,6 +59,7 @@
         buildWhiteBalances(p);
 
         if (p.isZoomSupported()) {
+            mMaxZoomRatio = p.getZoomRatios().get(p.getMaxZoom()) / ZOOM_MULTIPLIER;
             mSupportedFeatures.add(Feature.ZOOM);
         }
         if (p.isVideoSnapshotSupported()) {
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java
index 407fbb1..63b1fec 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraActions.java
@@ -47,7 +47,6 @@
     public static final int SET_FACE_DETECTION_LISTENER = 461;
     public static final int START_FACE_DETECTION =        462;
     public static final int STOP_FACE_DETECTION =         463;
-    public static final int SET_ERROR_CALLBACK =          464;
     // Presentation
     public static final int ENABLE_SHUTTER_SOUND =    501;
     public static final int SET_DISPLAY_ORIENTATION = 502;
@@ -107,8 +106,6 @@
                 return "START_FACE_DETECTION";
             case STOP_FACE_DETECTION:
                 return "STOP_FACE_DETECTION";
-            case SET_ERROR_CALLBACK:
-                return "SET_ERROR_CALLBACK";
             case ENABLE_SHUTTER_SOUND:
                 return "ENABLE_SHUTTER_SOUND";
             case SET_DISPLAY_ORIENTATION:
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
index 17cbc02..66762fd 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraAgent.java
@@ -43,7 +43,7 @@
  * {@code android.hardware.Camera.OnZoomChangeListener}, and
  */
 public abstract class CameraAgent {
-    public static final long CAMERA_OPERATION_TIMEOUT_MS = 2500;
+    public static final long CAMERA_OPERATION_TIMEOUT_MS = 3500;
 
     private static final Log.Tag TAG = new Log.Tag("CamAgnt");
 
@@ -155,14 +155,6 @@
     }
 
     /**
-     * A handler for all camera api runtime exceptions.
-     * The default behavior is to throw the runtime exception.
-     */
-    public static interface CameraExceptionCallback {
-        public void onCameraException(RuntimeException e);
-    }
-
-    /**
      * An interface which wraps
      * {@link android.hardware.Camera.ErrorCallback}
      */
@@ -293,12 +285,17 @@
      */
     public void openCamera(final Handler handler, final int cameraId,
                            final CameraOpenCallback callback) {
-        getDispatchThread().runJob(new Runnable() {
-            @Override
-            public void run() {
-                getCameraHandler().obtainMessage(CameraActions.OPEN_CAMERA, cameraId, 0,
-                        CameraOpenCallbackForward.getNewInstance(handler, callback)).sendToTarget();
-            }});
+        try {
+            getDispatchThread().runJob(new Runnable() {
+                @Override
+                public void run() {
+                    getCameraHandler().obtainMessage(CameraActions.OPEN_CAMERA, cameraId, 0,
+                            CameraOpenCallbackForward.getNewInstance(handler, callback)).sendToTarget();
+                }
+            });
+        } catch (final RuntimeException ex) {
+            getCameraExceptionHandler().onDispatchThreadException(ex);
+        }
     }
 
     /**
@@ -308,22 +305,30 @@
      * @param synced Whether this call should be synchronous.
      */
     public void closeCamera(CameraProxy camera, boolean synced) {
-        if (synced) {
-            final WaitDoneBundle bundle = new WaitDoneBundle();
+        try {
+            if (synced) {
+                // Don't bother to wait since camera is in bad state.
+                if (getCameraState().isInvalid()) {
+                    return;
+                }
+                final WaitDoneBundle bundle = new WaitDoneBundle();
 
-            getDispatchThread().runJobSync(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget();
-                    getCameraHandler().post(bundle.mUnlockRunnable);
-                }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera release");
-        } else {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().removeCallbacksAndMessages(null);
-                    getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget();
-                }});
+                getDispatchThread().runJobSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget();
+                        getCameraHandler().post(bundle.mUnlockRunnable);
+                    }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera release");
+            } else {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().removeCallbacksAndMessages(null);
+                        getCameraHandler().obtainMessage(CameraActions.RELEASE).sendToTarget();
+                    }});
+            }
+        } catch (final RuntimeException ex) {
+            getCameraExceptionHandler().onDispatchThreadException(ex);
         }
     }
 
@@ -331,8 +336,7 @@
      * Sets a callback for handling camera api runtime exceptions on
      * a handler.
      */
-    public abstract void setCameraDefaultExceptionCallback(CameraExceptionCallback callback,
-            Handler handler);
+    public abstract void setCameraExceptionHandler(CameraExceptionHandler exceptionHandler);
 
     /**
      * Recycles the resources used by this instance. CameraAgent will be in
@@ -356,11 +360,21 @@
     protected abstract DispatchThread getDispatchThread();
 
     /**
+     * @return The state machine tracking the camera API's current status.
+     */
+    protected abstract CameraStateHolder getCameraState();
+
+    /**
+     * @return The exception handler.
+     */
+    protected abstract CameraExceptionHandler getCameraExceptionHandler();
+
+    /**
      * An interface that takes camera operation requests and post messages to the
      * camera handler thread. All camera operations made through this interface is
      * asynchronous by default except those mentioned specifically.
      */
-    public static abstract class CameraProxy {
+    public abstract static class CameraProxy {
 
         /**
          * Returns the underlying {@link android.hardware.Camera} object used
@@ -388,6 +402,11 @@
         public abstract CameraCapabilities getCapabilities();
 
         /**
+         * @return The camera agent which creates this proxy.
+         */
+        public abstract CameraAgent getAgent();
+
+        /**
          * Reconnects to the camera device. On success, the camera device will
          * be returned through {@link CameraAgent
          * .CameraOpenCallback#onCameraOpened(com.android.camera.cameradevice.CameraAgent
@@ -399,12 +418,16 @@
          * @param cb The callback when any error happens.
          */
         public void reconnect(final Handler handler, final CameraOpenCallback cb) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().obtainMessage(CameraActions.RECONNECT, getCameraId(), 0,
-                            CameraOpenCallbackForward.getNewInstance(handler, cb)).sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().obtainMessage(CameraActions.RECONNECT, getCameraId(), 0,
+                                CameraOpenCallbackForward.getNewInstance(handler, cb)).sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -413,13 +436,22 @@
          * @see android.hardware.Camera#unlock()
          */
         public void unlock() {
+            // Don't bother to wait since camera is in bad state.
+            if (getCameraState().isInvalid()) {
+                return;
+            }
             final WaitDoneBundle bundle = new WaitDoneBundle();
-            getDispatchThread().runJobSync(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().sendEmptyMessage(CameraActions.UNLOCK);
-                    getCameraHandler().post(bundle.mUnlockRunnable);
-                }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera unlock");
+            try {
+                getDispatchThread().runJobSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().sendEmptyMessage(CameraActions.UNLOCK);
+                        getCameraHandler().post(bundle.mUnlockRunnable);
+                    }
+                }, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "camera unlock");
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -427,11 +459,15 @@
          * @see android.hardware.Camera#lock()
          */
         public void lock() {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().sendEmptyMessage(CameraActions.LOCK);
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().sendEmptyMessage(CameraActions.LOCK);
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -456,13 +492,17 @@
         // the STOP_PREVIEW case in its handler; in the meantime, changing API 2
         // sizes would require closing and reopening the camera.
         public void setPreviewTexture(final SurfaceTexture surfaceTexture) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -480,15 +520,23 @@
          * @see CameraSettings#setPreviewSize
          */
         public void setPreviewTextureSync(final SurfaceTexture surfaceTexture) {
+            // Don't bother to wait since camera is in bad state.
+            if (getCameraState().isInvalid()) {
+                return;
+            }
             final WaitDoneBundle bundle = new WaitDoneBundle();
-            getDispatchThread().runJobSync(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture)
-                            .sendToTarget();
-                    getCameraHandler().post(bundle.mUnlockRunnable);
-                }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "set preview texture");
+            try {
+                getDispatchThread().runJobSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture)
+                                .sendToTarget();
+                        getCameraHandler().post(bundle.mUnlockRunnable);
+                    }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "set preview texture");
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -497,25 +545,33 @@
          * @param surfaceHolder The {@link SurfaceHolder} for preview.
          */
         public void setPreviewDisplay(final SurfaceHolder surfaceHolder) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
          * Starts the camera preview.
          */
         public void startPreview() {
+            try {
             getDispatchThread().runJob(new Runnable() {
                 @Override
                 public void run() {
                     getCameraHandler()
                             .obtainMessage(CameraActions.START_PREVIEW_ASYNC, null).sendToTarget();
                 }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -523,6 +579,7 @@
          * the preview starts.
          */
         public void startPreviewWithCallback(final Handler h, final CameraStartPreviewCallback cb) {
+            try {
             getDispatchThread().runJob(new Runnable() {
                 @Override
                 public void run() {
@@ -530,6 +587,9 @@
                             CameraStartPreviewCallbackForward.getNewInstance(h, cb))
                                     .sendToTarget();
                 }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -538,13 +598,21 @@
          * continues to release resources related to camera preview.
          */
         public void stopPreview() {
+            // Don't bother to wait since camera is in bad state.
+            if (getCameraState().isInvalid()) {
+                return;
+            }
             final WaitDoneBundle bundle = new WaitDoneBundle();
-            getDispatchThread().runJobSync(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().obtainMessage(CameraActions.STOP_PREVIEW, bundle)
-                            .sendToTarget();
-                }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "stop preview");
+            try {
+                getDispatchThread().runJobSync(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().obtainMessage(CameraActions.STOP_PREVIEW, bundle)
+                                .sendToTarget();
+                    }}, bundle.mWaitLock, CAMERA_OPERATION_TIMEOUT_MS, "stop preview");
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -583,14 +651,18 @@
          * @param callbackBuffer The buffer allocated for the preview data.
          */
         public void addCallbackBuffer(final byte[] callbackBuffer) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.ADD_CALLBACK_BUFFER, callbackBuffer)
-                            .sendToTarget();
-                }
-            });
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.ADD_CALLBACK_BUFFER, callbackBuffer)
+                                .sendToTarget();
+                        }
+                    });
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -667,24 +739,32 @@
          * @param capture Whether to adjust the JPEG capture orientation as well as the preview one.
          */
         public void setDisplayOrientation(final int degrees, final boolean capture) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.SET_DISPLAY_ORIENTATION, degrees,
-                                    capture ? 1 : 0)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.SET_DISPLAY_ORIENTATION, degrees,
+                                        capture ? 1 : 0)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         public void setJpegOrientation(final int degrees) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.SET_JPEG_ORIENTATION, degrees, 0)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.SET_JPEG_ORIENTATION, degrees, 0)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -707,34 +787,33 @@
          * Starts the face detection.
          */
         public void startFaceDetection() {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().sendEmptyMessage(CameraActions.START_FACE_DETECTION);
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().sendEmptyMessage(CameraActions.START_FACE_DETECTION);
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
          * Stops the face detection.
          */
         public void stopFaceDetection() {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().sendEmptyMessage(CameraActions.STOP_FACE_DETECTION);
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().sendEmptyMessage(CameraActions.STOP_FACE_DETECTION);
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
-         * Registers an error callback.
-         *
-         * @param handler  The handler on which the callback will be invoked.
-         * @param cb The error callback.
-         * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback)
-         */
-        public abstract void setErrorCallback(Handler handler, CameraErrorCallback cb);
-
-        /**
          * Sets the camera parameters.
          *
          * @param params The camera parameters to use.
@@ -780,13 +859,22 @@
             }
 
             final CameraSettings copyOfSettings = settings.copy();
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraState().waitForStates(statesToAwait);
-                    getCameraHandler().obtainMessage(CameraActions.APPLY_SETTINGS, copyOfSettings)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        CameraStateHolder cameraState = getCameraState();
+                        // Don't bother to wait since camera is in bad state.
+                        if (cameraState.isInvalid()) {
+                            return;
+                        }
+                        cameraState.waitForStates(statesToAwait);
+                        getCameraHandler().obtainMessage(CameraActions.APPLY_SETTINGS, copyOfSettings)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
             return true;
         }
 
@@ -806,11 +894,15 @@
          * settings regardless of the dirty bit.
          */
         public void refreshSettings() {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler().sendEmptyMessage(CameraActions.REFRESH_PARAMETERS);
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler().sendEmptyMessage(CameraActions.REFRESH_PARAMETERS);
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
@@ -820,13 +912,17 @@
          *                 {@code false} to disable it.
          */
         public void enableShutterSound(final boolean enable) {
-            getDispatchThread().runJob(new Runnable() {
-                @Override
-                public void run() {
-                    getCameraHandler()
-                            .obtainMessage(CameraActions.ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0)
-                            .sendToTarget();
-                }});
+            try {
+                getDispatchThread().runJob(new Runnable() {
+                    @Override
+                    public void run() {
+                        getCameraHandler()
+                                .obtainMessage(CameraActions.ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0)
+                                .sendToTarget();
+                    }});
+            } catch (final RuntimeException ex) {
+                getAgent().getCameraExceptionHandler().onDispatchThreadException(ex);
+            }
         }
 
         /**
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraExceptionHandler.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraExceptionHandler.java
new file mode 100644
index 0000000..dc71b4b
--- /dev/null
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraExceptionHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 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 com.android.ex.camera2.portability;
+
+import android.os.Handler;
+
+/**
+ * A handler for all camera api runtime exceptions.
+ * The default behavior is to throw the runtime exception.
+ */
+public class CameraExceptionHandler {
+    private Handler mHandler;
+
+    private CameraExceptionCallback mCallback =
+            new CameraExceptionCallback() {
+                @Override
+                public void onCameraError(int errorCode) {
+                }
+                @Override
+                public void onCameraException(
+                        RuntimeException e, String commandHistory, int action, int state) {
+                    throw e;
+                }
+                @Override
+                public void onDispatchThreadException(RuntimeException e) {
+                    throw e;
+                }
+            };
+
+    /**
+     * A callback helps to handle RuntimeException thrown by camera framework.
+     */
+    public static interface CameraExceptionCallback {
+        public void onCameraError(int errorCode);
+        public void onCameraException(
+                RuntimeException e, String commandHistory, int action, int state);
+        public void onDispatchThreadException(RuntimeException e);
+    }
+
+    /**
+     * Construct a new instance of {@link CameraExceptionHandler} with a custom callback which will
+     * be executed on a specific {@link Handler}.
+     *
+     * @param callback The callback which will be invoked.
+     * @param handler The handler in which the callback will be invoked in.
+     */
+    public CameraExceptionHandler(CameraExceptionCallback callback, Handler handler) {
+        mHandler = handler;
+        mCallback = callback;
+    }
+
+    /**
+     * Construct a new instance of {@link CameraExceptionHandler} with a default callback which will
+     * be executed on a specific {@link Handler}.
+     *
+     * @param handler The handler in which the default callback will be invoked in.
+     */
+    public CameraExceptionHandler(Handler handler) {
+        mHandler = handler;
+    }
+
+    /**
+     * Invoke @{link CameraExceptionCallback} when an error is reported by Android camera framework.
+     *
+     * @param errorCode An integer to represent the error code.
+     * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback)
+     */
+    public void onCameraError(final int errorCode) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mCallback.onCameraError(errorCode);
+            }
+        });
+    }
+
+    /**
+     * Invoke @{link CameraExceptionCallback} when a runtime exception is thrown by Android camera
+     * framework.
+     *
+     * @param ex The runtime exception object.
+     */
+    public void onCameraException(
+            final RuntimeException ex, final String commandHistory,
+            final int action, final int state) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mCallback.onCameraException(ex, commandHistory, action, state);
+            }
+        });
+    }
+
+    /**
+     * Invoke @{link CameraExceptionCallback} when a runtime exception is thrown by
+     * @{link DispatchThread}.
+     *
+     * @param ex The runtime exception object.
+     */
+    public void onDispatchThreadException(final RuntimeException ex) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mCallback.onDispatchThreadException(ex);
+            }
+        });
+    }
+}
+
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
index 87e9adf..ccd980a 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraSettings.java
@@ -58,7 +58,7 @@
     protected boolean mAutoWhiteBalanceLocked;
     protected boolean mRecordingHintEnabled;
     protected GpsData mGpsData;
-    protected Size mExifThumbnailSize = new Size(0,0);
+    protected Size mExifThumbnailSize;
 
     /**
      * An immutable class storing GPS related information.
@@ -491,20 +491,22 @@
     }
 
     /**
-     * Sets the size of the thumbnail in EXIF header.
+     * Sets the size of the thumbnail in EXIF header. To suppress thumbnail
+     * generation, set a size of (0,0).
      *
-     * @param s The size for the thumbnail. {@code null} will clear the size to
-     *          (0,0).
+     * @param s The size for the thumbnail. If {@code null}, agent will not
+     *          set a thumbnail size.
      */
     public void setExifThumbnailSize(Size s) {
-        if (s != null) {
-            mExifThumbnailSize = s;
-        } else {
-            mExifThumbnailSize = new Size(0,0);
-        }
+        mExifThumbnailSize = s;
     }
 
+    /**
+     * Gets the size of the thumbnail in EXIF header.
+     *
+     * @return desired thumbnail size, or null if no size was set
+     */
     public Size getExifThumbnailSize() {
-        return new Size(mExifThumbnailSize);
+        return (mExifThumbnailSize == null) ? null : new Size(mExifThumbnailSize);
     }
 }
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java b/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java
index c8d82b6..b758af2 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/CameraStateHolder.java
@@ -24,11 +24,23 @@
     private static final Log.Tag TAG = new Log.Tag("CamStateHolder");
 
     private int mState;
+    private boolean mInvalid;
 
+    /**
+     * Construct a new instance of @{link CameraStateHolder} with an initial state.
+     *
+     * @param state The initial state.
+     */
     public CameraStateHolder(int state) {
         setState(state);
+        mInvalid = false;
     }
 
+    /**
+     * Change to a new state.
+     *
+     * @param state The new state.
+     */
     public synchronized void setState(int state) {
         if (mState != state) {
             Log.v(TAG, "setState - state = " + Integer.toBinaryString(state));
@@ -37,10 +49,31 @@
         this.notifyAll();
     }
 
+    /**
+     * Obtain the current state.
+     *
+     * @return The current state.
+     */
     public synchronized int getState() {
         return mState;
     }
 
+    /**
+     * Change the state to be invalid. Once invalidated, the state will be invalid forever.
+     */
+    public synchronized void invalidate() {
+        mInvalid = true;
+    }
+
+    /**
+     * Whether the state is invalid.
+     *
+     * @return True if the state is invalid.
+     */
+    public synchronized boolean isInvalid() {
+        return mInvalid;
+    }
+
     private static interface ConditionChecker {
         /**
          * @return Whether the condition holds.
diff --git a/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java b/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java
index ec2a555..6b1c5ab 100644
--- a/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java
+++ b/camera2/portability/src/com/android/ex/camera2/portability/HistoryHandler.java
@@ -35,6 +35,10 @@
         mMsgHistory.offerLast(-1);
     }
 
+    Integer getCurrentMessage() {
+        return mMsgHistory.peekLast();
+    }
+
     String generateHistoryString(int cameraId) {
         String info = new String("HIST");
         info += "_ID" + cameraId;
diff --git a/common/java/com/android/common/widget/CompositeCursorAdapter.java b/common/java/com/android/common/widget/CompositeCursorAdapter.java
index dddbcf6..8a3fa9b 100644
--- a/common/java/com/android/common/widget/CompositeCursorAdapter.java
+++ b/common/java/com/android/common/widget/CompositeCursorAdapter.java
@@ -170,7 +170,12 @@
         mCount = 0;
         for (Partition partition : mPartitions) {
             Cursor cursor = partition.cursor;
-            int count = cursor != null ? cursor.getCount() : 0;
+            int count;
+            if (cursor == null || cursor.isClosed()) {
+                count = 0;
+            } else {
+                count = cursor.getCount();
+            }
             if (partition.hasHeader) {
                 if (count != 0 || partition.showIfEmpty) {
                     count++;
@@ -428,7 +433,9 @@
                     return null;
                 }
                 Cursor cursor = mPartition.cursor;
-                cursor.moveToPosition(offset);
+                if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
+                    return null;
+                }
                 return cursor;
             }
             start = end;