Refactor Camera2CameraController to enable restart delays

This CL does a local refactoring of Camera2CameraController to make it
possible to restart (recover) after a delay.

Bug: 372258646
Bug: 344752133
Test: CameraPipe multi-resume
Change-Id: I2f6d5fbe561e04a8566c034518abef71828ce885
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 927e969..fc7821a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 /**
@@ -86,6 +87,8 @@
 
     @GuardedBy("lock") private var lastCameraError: CameraError? = null
 
+    @GuardedBy("lock") private var restartJob: Job? = null
+
     private var currentCamera: VirtualCamera? = null
     private var currentSession: CaptureSessionState? = null
     private var currentSurfaceMap: Map<StreamId, Surface>? = null
@@ -119,120 +122,149 @@
             }
     }
 
-    override fun start(): Unit =
+    override fun start() {
+        synchronized(lock) { startLocked() }
+    }
+
+    override fun stop() {
+        synchronized(lock) { stopLocked() }
+    }
+
+    private fun restart(delayMs: Long) {
         synchronized(lock) {
-            if (controllerState == ControllerState.CLOSED) {
-                Log.info { "Ignoring start(): Camera2CameraController is already closed" }
-                return
-            } else if (controllerState == ControllerState.STARTED) {
-                Log.warn { "Ignoring start(): Camera2CameraController is already started" }
-                return
-            }
-            lastCameraError = null
-            val camera =
-                virtualCameraManager.open(
-                    graphConfig.camera,
-                    graphConfig.sharedCameraIds,
-                    graphListener,
-                ) { _ ->
-                    isForeground
-                }
-            if (camera == null) {
-                Log.error {
-                    "Failed to start Camera2CameraController: Open request submission failed"
-                }
-                return
-            }
-
-            check(currentCamera == null)
-            check(currentSession == null)
-
-            currentCamera = camera
-            val session =
-                CaptureSessionState(
-                    graphListener,
-                    captureSessionFactory,
-                    captureSequenceProcessorFactory,
-                    cameraSurfaceManager,
-                    timeSource,
-                    graphConfig.flags,
-                    scope
-                )
-            currentSession = session
-
-            val surfaces: Map<StreamId, Surface>? = currentSurfaceMap
-            if (surfaces != null) {
-                session.configureSurfaceMap(surfaces)
-            }
-
-            controllerState = ControllerState.STARTED
-            Log.debug { "Started Camera2CameraController" }
-            currentCameraStateJob?.cancel()
-            currentCameraStateJob = scope.launch { bindSessionToCamera() }
-        }
-
-    override fun stop(): Unit =
-        synchronized(lock) {
-            if (controllerState == ControllerState.CLOSED) {
-                Log.warn { "Ignoring stop(): Camera2CameraController is already closed" }
-                return
-            } else if (
-                controllerState == ControllerState.STOPPING ||
-                    controllerState == ControllerState.STOPPED
-            ) {
-                Log.warn { "Ignoring stop(): CameraController already stopping or stopped" }
-                return
-            }
-
-            val camera = currentCamera
-            val session = currentSession
-
-            currentCamera = null
-            currentSession = null
-
-            controllerState = ControllerState.STOPPING
-            Log.debug { "Stopping Camera2CameraController" }
-            disconnectSessionAndCamera(session, camera)
-        }
-
-    private fun onCameraStatusChanged(cameraStatus: CameraStatus): Unit =
-        synchronized(lock) {
-            Log.debug { "$this ($cameraId) camera status changed to $cameraStatus" }
-            if (
-                cameraStatus is CameraStatus.CameraAvailable ||
-                    cameraStatus is CameraStatus.CameraUnavailable
-            ) {
-                [email protected] = cameraStatus
-            }
-
-            var shouldRestart = false
-            when (controllerState) {
-                ControllerState.DISCONNECTED ->
-                    if (
-                        cameraStatus is CameraStatus.CameraAvailable ||
-                            cameraStatus is CameraStatus.CameraPrioritiesChanged
-                    ) {
-                        shouldRestart = true
+            restartJob?.cancel()
+            restartJob =
+                scope.launch {
+                    delay(delayMs)
+                    synchronized(lock) {
+                        if (
+                            controllerState != ControllerState.CLOSED &&
+                                controllerState != ControllerState.STOPPING &&
+                                controllerState != ControllerState.STOPPED
+                        ) {
+                            controllerState
+                            stopLocked()
+                            startLocked()
+                        }
                     }
-                ControllerState.ERROR ->
-                    if (
-                        cameraStatus is CameraStatus.CameraAvailable &&
-                            lastCameraError != CameraError.ERROR_GRAPH_CONFIG
-                    ) {
-                        shouldRestart = true
-                    }
-            }
-            if (!shouldRestart) {
-                Log.debug {
-                    "Camera status changed but not restarting: " +
-                        "Controller state = $controllerState, camera status = $cameraStatus."
                 }
-                return
-            }
-            Log.debug { "Restarting Camera2CameraController" }
-            stop()
-            start()
         }
+    }
+
+    @GuardedBy("lock")
+    private fun startLocked() {
+        if (controllerState == ControllerState.CLOSED) {
+            Log.info { "Ignoring start(): Camera2CameraController is already closed" }
+            return
+        } else if (controllerState == ControllerState.STARTED) {
+            Log.warn { "Ignoring start(): Camera2CameraController is already started" }
+            return
+        }
+        lastCameraError = null
+        val camera =
+            virtualCameraManager.open(
+                graphConfig.camera,
+                graphConfig.sharedCameraIds,
+                graphListener,
+            ) { _ ->
+                isForeground
+            }
+        if (camera == null) {
+            Log.error { "Failed to start Camera2CameraController: Open request submission failed" }
+            return
+        }
+
+        check(currentCamera == null)
+        check(currentSession == null)
+
+        currentCamera = camera
+        val session =
+            CaptureSessionState(
+                graphListener,
+                captureSessionFactory,
+                captureSequenceProcessorFactory,
+                cameraSurfaceManager,
+                timeSource,
+                graphConfig.flags,
+                scope
+            )
+        currentSession = session
+
+        val surfaces: Map<StreamId, Surface>? = currentSurfaceMap
+        if (surfaces != null) {
+            session.configureSurfaceMap(surfaces)
+        }
+
+        controllerState = ControllerState.STARTED
+        Log.debug { "Started Camera2CameraController" }
+        currentCameraStateJob?.cancel()
+        currentCameraStateJob = scope.launch { bindSessionToCamera() }
+    }
+
+    @GuardedBy("lock")
+    private fun stopLocked() {
+        if (controllerState == ControllerState.CLOSED) {
+            Log.warn { "Ignoring stop(): Camera2CameraController is already closed" }
+            return
+        } else if (
+            controllerState == ControllerState.STOPPING ||
+                controllerState == ControllerState.STOPPED
+        ) {
+            Log.warn { "Ignoring stop(): CameraController already stopping or stopped" }
+            return
+        }
+
+        val camera = currentCamera
+        val session = currentSession
+
+        currentCamera = null
+        currentSession = null
+
+        controllerState = ControllerState.STOPPING
+        Log.debug { "Stopping Camera2CameraController" }
+        disconnectSessionAndCamera(session, camera)
+    }
+
+    private fun onCameraStatusChanged(cameraStatus: CameraStatus) {
+        val shouldRestart =
+            synchronized(lock) {
+                Log.debug { "$this ($cameraId) camera status changed to $cameraStatus" }
+                if (
+                    cameraStatus is CameraStatus.CameraAvailable ||
+                        cameraStatus is CameraStatus.CameraUnavailable
+                ) {
+                    [email protected] = cameraStatus
+                }
+
+                var shouldRestart = false
+                when (controllerState) {
+                    ControllerState.DISCONNECTED ->
+                        if (
+                            cameraStatus is CameraStatus.CameraAvailable ||
+                                cameraStatus is CameraStatus.CameraPrioritiesChanged
+                        ) {
+                            shouldRestart = true
+                        }
+                    ControllerState.ERROR ->
+                        if (
+                            cameraStatus is CameraStatus.CameraAvailable &&
+                                lastCameraError != CameraError.ERROR_GRAPH_CONFIG
+                        ) {
+                            shouldRestart = true
+                        }
+                }
+                shouldRestart
+            }
+        if (!shouldRestart) {
+            Log.debug {
+                "Camera status changed but not restarting: " +
+                    "Controller state = $controllerState, camera status = $cameraStatus."
+            }
+            return
+        }
+        Log.debug { "Restarting Camera2CameraController" }
+        restart(0L)
+    }
 
     override fun close(): Unit =
         synchronized(lock) {