Add a CameraGraph.Flag that disables graph level Surface tracking

Add a CameraGraph.Flag that allow users to disable graph level Surface
tracking on legacy hardware level devices that need Surfaces to be
relinquished and refreshed when camera is closed/disconnected.

Bug: 344749041
Test: Tested most tests on some legacy devices in the CameraX lab.
Change-Id: I23053aa5e1c6a8a505575ac709d627d5698a8ca5
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 3ac0b9a..ff2a1a5 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
@@ -51,10 +51,13 @@
 import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCameraDeviceOnCameraGraphCloseQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnDisconnectQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnVideoQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.ConfigureSurfaceToSecondarySessionFailQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
 import androidx.camera.camera2.pipe.integration.compat.quirk.DisableAbortCapturesOnStopWithSessionProcessorQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.FinalizeSessionOnCloseQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.PreviewOrientationIncorrectQuirk
 import androidx.camera.camera2.pipe.integration.compat.quirk.QuickSuccessiveImageCaptureFailsRepeatingRequestQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.TextureViewIsClosedQuirk
 import androidx.camera.camera2.pipe.integration.compat.workaround.TemplateParamsOverride
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.CameraScope
@@ -1117,6 +1120,13 @@
                     0u
                 }
 
+            val shouldDisableGraphLevelSurfaceTracking =
+                cameraQuirks.quirks.let {
+                    it.contains(ConfigureSurfaceToSecondarySessionFailQuirk::class.java) ||
+                        it.contains(PreviewOrientationIncorrectQuirk::class.java) ||
+                        it.contains(TextureViewIsClosedQuirk::class.java)
+                }
+
             return CameraGraph.Flags(
                 abortCapturesOnStop = shouldAbortCapturesOnStop,
                 awaitRepeatingRequestBeforeCapture =
@@ -1131,6 +1141,7 @@
                 closeCaptureSessionOnDisconnect = shouldCloseCaptureSessionOnDisconnect,
                 closeCameraDeviceOnClose = shouldCloseCameraDeviceOnClose,
                 finalizeSessionOnCloseBehavior = shouldFinalizeSessionOnCloseBehavior,
+                disableGraphLevelSurfaceTracking = shouldDisableGraphLevelSurfaceTracking,
             )
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index f5da2b9..fee1665f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -383,6 +383,16 @@
          * - API levels: All
          */
         val closeCameraDeviceOnClose: Boolean = false,
+
+        /**
+         * Flag to disable CameraGraph level Surface usage tracking. On legacy hardware levels, we
+         * need to explicitly relinquish current Surface usages on camera closure (or disconnection)
+         * such that CameraX can refresh the Surfaces used in the CameraGraph.
+         * - Bug(s): b/344749041
+         * - Device(s): LEGACY camera hardware level
+         * - API levels: 23 or LEGACY hardware level.
+         */
+        val disableGraphLevelSurfaceTracking: Boolean = false
     ) {
 
         @JvmInline
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
index 06ee0f1..4f8f0fa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
@@ -135,13 +135,15 @@
             streamGraphImpl: StreamGraphImpl,
             cameraController: CameraController,
             cameraSurfaceManager: CameraSurfaceManager,
-            imageSourceMap: ImageSourceMap
+            imageSourceMap: ImageSourceMap,
+            graphConfig: CameraGraph.Config,
         ): SurfaceGraph {
             return SurfaceGraph(
                 streamGraphImpl,
                 cameraController,
                 cameraSurfaceManager,
-                imageSourceMap.imageSources
+                imageSourceMap.imageSources,
+                graphConfig.flags,
             )
         }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
index 76885a4..0992ece 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/SurfaceGraph.kt
@@ -35,7 +35,8 @@
     private val streamGraphImpl: StreamGraphImpl,
     private val cameraController: CameraController,
     private val surfaceManager: CameraSurfaceManager,
-    private val imageSources: Map<StreamId, ImageSource>
+    private val imageSources: Map<StreamId, ImageSource>,
+    private val cameraGraphFlags: CameraGraph.Flags,
 ) {
     private val lock = Any()
 
@@ -66,8 +67,17 @@
                         "Removed surface for $streamId"
                     }
                 }
-                var oldSurfaceToken: AutoCloseable? = null
 
