Implement CameraState in camera2

Add Camera2 implementation of the public camera state API. More specifically, compute and publish new camera states from within Camera2CameraImpl.

Bug: 150921286
Test: Camera2CameraImplStateTest
Change-Id: I2a88fa300dd8747dedba96a9e46a57f555b26494
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
new file mode 100644
index 0000000..49a77ea
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
@@ -0,0 +1,513 @@
+/*
+ * Copyright 2021 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 androidx.camera.camera2.internal
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat.CAMERA_UNAVAILABLE_DO_NOT_DISTURB
+import androidx.camera.camera2.internal.compat.CameraManagerCompat
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraState
+import androidx.camera.core.CameraState.ERROR_CAMERA_IN_USE
+import androidx.camera.core.CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED
+import androidx.camera.core.CameraState.create
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.CameraStateRegistry
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.MainThreadAsyncHandler
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.core.os.HandlerCompat
+import androidx.lifecycle.Observer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import java.util.concurrent.Executor
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class Camera2CameraImplStateTest {
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    private lateinit var cameraId: String
+    private lateinit var camera: Camera2CameraImpl
+    private lateinit var cameraStateRegistry: CameraStateRegistry
+
+    @Before
+    fun setCameraId() {
+        val nullableCameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)
+        assumeFalse("Device doesn't have an available back facing camera", nullableCameraId == null)
+        cameraId = nullableCameraId!!
+    }
+
+    @After
+    fun releaseCameraResources() {
+        if (::camera.isInitialized) {
+            camera.release().get()
+        }
+    }
+
+    @Test
+    fun shouldEmitClosedStateInitially() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = {},
+            expectedState = create(CameraState.Type.CLOSED)
+        )
+    }
+
+    @Test
+    fun shouldEmitPendingOpenState_whenOpeningClosedCamera_andCameraUnavailable() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        // Open fake camera
+        val fakeCamera = FakeCamera()
+        cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {})
+        cameraStateRegistry.tryOpenCamera(fakeCamera)
+        cameraStateRegistry.markCameraState(fakeCamera, CameraInternal.State.OPEN)
+
+        // Try to open camera. This should be prevented since fakeCamera is already open.
+        assertCameraStateAfterAction(
+            action = { camera.open() },
+            expectedState = create(CameraState.Type.PENDING_OPEN)
+        )
+    }
+
+    @Test
+    fun shouldEmitOpeningState_whenOpeningClosedCamera_andCameraAvailable() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = { camera.open() },
+            expectedState = create(CameraState.Type.OPENING)
+        )
+    }
+
+    @Test
+    fun shouldEmitOpenState_whenCameraOpened() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = {
+                camera.open()
+                camera.awaitCameraOpen()
+            },
+            expectedState = create(CameraState.Type.OPEN)
+        )
+    }
+
+    @Test
+    fun shouldEmitClosedState_afterOpeningCameraThrowsDNDException() {
+        val cameraManager = TestCameraManager(
+            onOpenCamera = {
+                throw CameraAccessExceptionCompat(CAMERA_UNAVAILABLE_DO_NOT_DISTURB)
+            }
+        )
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = { camera.open() },
+            expectedStatePredicate = { state ->
+                state.type == CameraState.Type.CLOSED &&
+                    state.error?.code == ERROR_DO_NOT_DISTURB_MODE_ENABLED
+            }
+        )
+    }
+
+    @Test
+    fun shouldEmitOpeningState_whenOpeningCameraThrowsSecurityException() {
+        val cameraManager = TestCameraManager(onOpenCamera = { throw SecurityException() })
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = { camera.open() },
+            expectedState = create(CameraState.Type.OPENING)
+        )
+    }
+
+    @Test
+    fun shouldEmitOpeningState_whenOpeningCameraEncountersRecoverableError() {
+        val cameraManager = TestCameraManager(onOpenCamera = { })
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = {
+                camera.open()
+                cameraManager.triggerRecoverableError()
+            },
+            expectedStatePredicate = { state ->
+                state.type == CameraState.Type.OPENING && state.error != null
+            }
+        )
+
+        // Clean up
+        camera.close()
+        cameraManager.triggerClose()
+    }
+
+    @Test
+    fun shouldEmitClosingState_whenOpeningCameraEncountersCriticalError() {
+        val cameraManager = TestCameraManager(onOpenCamera = {})
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = {
+                camera.open()
+                cameraManager.triggerCriticalError()
+            },
+            expectedStatePredicate = { state ->
+                state.type == CameraState.Type.CLOSING && state.error != null
+            }
+        )
+
+        // Clean up
+        camera.close()
+        cameraManager.triggerClose()
+    }
+
+    @Test
+    fun shouldEmitPendingOpenState_afterReachingMaxReopenAttempts() {
+        val semaphore = Semaphore(0)
+        val cameraManager = TestCameraManager(
+            onOpenCamera = {
+                semaphore.release()
+                throw SecurityException()
+            }
+        )
+        initializeCamera(cameraManager)
+
+        assertCameraStateAfterAction(
+            action = {
+                camera.open()
+                awaitMaxReopenAttemptsReached(semaphore)
+            },
+            expectedState = create(CameraState.Type.PENDING_OPEN)
+        )
+    }
+
+    @Test
+    fun shouldEmitOpeningState_whenOpenCameraEncountersRecoverableError() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        camera.open()
+        camera.awaitCameraOpen()
+
+        assertCameraStateAfterAction(
+            action = { cameraManager.triggerDisconnect() },
+            expectedState = create(
+                CameraState.Type.OPENING,
+                CameraState.StateError.create(ERROR_CAMERA_IN_USE)
+            )
+        )
+
+        // Clean up
+        camera.close()
+        cameraManager.triggerClose()
+    }
+
+    @Test
+    fun shouldEmitClosingState_whenOpenCameraEncountersCriticalError() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        camera.open()
+        camera.awaitCameraOpen()
+
+        assertCameraStateAfterAction(
+            action = { cameraManager.triggerCriticalError() },
+            expectedStatePredicate = { state ->
+                state.type == CameraState.Type.CLOSING && state.error != null
+            }
+        )
+
+        // Clean up
+        camera.close()
+        cameraManager.triggerClose()
+    }
+
+    @Test
+    fun shouldEmitClosingState_whenOpenCameraGetsCloseSignal() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        camera.open()
+        camera.awaitCameraOpen()
+
+        assertCameraStateAfterAction(
+            action = { camera.close() },
+            expectedState = create(CameraState.Type.CLOSING)
+        )
+    }
+
+    @Test
+    fun shouldEmitClosedState_whenCameraClosed() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        camera.open()
+        camera.awaitCameraOpen()
+
+        assertCameraStateAfterAction(
+            action = {
+                camera.close()
+                camera.awaitCameraClosed()
+            },
+            expectedState = create(CameraState.Type.CLOSED)
+        )
+    }
+
+    @Test
+    fun shouldEmitOpeningState_whenPendingOpenCameraReceivesOpenSignal() {
+        val cameraManager = TestCameraManager()
+        initializeCamera(cameraManager)
+
+        // Open fake camera
+        val fakeCamera = FakeCamera()
+        cameraStateRegistry.registerCamera(fakeCamera, CameraXExecutors.directExecutor(), {})
+        cameraStateRegistry.tryOpenCamera(fakeCamera)
+        cameraStateRegistry.markCameraState(fakeCamera, CameraInternal.State.OPEN)
+
+        // Try to open camera. This should be prevented since fakeCamera is already open.
+        assertCameraStateAfterAction(
+            action = { camera.open() },
+            expectedState = create(CameraState.Type.PENDING_OPEN)
+        )
+
+        // The camera should start opening when fakeCamera is closed
+        assertCameraStateAfterAction(
+            action = {
+                // Close fake camera
+                fakeCamera.close()
+                cameraStateRegistry.markCameraState(fakeCamera, CameraInternal.State.CLOSED)
+            },
+            expectedState = create(CameraState.Type.OPENING)
+        )
+    }
+
+    private fun initializeCamera(cameraManager: TestCameraManager) {
+        // Build camera manager wrapper
+        val cameraManagerCompat = CameraManagerCompat.from(cameraManager)
+
+        // Build camera info
+        val camera2CameraInfo = Camera2CameraInfoImpl(
+            cameraId,
+            cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
+        )
+
+        // Initialize camera state registry and only allow 1 open camera at most inside CameraX
+        cameraStateRegistry = CameraStateRegistry(1)
+
+        // Initialize camera instance
+        camera = Camera2CameraImpl(
+            cameraManagerCompat,
+            cameraId,
+            camera2CameraInfo,
+            cameraStateRegistry,
+            CameraXExecutors.directExecutor(),
+            cameraHandler
+        )
+    }
+
+    private fun Camera2CameraImpl.awaitCameraOpen() {
+        awaitInternalState(CameraInternal.State.OPEN)
+    }
+
+    private fun Camera2CameraImpl.awaitCameraClosed() {
+        awaitInternalState(CameraInternal.State.CLOSED)
+    }
+
+    private fun Camera2CameraImpl.awaitInternalState(state: CameraInternal.State) = runBlocking {
+        val receivedState = CompletableDeferred<Unit>()
+        val observer = object : Observable.Observer<CameraInternal.State> {
+            override fun onNewData(value: CameraInternal.State?) {
+                if (value == state) {
+                    receivedState.complete(Unit)
+                }
+            }
+
+            override fun onError(t: Throwable) {
+                // No-op
+            }
+        }
+        cameraState.addObserver(CameraXExecutors.directExecutor(), observer)
+
+        try {
+            withTimeout(CAMERA_OPEN_CLOSE_WAIT) { receivedState.await() }
+        } finally {
+            cameraState.removeObserver(observer)
+        }
+    }
+
+    private fun awaitMaxReopenAttemptsReached(semaphore: Semaphore) {
+        while (true) {
+            val cameraOpenAttempted =
+                semaphore.tryAcquire(CAMERA_REOPEN_WAIT, TimeUnit.MILLISECONDS)
+            if (!cameraOpenAttempted) {
+                return
+            }
+        }
+    }
+
+    private fun assertCameraStateAfterAction(action: () -> Unit, expectedState: CameraState) {
+        assertCameraStateAfterAction(action, { state -> state == expectedState })
+    }
+
+    private fun assertCameraStateAfterAction(
+        action: () -> Unit,
+        expectedStatePredicate: ((CameraState) -> Boolean)
+    ) = runBlocking {
+        val nextStateReceived = CompletableDeferred<Unit>()
+        val stateObserver = Observer<CameraState> { state ->
+            if (expectedStatePredicate.invoke(state)) {
+                nextStateReceived.complete(Unit)
+            }
+        }
+
+        withContext(Dispatchers.Main) {
+            camera.cameraInfo.cameraState.observeForever(stateObserver)
+        }
+
+        action.invoke()
+
+        try {
+            withTimeout(CAMERA_STATE_WAIT) { nextStateReceived.await() }
+        } finally {
+            withContext(Dispatchers.Main) {
+                camera.cameraInfo.cameraState.removeObserver(stateObserver)
+            }
+        }
+    }
+
+    class TestCameraManager(private val onOpenCamera: (() -> Unit)? = null) :
+        CameraManagerCompat.CameraManagerCompatImpl {
+
+        private val forwardCameraManager = CameraManagerCompat.CameraManagerCompatImpl.from(
+            ApplicationProvider.getApplicationContext(),
+            MainThreadAsyncHandler.getInstance()
+        )
+        private var stateCallback: CameraDevice.StateCallback? = null
+
+        override fun getCameraIdList(): Array<String> {
+            return forwardCameraManager.cameraIdList
+        }
+
+        override fun registerAvailabilityCallback(
+            executor: Executor,
+            callback: CameraManager.AvailabilityCallback
+        ) {
+            // No-op
+        }
+
+        override fun unregisterAvailabilityCallback(callback: CameraManager.AvailabilityCallback) {
+            // No-op
+        }
+
+        override fun getCameraCharacteristics(cameraId: String): CameraCharacteristics {
+            return forwardCameraManager.getCameraCharacteristics(cameraId)
+        }
+
+        override fun openCamera(
+            cameraId: String,
+            executor: Executor,
+            callback: CameraDevice.StateCallback
+        ) {
+            stateCallback = callback
+            if (onOpenCamera == null) {
+                forwardCameraManager.openCamera(cameraId, executor, callback)
+            } else {
+                onOpenCamera.invoke()
+            }
+        }
+
+        override fun getCameraManager(): CameraManager {
+            return forwardCameraManager.cameraManager
+        }
+
+        fun triggerRecoverableError() {
+            stateCallback?.onError(
+                Mockito.mock(CameraDevice::class.java),
+                CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE
+            )
+        }
+
+        fun triggerCriticalError() {
+            stateCallback?.onError(
+                Mockito.mock(CameraDevice::class.java),
+                CameraDevice.StateCallback.ERROR_CAMERA_DISABLED
+            )
+        }
+
+        fun triggerDisconnect() {
+            stateCallback?.onDisconnected(Mockito.mock(CameraDevice::class.java))
+        }
+
+        fun triggerClose() {
+            stateCallback?.onClosed(Mockito.mock(CameraDevice::class.java))
+        }
+    }
+
+    companion object {
+        private const val CAMERA_OPEN_CLOSE_WAIT = 5000.toLong() // 5 seconds
+        private const val CAMERA_REOPEN_WAIT = 3000.toLong() // 3 seconds
+        private const val CAMERA_STATE_WAIT = 1000.toLong() // 1 second
+
+        private lateinit var cameraHandlerThread: HandlerThread
+        private lateinit var cameraHandler: Handler
+
+        @JvmStatic
+        @BeforeClass
+        fun classSetup() {
+            cameraHandlerThread = HandlerThread("CameraThread")
+            cameraHandlerThread.start()
+            cameraHandler = HandlerCompat.createAsync(cameraHandlerThread.looper)
+        }
+
+        @JvmStatic
+        @AfterClass
+        fun classTeardown() {
+            cameraHandlerThread.quitSafely()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index a6a4244..feba09a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraState;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
@@ -953,7 +954,8 @@
                 case CameraAccessExceptionCompat.CAMERA_UNAVAILABLE_DO_NOT_DISTURB:
                     // Camera2 is unable to call the onError() callback for this case. It has to
                     // reset the state here.
-                    setState(InternalState.INITIALIZED);
+                    setState(InternalState.INITIALIZED, CameraState.StateError.create(
+                            CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED, e));
                     break;
                 default:
                     // Camera2 will call the onError() callback with the specific error code that
@@ -1021,11 +1023,7 @@
             @Override
             @ExecutedBy("mExecutor")
             public void onFailure(Throwable t) {
-                if (t instanceof CameraAccessException) {
-                    debugLog("Unable to configure camera due to " + t.getMessage());
-                } else if (t instanceof CancellationException) {
-                    debugLog("Unable to configure camera cancelled");
-                } else if (t instanceof DeferrableSurface.SurfaceClosedException) {
+                if (t instanceof DeferrableSurface.SurfaceClosedException) {
                     SessionConfig sessionConfig =
                             findSessionConfigForSurface(
                                     ((DeferrableSurface.SurfaceClosedException) t)
@@ -1033,13 +1031,31 @@
                     if (sessionConfig != null) {
                         postSurfaceClosedError(sessionConfig);
                     }
+                    return;
+                }
+
+                // A CancellationException is thrown when (1) A CaptureSession is closed while it
+                // is opening. In this case, another CaptureSession should be opened shortly
+                // after or (2) When opening a CaptureSession fails.
+                // TODO(b/183504720): Distinguish between both scenarios, and communicate the
+                //  second one to the developer.
+                if (t instanceof CancellationException) {
+                    debugLog("Unable to configure camera cancelled");
+                    return;
+                }
+
+                // Only report camera config error if the camera is open. Ignore otherwise.
+                if (mState == InternalState.OPENED) {
+                    setState(InternalState.OPENED,
+                            CameraState.StateError.create(CameraState.ERROR_STREAM_CONFIG, t));
+                }
+
+                if (t instanceof CameraAccessException) {
+                    debugLog("Unable to configure camera due to " + t.getMessage());
                 } else if (t instanceof TimeoutException) {
                     // TODO: Consider to handle the timeout error.
                     Logger.e(TAG, "Unable to configure camera " + mCameraInfoInternal.getCameraId()
                             + ", timeout!");
-                } else {
-                    // Throw the unexpected error.
-                    throw new RuntimeException(t);
                 }
             }
         }, mExecutor);
@@ -1270,7 +1286,13 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
     void setState(@NonNull InternalState state) {
-        setState(state, /*notifyImmediately=*/true);
+        setState(state, /*stateError=*/null);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @ExecutedBy("mExecutor")
+    void setState(@NonNull InternalState state, @Nullable CameraState.StateError stateError) {
+        setState(state, stateError, /*notifyImmediately=*/true);
     }
 
     /**
@@ -1283,7 +1305,8 @@
      */
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
-    void setState(@NonNull InternalState state, boolean notifyImmediately) {
+    void setState(@NonNull InternalState state, @Nullable CameraState.StateError stateError,
+            boolean notifyImmediately) {
         debugLog("Transitioning camera internal state: " + mState + " --> " + state);
         mState = state;
         // Convert the internal state to the publicly visible state
@@ -1316,6 +1339,7 @@
         }
         mCameraStateRegistry.markCameraState(this, publicState, notifyImmediately);
         mObservableState.postValue(publicState);
+        mCameraStateMachine.updateState(publicState, stateError);
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -1470,7 +1494,7 @@
                     // this will wait for the next available camera.
                     Logger.d(TAG, String.format("Attempt to reopen camera[%s] after error[%s]",
                             cameraDevice.getId(), getErrorMessage(error)));
-                    reopenCameraAfterError();
+                    reopenCameraAfterError(error);
                     break;
                 default:
                     // TODO: Properly handle other errors. For now, we will close the camera.
@@ -1481,14 +1505,20 @@
                                     + ": "
                                     + getErrorMessage(error)
                                     + " closing camera.");
