Merge "Adapt GraphState to CameraState" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index 8f93913..657c698 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -28,6 +28,7 @@
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
@@ -64,13 +65,14 @@
         cameraPipe,
     ),
 ) : UseCaseCamera {
-    val useCaseCameraGraphConfig = UseCaseCameraConfig(useCases).provideUseCaseGraphConfig(
-        callbackMap = callbackMap,
-        cameraConfig = cameraConfig,
-        cameraPipe = cameraPipe,
-        requestListener = ComboRequestListener(),
-        useCaseSurfaceManager = useCaseSurfaceManager,
-    )
+    val useCaseCameraGraphConfig =
+        UseCaseCameraConfig(useCases, CameraStateAdapter()).provideUseCaseGraphConfig(
+            callbackMap = callbackMap,
+            cameraConfig = cameraConfig,
+            cameraPipe = cameraPipe,
+            requestListener = ComboRequestListener(),
+            useCaseSurfaceManager = useCaseSurfaceManager,
+        )
 
     override val requestControl: UseCaseCameraRequestControl = UseCaseCameraRequestControlImpl(
         configAdapter = CaptureConfigAdapter(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index c3b6ccd..91b9fe9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -67,7 +67,7 @@
 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalCamera2Interop::class)
 class CameraControlAdapter @Inject constructor(
     private val cameraProperties: CameraProperties,
-    private val cameraStateAdapter: CameraStateAdapter,
+    private val cameraControlStateAdapter: CameraControlStateAdapter,
     private val evCompControl: EvCompControl,
     private val flashControl: FlashControl,
     private val torchControl: TorchControl,
@@ -131,7 +131,7 @@
                     zoomControl.minZoom,
                     zoomControl.maxZoom
                 )
-                cameraStateAdapter.setZoomState(zoomValue)
+                cameraControlStateAdapter.setZoomState(zoomValue)
             }
         }.asListenableFuture()
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
new file mode 100644
index 0000000..6e3e98c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlStateAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 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.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.EvCompControl
+import androidx.camera.camera2.pipe.integration.impl.TorchControl
+import androidx.camera.camera2.pipe.integration.impl.ZoomControl
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * [CameraControlStateAdapter] caches and updates based on callbacks from the active CameraGraph.
+ */
+@SuppressLint("UnsafeOptInUsageError")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@CameraScope
+class CameraControlStateAdapter @Inject constructor(
+    private val zoomControl: ZoomControl,
+    private val evCompControl: EvCompControl,
+    private val torchControl: TorchControl,
+) {
+    val torchStateLiveData: LiveData<Int>
+        get() = torchControl.torchStateLiveData
+
+    private val _zoomState by lazy {
+        MutableLiveData<ZoomState>(
+            ZoomValue(
+                zoomControl.zoomRatio,
+                zoomControl.minZoom,
+                zoomControl.maxZoom
+            )
+        )
+    }
+    val zoomStateLiveData: LiveData<ZoomState>
+        get() = _zoomState
+
+    suspend fun setZoomState(value: ZoomState) {
+        withContext(Dispatchers.Main) {
+            _zoomState.value = value
+        }
+    }
+
+    val exposureState: ExposureState
+        get() = evCompControl.exposureState
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 3f8b947..2f49b0ac 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -43,7 +43,6 @@
 import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -59,7 +58,8 @@
 class CameraInfoAdapter @Inject constructor(
     private val cameraProperties: CameraProperties,
     private val cameraConfig: CameraConfig,
-    private val cameraState: CameraStateAdapter,
+    private val cameraStateAdapter: CameraStateAdapter,
+    private val cameraControlStateAdapter: CameraControlStateAdapter,
     private val cameraCallbackMap: CameraCallbackMap,
 ) : CameraInfoInternal {
     private lateinit var camcorderProfileProviderAdapter: CamcorderProfileProviderAdapter
@@ -94,16 +94,13 @@
         )
     }
 
-    override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomStateLiveData
-    override fun getTorchState(): LiveData<Int> = cameraState.torchStateLiveData
+    override fun getZoomState(): LiveData<ZoomState> = cameraControlStateAdapter.zoomStateLiveData
+    override fun getTorchState(): LiveData<Int> = cameraControlStateAdapter.torchStateLiveData
 
     @SuppressLint("UnsafeOptInUsageError")