+                if (cameraGraphFlags.disableGraphLevelSurfaceTracking) {
+                    if (surface == null) {
+                        surfaceMap.remove(streamId)
+                    } else {
+                        surfaceMap[streamId] = surface
+                    }
+                    return@synchronized null
+                }
+
+                var oldSurfaceToken: AutoCloseable? = null
                 if (surface == null) {
                     // TODO: Tell the graph processor that it should resubmit the repeating request
                     // or reconfigure the camera2 captureSession
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 5d975cc..7397387 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -118,8 +118,15 @@
     private val imageSourceMap = ImageSourceMap(graphConfig, streamGraph, imageSources)
     private val frameDistributor =
         FrameDistributor(imageSourceMap.imageSources, frameCaptureQueue) {}
+    private val cameraGraphFlags = CameraGraph.Flags()
     private val surfaceGraph =
-        SurfaceGraph(streamGraph, cameraController, cameraSurfaceManager, emptyMap())
+        SurfaceGraph(
+            streamGraph,
+            cameraController,
+            cameraSurfaceManager,
+            emptyMap(),
+            cameraGraphFlags,
+        )
     private val audioRestriction = FakeAudioRestrictionController()
     private val cameraGraph =
         CameraGraphImpl(
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
index 4f7c6c4..a9a6c94 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
@@ -19,6 +19,7 @@
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraGraphId
 import androidx.camera.camera2.pipe.CameraSurfaceManager
 import androidx.camera.camera2.pipe.testing.FakeCameraController
@@ -50,8 +51,15 @@
     private val fakeSurfaceListener: CameraSurfaceManager.SurfaceListener = mock()
     private val cameraSurfaceManager =
         CameraSurfaceManager().also { it.addListener(fakeSurfaceListener) }
+    private val cameraGraphFlags = CameraGraph.Flags()
     private val surfaceGraph =
-        SurfaceGraph(streamMap, fakeCameraController, cameraSurfaceManager, emptyMap())
+        SurfaceGraph(
+            streamMap,
+            fakeCameraController,
+            cameraSurfaceManager,
+            emptyMap(),
+            cameraGraphFlags,
+        )
 
     private val stream1 = streamMap[config.streamConfig1]!!
     private val stream2 = streamMap[config.streamConfig2]!!
@@ -144,6 +152,45 @@
     }
 
     @Test
+    fun outputSurfacesArePassedToListenerWhenAvailableWithGraphTrackingOff() {
+        val surfaceGraph2 =
+            SurfaceGraph(
+                streamMap,
+                fakeCameraController,
+                cameraSurfaceManager,
+                emptyMap(),
+                cameraGraphFlags.copy(disableGraphLevelSurfaceTracking = true),
+            )
+
+        assertThat(fakeCameraController.surfaceMap).isNull()
+
+        surfaceGraph2[stream1.id] = fakeSurface1
+        surfaceGraph2[stream2.id] = fakeSurface2
+        surfaceGraph2[stream3.id] = fakeSurface3
+        surfaceGraph2[stream4.id] = fakeSurface4
+        surfaceGraph2[stream5.id] = fakeSurface5
+        surfaceGraph2[stream6.id] = fakeSurface6
+        surfaceGraph2[stream7.id] = fakeSurface7
+        surfaceGraph2[stream8.id] = fakeSurface8
+        assertThat(fakeCameraController.surfaceMap).isNull()
+
+        surfaceGraph2[stream9.id] = fakeSurface9
+        surfaceGraph2[stream10.id] = fakeSurface10
+
+        assertThat(fakeCameraController.surfaceMap).isNotNull()
+        assertThat(fakeCameraController.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1)
+        assertThat(fakeCameraController.surfaceMap?.get(stream2.id)).isEqualTo(fakeSurface2)
+        assertThat(fakeCameraController.surfaceMap?.get(stream3.id)).isEqualTo(fakeSurface3)
+        assertThat(fakeCameraController.surfaceMap?.get(stream4.id)).isEqualTo(fakeSurface4)
+        assertThat(fakeCameraController.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
+        assertThat(fakeCameraController.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
+        assertThat(fakeCameraController.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
+        assertThat(fakeCameraController.surfaceMap?.get(stream8.id)).isEqualTo(fakeSurface8)
+        assertThat(fakeCameraController.surfaceMap?.get(stream9.id)).isEqualTo(fakeSurface9)
+        assertThat(fakeCameraController.surfaceMap?.get(stream10.id)).isEqualTo(fakeSurface10)
+    }
+
+    @Test
     fun onlyMostRecentSurfacesArePassedToSession() {
         val fakeSurface1A = Surface(SurfaceTexture(7))
         val fakeSurface1B = Surface(SurfaceTexture(8))
@@ -191,6 +238,26 @@
     }
 
     @Test
+    fun newSurfacesDoesNotAcquireTokensWithGraphTrackingOff() {
+        val surfaceGraph2 =
+            SurfaceGraph(
+                streamMap,
+                fakeCameraController,
+                cameraSurfaceManager,
+                emptyMap(),
+                cameraGraphFlags.copy(disableGraphLevelSurfaceTracking = true),
+            )
+        surfaceGraph2[stream1.id] = fakeSurface1
+
+        verify(fakeSurfaceListener, never()).onSurfaceActive(eq(fakeSurface1))
+        verify(fakeSurfaceListener, never()).onSurfaceActive(eq(fakeSurface2))
+        verify(fakeSurfaceListener, never()).onSurfaceActive(eq(fakeSurface3))
+        verify(fakeSurfaceListener, never()).onSurfaceInactive(eq(fakeSurface1))
+        verify(fakeSurfaceListener, never()).onSurfaceInactive(eq(fakeSurface2))
+        verify(fakeSurfaceListener, never()).onSurfaceInactive(eq(fakeSurface3))
+    }
+
+    @Test
     fun replacingSurfacesReleasesPreviousToken() {
         surfaceGraph[stream1.id] = fakeSurface1
         surfaceGraph[stream1.id] = fakeSurface2
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
index 34f4db5..f9ff936 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
@@ -58,7 +58,6 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.junit.After
-import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Rule
@@ -244,11 +243,6 @@
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
     fun canRecovered_afterReceivingCameraOnDisconnectedEvent() {
-        // TODO(b/344749041) The tests can run failed on API 27 devices in camera-pipe config
-        assumeFalse(
-            Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 &&
-                implName == CameraPipeConfig::class.simpleName
-        )
         // Launch CameraX activity
         cameraXActivityScenario = launchCameraXActivity(cameraId)
         with(cameraXActivityScenario) {