-                    setState(InternalState.CLOSING);
+
+                    int publicErrorCode =
+                            error == CameraDevice.StateCallback.ERROR_CAMERA_DISABLED
+                                    ? CameraState.ERROR_CAMERA_DISABLED
+                                    : CameraState.ERROR_CAMERA_FATAL_ERROR;
+                    setState(InternalState.CLOSING, CameraState.StateError.create(publicErrorCode));
+
                     closeCamera(/*abortInFlightCaptures=*/false);
                     break;
             }
         }
 
         @ExecutedBy("mExecutor")
-        private void reopenCameraAfterError() {
+        private void reopenCameraAfterError(int error) {
             // After an error, we must close the current camera device before we can open a new
             // one. To accomplish this, we will close the current camera and wait for the
             // onClosed() callback to reopen the device. It is also possible that the device can
@@ -1496,7 +1526,21 @@
             Preconditions.checkState(mCameraDeviceError != ERROR_NONE,
                     "Can only reopen camera device after error if the camera device is actually "
                             + "in an error state.");
-            setState(InternalState.REOPENING);
+
+            int publicErrorCode;
+            switch (error) {
+                case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
+                    publicErrorCode = CameraState.ERROR_CAMERA_IN_USE;
+                    break;
+                case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
+                    publicErrorCode = CameraState.ERROR_MAX_CAMERAS_IN_USE;
+                    break;
+                default:
+                    publicErrorCode = CameraState.ERROR_OTHER_RECOVERABLE_ERROR;
+                    break;
+            }
+            setState(InternalState.REOPENING, CameraState.StateError.create(publicErrorCode));
+
             closeCamera(/*abortInFlightCaptures=*/false);
         }
 
@@ -1521,7 +1565,9 @@
                 // Set the state to PENDING_OPEN, so that an attempt to reopen the camera is made if
                 // it later becomes available to open, but ignore immediate reopen attempt from
                 // CameraStateRegistry.OnOpenAvailableListener.
-                setState(InternalState.PENDING_OPEN, /*notifyImmediately=*/false);
+                setState(InternalState.PENDING_OPEN,
+                        /*stateError=*/null,
+                        /*notifyImmediately=*/false);
             }
         }