-    override fun getExposureState(): ExposureState = cameraState.exposureState
+    override fun getExposureState(): ExposureState = cameraControlStateAdapter.exposureState
 
-    override fun getCameraState(): LiveData<CameraState> {
-        Log.warn { "TODO: CameraState is not yet supported." }
-        return MutableLiveData(CameraState.create(CameraState.Type.CLOSED))
-    }
+    override fun getCameraState(): LiveData<CameraState> = cameraStateAdapter.cameraState
 
     override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
         cameraCallbackMap.addCaptureCallback(callback, executor)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index b4bcd62..c929ca0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -30,7 +30,6 @@
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.LiveDataObservable
 import androidx.camera.core.impl.Observable
 import com.google.common.util.concurrent.ListenableFuture
 import javax.inject.Inject
@@ -49,16 +48,14 @@
     private val cameraInfo: CameraInfoInternal,
     private val cameraController: CameraControlInternal,
     private val threads: UseCaseThreads,
+    private val cameraStateAdapter: CameraStateAdapter
 ) : CameraInternal {
     private val cameraId = config.cameraId
     private var coreCameraConfig: androidx.camera.core.impl.CameraConfig =
         CameraConfigs.emptyConfig()
     private val debugId = cameraAdapterIds.incrementAndGet()
-    private val cameraState = LiveDataObservable<CameraInternal.State>()
 
     init {
-        cameraState.postValue(CameraInternal.State.CLOSED)
-
         debug { "Created $this for $cameraId" }
         // TODO: Consider preloading the list of camera ids and metadata.
     }
@@ -78,7 +75,9 @@
     }
 
     override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
-    override fun getCameraState(): Observable<CameraInternal.State> = cameraState
+    override fun getCameraState(): Observable<CameraInternal.State> =
+        cameraStateAdapter.cameraInternalState
+
     override fun getCameraControlInternal(): CameraControlInternal = cameraController
 
     // UseCase attach / detach behaviors.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
index e4cc398..28de2a6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2022 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.
@@ -16,51 +16,128 @@
 
 package androidx.camera.camera2.pipe.integration.adapter
 
-import android.annotation.SuppressLint
+import android.os.Looper
+import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
+import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.integration.config.CameraScope
-import androidx.camera.camera2.pipe.integration.impl.EvCompControl
-import androidx.camera.camera2.pipe.integration.impl.TorchControl
-import androidx.camera.camera2.pipe.integration.impl.ZoomControl
-import androidx.camera.core.ExposureState
-import androidx.camera.core.ZoomState
-import androidx.lifecycle.LiveData
+import androidx.camera.core.CameraState
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.LiveDataObservable
 import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
 import javax.inject.Inject
 
-/**
- * [CameraStateAdapter] caches and updates based on callbacks from the active CameraGraph.
- */
-@SuppressLint("UnsafeOptInUsageError")
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraScope
-class CameraStateAdapter @Inject constructor(
-    private val zoomControl: ZoomControl,
-    private val evCompControl: EvCompControl,
-    private val torchControl: TorchControl,
-) {
-    val torchStateLiveData: LiveData<Int>
-        get() = torchControl.torchStateLiveData
+@RequiresApi(21)
+class CameraStateAdapter @Inject constructor() {
+    private val lock = Any()
 
-    private val _zoomState by lazy {
-        MutableLiveData<ZoomState>(
-            ZoomValue(
-                zoomControl.zoomRatio,
-                zoomControl.minZoom,
-                zoomControl.maxZoom
-            )
-        )
+    internal val cameraInternalState = LiveDataObservable<CameraInternal.State>()
+    internal val cameraState = MutableLiveData<CameraState>()
+
+    @GuardedBy("lock")
+    private var currentGraph: CameraGraph? = null
+
+    @GuardedBy("lock")
+    private var currentGraphState: GraphState = GraphStateStopped
+
+    init {
+        postCameraState(CameraInternal.State.CLOSED)
     }
-    val zoomStateLiveData: LiveData<ZoomState>
-        get() = _zoomState
-    suspend fun setZoomState(value: ZoomState) {
-        withContext(Dispatchers.Main) {
-            _zoomState.value = value
+
+    public fun onGraphUpdated(cameraGraph: CameraGraph) = synchronized(lock) {
+        Log.debug { "Camera graph updated from $currentGraph to $cameraGraph" }
+        if (currentGraphState != GraphStateStopped) {
+            postCameraState(CameraInternal.State.CLOSING)
+            postCameraState(CameraInternal.State.CLOSED)
+        }
+        currentGraph = cameraGraph
+        currentGraphState = GraphStateStopped
+    }
+
+    public fun onGraphStateUpdated(cameraGraph: CameraGraph, graphState: GraphState) =
+        synchronized(lock) {
+            Log.debug { "$cameraGraph state updated to $graphState" }
+            handleStateTransition(cameraGraph, graphState)
+        }
+
+    @GuardedBy("lock")
+    private fun handleStateTransition(cameraGraph: CameraGraph, graphState: GraphState) {
+        // If the transition came from a different camera graph, consider it stale and ignore it.
+        if (cameraGraph != currentGraph) {
+            Log.debug { "Ignored stale transition $graphState for $cameraGraph" }
+            return
+        }
+
+        if (!isTransitionPermissible(currentGraphState, graphState)) {
+            Log.warn { "Impermissible state transition from $currentGraphState to $graphState" }
+            return
+        }
+        currentGraphState = graphState
+
+        // Now that the current graph state is updated, post the latest states.
+        Log.debug { "Updated current graph state to $currentGraphState" }
+        postCameraState(currentGraphState.toCameraInternalState())
+    }
+
+    private fun postCameraState(internalState: CameraInternal.State) {
+        cameraInternalState.postValue(internalState)
+        cameraState.setOrPostValue(CameraState.create(internalState.toCameraState()))
+    }
+
+    private fun isTransitionPermissible(oldState: GraphState, newState: GraphState): Boolean {
+        return when (oldState) {
+            GraphStateStarting ->
+                newState == GraphStateStarted ||
+                    newState == GraphStateStopping ||
+                    newState == GraphStateStopped
+
+            GraphStateStarted ->
+                newState == GraphStateStopping ||
+                    newState == GraphStateStopped
+
+            GraphStateStopping ->
+                newState == GraphStateStopped ||
+                    newState == GraphStateStarting
+
+            GraphStateStopped ->
+                newState == GraphStateStarting ||
+                    newState == GraphStateStarted
+
+            else -> false
         }
     }
+}
 
-    val exposureState: ExposureState
-        get() = evCompControl.exposureState
+@RequiresApi(21)
+internal fun GraphState.toCameraInternalState(): CameraInternal.State = when (this) {
+    GraphStateStarting -> CameraInternal.State.OPENING
+    GraphStateStarted -> CameraInternal.State.OPEN
+    GraphStateStopping -> CameraInternal.State.CLOSING
+    GraphStateStopped -> CameraInternal.State.CLOSED
+    else -> throw IllegalArgumentException("Unexpected graph state: $this")
+}
+
+@RequiresApi(21)
+internal fun CameraInternal.State.toCameraState(): CameraState.Type = when (this) {
+    CameraInternal.State.CLOSED -> CameraState.Type.CLOSED
+    CameraInternal.State.OPENING -> CameraState.Type.OPENING
+    CameraInternal.State.OPEN -> CameraState.Type.OPEN
+    CameraInternal.State.CLOSING -> CameraState.Type.CLOSING
+    CameraInternal.State.PENDING_OPEN -> CameraState.Type.PENDING_OPEN
+    else -> throw IllegalArgumentException("Unexpected CameraInternal state: $this")
+}
+
+internal fun MutableLiveData<CameraState>.setOrPostValue(cameraState: CameraState) {
+    if (Looper.myLooper() == Looper.getMainLooper()) {
+        this.value = cameraState
+    } else {
+        this.postValue(cameraState)
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
index bfef920..f529114 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.StreamFormat
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter.Companion.toCamera2ImplConfig
 import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
@@ -46,11 +47,13 @@
 annotation class UseCaseCameraScope
 
 /** Dependency bindings for building a [UseCaseCamera] */
-@Module(includes = [
-    CapturePipelineImpl.Bindings::class,
-    UseCaseCameraImpl.Bindings::class,
-    UseCaseCameraRequestControlImpl.Bindings::class,
-])
+@Module(
+    includes = [
+        CapturePipelineImpl.Bindings::class,
+        UseCaseCameraImpl.Bindings::class,
+        UseCaseCameraRequestControlImpl.Bindings::class,
+    ]
+)
 abstract class UseCaseCameraModule {
     // Used for dagger provider methods that are static.
     companion object
@@ -59,7 +62,8 @@
 /** Dagger module for binding the [UseCase]'s to the [UseCaseCamera]. */
 @Module
 class UseCaseCameraConfig(
-    private val useCases: List<UseCase>
+    private val useCases: List<UseCase>,
+    private val cameraStateAdapter: CameraStateAdapter,
 ) {
     @UseCaseCameraScope
     @Provides
@@ -105,11 +109,13 @@
         }
 
         // Build up a config (using TEMPLATE_PREVIEW by default)
-        val graph = cameraPipe.create(CameraGraph.Config(
-            camera = cameraConfig.cameraId,
-            streams = streamConfigMap.keys.toList(),
-            defaultListeners = listOf(callbackMap, requestListener),
-        ))
+        val graph = cameraPipe.create(
+            CameraGraph.Config(
+                camera = cameraConfig.cameraId,
+                streams = streamConfigMap.keys.toList(),
+                defaultListeners = listOf(callbackMap, requestListener),
+            )
+        )
 
         val surfaceToStreamMap = mutableMapOf<DeferrableSurface, StreamId>()
         streamConfigMap.forEach { (streamConfig, deferrableSurface) ->
@@ -138,6 +144,7 @@
         return UseCaseGraphConfig(
             graph = graph,
             surfaceToStreamMap = surfaceToStreamMap,
+            cameraStateAdapter = cameraStateAdapter,
         )
     }
 }
@@ -145,6 +152,7 @@
 data class UseCaseGraphConfig(
     val graph: CameraGraph,
     val surfaceToStreamMap: Map<DeferrableSurface, StreamId>,
+    val cameraStateAdapter: CameraStateAdapter,
 )
 
 /** Dagger subcomponent for a single [UseCaseCamera] instance. */
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 561805c..9ab3c3c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -85,6 +85,7 @@
     override val requestControl: UseCaseCameraRequestControl,
 ) : UseCaseCamera {
     private val debugId = useCaseCameraIds.incrementAndGet()
+    private val graphStateJob: Job
 
     override var runningUseCases = setOf<UseCase>()
         set(value) {
@@ -105,9 +106,20 @@
 
     init {
         debug { "Configured $this for $useCases" }
+        useCaseGraphConfig.apply {
+            cameraStateAdapter.onGraphUpdated(graph)
+        }
+        graphStateJob = threads.scope.launch {
+            useCaseGraphConfig.apply {
+                graph.graphState.collect {
+                    cameraStateAdapter.onGraphStateUpdated(graph, it)
+                }
+            }
+        }
     }
 
     override fun close(): Job {
+        graphStateJob.cancel()
         return threads.scope.launch {
             debug { "Closing $this" }
             useCaseGraphConfig.graph.close()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 537adf9..7021369 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
@@ -66,8 +67,9 @@
     @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Java version required for Dagger
     private val controls: java.util.Set<UseCaseCameraControl>,
     private val camera2CameraControl: Camera2CameraControl,
+    private val cameraStateAdapter: CameraStateAdapter,
     cameraProperties: CameraProperties,
-    displayInfoManager: DisplayInfoManager
+    displayInfoManager: DisplayInfoManager,
 ) {
     private val lock = Any()
 
@@ -242,7 +244,7 @@
         }
 
         // Create and configure the new camera component.
-        _activeComponent = builder.config(UseCaseCameraConfig(useCases)).build()
+        _activeComponent = builder.config(UseCaseCameraConfig(useCases, cameraStateAdapter)).build()
         for (control in allControls) {
             control.useCaseCamera = camera
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapterTest.kt
new file mode 100644
index 0000000..7056538
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapterTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 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.pipe.integration.adapter
+
+import android.os.Build
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
+import androidx.camera.core.CameraState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+internal class CameraStateAdapterTest {
+    private val cameraStateAdapter = CameraStateAdapter()
+    private val cameraGraph1 = FakeCameraGraph()
+    private val cameraGraph2 = FakeCameraGraph()
+
+    @Test
+    fun testNormalStateTransitions() {
+        cameraStateAdapter.onGraphUpdated(cameraGraph1)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarting)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPENING)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarted)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPEN)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStopped)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+    }
+
+    @Test
+    fun testStaleStateTransitions() {
+        cameraStateAdapter.onGraphUpdated(cameraGraph1)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarting)
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarted)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPEN)
+
+        // Simulate that a new camera graph is created.
+        cameraStateAdapter.onGraphUpdated(cameraGraph2)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph2, GraphStateStarting)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPENING)
+
+        // This came from cameraGraph1 and thereby making the transition stale.
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStopped)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPENING)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph2, GraphStateStarted)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPEN)
+    }
+
+    @Test
+    fun testImpermissibleStateTransitions() {
+        cameraStateAdapter.onGraphUpdated(cameraGraph1)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+
+        // Impermissible state transition from GraphStateStopped to GraphStateStopping
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStopping)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.CLOSED)
+
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarting)
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarted)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPEN)
+
+        // Impermissible state transition from GraphStateStarted to GraphStateStarting
+        cameraStateAdapter.onGraphStateUpdated(cameraGraph1, GraphStateStarting)
+        assertThat(cameraStateAdapter.cameraState.value?.type).isEqualTo(CameraState.Type.OPEN)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
index 252f425..34c2fa3 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
@@ -33,6 +33,7 @@
 import androidx.camera.core.impl.TagBundle
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -43,7 +44,6 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import java.util.concurrent.Executors
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -66,6 +66,7 @@
         useCaseGraphConfig = UseCaseGraphConfig(
             graph = FakeCameraGraph(),
             surfaceToStreamMap = mapOf(surface to StreamId(0)),
+            cameraStateAdapter = CameraStateAdapter(),
         ),
         cameraProperties = fakeCameraProperties,
         threads = fakeUseCaseThreads,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index 2b64455..f21cfe8 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -31,6 +31,7 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -47,6 +48,11 @@
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.impl.utils.futures.Futures
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -63,11 +69,6 @@
 import org.mockito.Mockito.mock
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.Executors
-import java.util.concurrent.ScheduledFuture
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
@@ -199,6 +200,7 @@
             useCaseGraphConfig = UseCaseGraphConfig(
                 graph = FakeCameraGraph(fakeCameraGraphSession = fakeCameraGraphSession),
                 surfaceToStreamMap = emptyMap(),
+                cameraStateAdapter = CameraStateAdapter(),
             ),
         )
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
index f8902eb..0d3dc40 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
@@ -24,6 +24,7 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestMetadata
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -39,6 +40,8 @@
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.TagBundle
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -48,8 +51,6 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -72,6 +73,7 @@
     private val fakeUseCaseGraphConfig = UseCaseGraphConfig(
         graph = fakeCameraGraph,
         surfaceToStreamMap = surfaceToStreamMap,
+        cameraStateAdapter = CameraStateAdapter(),
     )
     private val fakeConfigAdapter = CaptureConfigAdapter(
         useCaseGraphConfig = fakeUseCaseGraphConfig,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
index 6126329..00097a2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
@@ -20,6 +20,7 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -68,6 +69,7 @@
     private val fakeUseCaseGraphConfig = UseCaseGraphConfig(
         graph = fakeCameraGraph,
         surfaceToStreamMap = surfaceToStreamMap,
+        cameraStateAdapter = CameraStateAdapter(),
     )
     private val fakeConfigAdapter = CaptureConfigAdapter(
         useCaseGraphConfig = fakeUseCaseGraphConfig,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 13ef7aa..a98af2c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
@@ -185,6 +186,7 @@
             useCaseThreads,
             ComboRequestListener()
         ),
-        displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+        cameraStateAdapter = CameraStateAdapter(),
+        displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext()),
     )
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index b99fa69..0aafb22 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -19,6 +19,7 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
@@ -198,7 +199,8 @@
         return CameraInfoAdapter(
             cameraProperties,
             CameraConfig(cameraId),
-            CameraStateAdapter(
+            CameraStateAdapter(),
+            CameraControlStateAdapter(
                 ZoomControl(FakeZoomCompat()),
                 EvCompControl(FakeEvCompCompat()),
                 TorchControl(cameraProperties, useCaseThreads),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index c4c65e5..3b3ad4c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
@@ -35,7 +36,7 @@
 import kotlinx.coroutines.Job
 
 class FakeUseCaseCameraComponentBuilder : UseCaseCameraComponent.Builder {
-    private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList())
+    private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList(), CameraStateAdapter())
 
     override fun config(config: UseCaseCameraConfig): UseCaseCameraComponent.Builder {
         this.config = config