Merge "Handle children UseCase state changes." into androidx-main
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index f2aa6189..c44bea5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -523,7 +523,7 @@
 
     @VisibleForTesting
     @NonNull
-    DeferrableSurface getDeferrableSurfaceForTesting() {
+    public DeferrableSurface getDeferrableSurfaceForTesting() {
         return mSettableSurface;
     }
 
@@ -533,6 +533,14 @@
     }
 
     /**
+     * @return true if this edge is connected to a Surface provider.
+     */
+    @VisibleForTesting
+    public boolean hasProvider() {
+        return mSettableSurface.hasProvider();
+    }
+
+    /**
      * A {@link DeferrableSurface} that sets another {@link DeferrableSurface} as the source.
      *
      * <p>This class provides mechanisms to link an {@link DeferrableSurface}, and propagates
@@ -566,6 +574,11 @@
             return mProvider == null && !isClosed();
         }
 
+        @VisibleForTesting
+        boolean hasProvider() {
+            return mProvider != null;
+        }
+
         /**
          * Sets the {@link DeferrableSurface} that provides the surface.
          *
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index 0339e04..a08de59 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -19,13 +19,19 @@
 import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
 import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
 
 import android.os.Build;
 import android.util.Size;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Preview;
 import androidx.camera.core.UseCase;
@@ -34,6 +40,7 @@
 import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.MutableConfig;
 import androidx.camera.core.impl.Observable;
 import androidx.camera.core.impl.SessionConfig;
@@ -67,6 +74,9 @@
     // Specs for children UseCase, calculated and set by StreamSharing.
     @NonNull
     final Map<UseCase, SurfaceEdge> mChildrenEdges = new HashMap<>();
+    // Whether a children is in the active state. See: UseCase.State.ACTIVE
+    @NonNull
+    final Map<UseCase, Boolean> mChildrenActiveState = new HashMap<>();
     // Config factory for getting children's config.
     @NonNull
     private final UseCaseConfigFactory mUseCaseConfigFactory;
@@ -89,6 +99,10 @@
         mParentCamera = parentCamera;
         mUseCaseConfigFactory = useCaseConfigFactory;
         mChildren = children;
+        // Set children state to inactive by default.
+        for (UseCase child : children) {
+            mChildrenActiveState.put(child, false);
+        }
     }
 
     // --- API for StreamSharing ---
@@ -185,21 +199,66 @@
     }
 
     // --- Handle children state change ---
-    // TODO(b/264936250): Handle children state changes.
+    @MainThread
     @Override
     public void onUseCaseActive(@NonNull UseCase useCase) {
+        checkMainThread();
+        if (isUseCaseActive(useCase)) {
+            return;
+        }
+        mChildrenActiveState.put(useCase, true);
+        DeferrableSurface childSurface = getChildRepeatingSurface(useCase);
+        if (childSurface != null) {
+            forceSetProvider(getUseCaseEdge(useCase), childSurface);
+        }
     }
 
+    @MainThread
     @Override
     public void onUseCaseInactive(@NonNull UseCase useCase) {
+        checkMainThread();
+        if (!isUseCaseActive(useCase)) {
+            return;
+        }
+        mChildrenActiveState.put(useCase, false);
+        getUseCaseEdge(useCase).disconnect();
     }
 
+    @MainThread
     @Override
     public void onUseCaseUpdated(@NonNull UseCase useCase) {
+        checkMainThread();
+        if (!isUseCaseActive(useCase)) {
+            // No-op if the child is inactive. It will connect when it becomes active.
+            return;
+        }
+        SurfaceEdge edge = getUseCaseEdge(useCase);
+        DeferrableSurface childSurface = getChildRepeatingSurface(useCase);
+        if (childSurface != null) {
+            // If the child has a Surface, connect. VideoCapture uses this mechanism to
+            // resume/start recording.
+            forceSetProvider(edge, childSurface);
+        } else {
+            // If the child has no Surface, disconnect. VideoCapture uses this mechanism to
+            // pause/stop recording.
+            edge.disconnect();
+        }
     }
 
+    @MainThread
     @Override
     public void onUseCaseReset(@NonNull UseCase useCase) {
+        checkMainThread();
+        SurfaceEdge edge = getUseCaseEdge(useCase);
+        edge.invalidate();
+        if (!isUseCaseActive(useCase)) {
+            // No-op if the child is inactive. It will connect when it becomes active.
+            return;
+        }
+        DeferrableSurface childSurface = getChildRepeatingSurface(useCase);
+        if (childSurface != null) {
+            forceSetProvider(edge, childSurface);
+        }
     }
 
     // --- Forward parent camera properties and events ---
@@ -225,6 +284,40 @@
 
     // --- private methods ---
     @NonNull
+    private SurfaceEdge getUseCaseEdge(@NonNull UseCase useCase) {
+        return requireNonNull(mChildrenEdges.get(useCase));
+    }
+
+    private boolean isUseCaseActive(@NonNull UseCase useCase) {
+        return requireNonNull(mChildrenActiveState.get(useCase));
+    }
+
+    private void forceSetProvider(@NonNull SurfaceEdge edge,
+            @NonNull DeferrableSurface surface) {
+        edge.invalidate();
+        try {
+            edge.setProvider(surface);
+        } catch (DeferrableSurface.SurfaceClosedException e) {
+            // Throws an exception when DeferrableSurface is closed, which should never happen.
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Gets the {@link DeferrableSurface} associated with the child.
+     */
+    @Nullable
+    private static DeferrableSurface getChildRepeatingSurface(@NonNull UseCase child) {
+        // TODO(b/267620162): use non-repeating surface for ImageCapture.
+        List<DeferrableSurface> surfaces =
+                child.getSessionConfig().getRepeatingCaptureConfig().getSurfaces();
+        checkState(surfaces.size() <= 1);
+        if (surfaces.size() == 1) {
+            return surfaces.get(0);
+        }
+        return null;
+    }
+
     CameraCaptureCallback createCameraCaptureCallback() {
         return new CameraCaptureCallback() {
             @Override
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
index dd0a624..456858f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
@@ -16,15 +16,19 @@
 
 package androidx.camera.core.streamsharing
 
+import android.graphics.ImageFormat
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.os.Build
 import android.util.Size
 import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionConfig.defaultEmptySessionConfig
 import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.processing.SurfaceEdge
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeDeferrableSurface
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
 import com.google.common.truth.Truth.assertThat
@@ -44,12 +48,22 @@
 class VirtualCameraTest {
 
     companion object {
+        private const val CLOSED = true
+        private const val OPEN = false
+        private const val HAS_PROVIDER = true
+        private const val NO_PROVIDER = false
         private val INPUT_SIZE = Size(800, 600)
+        private val SESSION_CONFIG_WITH_SURFACE = SessionConfig.Builder()
+            .addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE)).build()
     }
 
     private val parentCamera = FakeCamera()
     private val child1 = FakeUseCase()
     private val child2 = FakeUseCase()
+    private val childrenEdges = mapOf(
+        Pair(child1 as UseCase, createSurfaceEdge()),
+        Pair(child2 as UseCase, createSurfaceEdge())
+    )
     private val useCaseConfigFactory = FakeUseCaseConfigFactory()
     private lateinit var virtualCamera: VirtualCamera
 
@@ -59,6 +73,67 @@
     }
 
     @Test
+    fun setUseCaseActiveAndInactive_surfaceConnectsAndDisconnects() {
+        // Arrange.
+        virtualCamera.bindChildren()
+        virtualCamera.setChildrenEdges(childrenEdges)
+        child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+        // Assert: edge open by default.
+        verifyEdge(child1, OPEN, NO_PROVIDER)
+        // Set UseCase to active, verify it has provider.
+        child1.notifyActiveForTesting()
+        verifyEdge(child1, OPEN, HAS_PROVIDER)
+        // Set UseCase to inactive, verify it's closed.
+        child1.notifyInactiveForTesting()
+        verifyEdge(child1, CLOSED, HAS_PROVIDER)
+        // Set UseCase to active, verify it becomes open again.
+        child1.notifyActiveForTesting()
+        verifyEdge(child1, OPEN, HAS_PROVIDER)
+    }
+
+    @Test
+    fun resetUseCase_edgeInvalidated() {
+        // Arrange: setup and get the old DeferrableSurface.
+        virtualCamera.bindChildren()
+        virtualCamera.setChildrenEdges(childrenEdges)
+        child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+        child1.notifyActiveForTesting()
+        val oldSurface = childrenEdges[child1]!!.deferrableSurfaceForTesting
+        // Act: notify reset.
+        child1.notifyResetForTesting()
+        // Assert: DeferrableSurface is recreated. The old one is closed.
+        assertThat(oldSurface.isClosed).isTrue()
+        assertThat(childrenEdges[child1]!!.deferrableSurfaceForTesting)
+            .isNotSameInstanceAs(oldSurface)
+        verifyEdge(child1, OPEN, HAS_PROVIDER)
+    }
+
+    @Test
+    fun updateUseCaseWithAndWithoutSurface_surfaceConnectsAndDisconnects() {
+        // Arrange
+        virtualCamera.bindChildren()
+        virtualCamera.setChildrenEdges(childrenEdges)
+        child1.notifyActiveForTesting()
+        verifyEdge(child1, OPEN, NO_PROVIDER)
+
+        // Act: set Surface and update
+        child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+        child1.notifyUpdatedForTesting()
+        // Assert: edge is connected.
+        verifyEdge(child1, OPEN, HAS_PROVIDER)
+        // Act: remove Surface and update.
+        child1.updateSessionConfigForTesting(defaultEmptySessionConfig())
+        child1.notifyUpdatedForTesting()
+        // Assert: edge is disconnected.
+        verifyEdge(child1, CLOSED, HAS_PROVIDER)
+        // Act: set Surface and update.
+        child1.updateSessionConfigForTesting(SESSION_CONFIG_WITH_SURFACE)
+        child1.notifyUpdatedForTesting()
+        // Assert: edge is connected again.
+        verifyEdge(child1, OPEN, HAS_PROVIDER)
+    }
+
+    @Test
     fun virtualCameraInheritsParentProperties() {
         assertThat(virtualCamera.cameraState).isEqualTo(parentCamera.cameraState)
         assertThat(virtualCamera.cameraInfo).isEqualTo(parentCamera.cameraInfo)
@@ -67,12 +142,8 @@
 
     @Test
     fun updateChildrenSpec_updateAndNotifyChildren() {
-        // Arrange: create children spec map.
-        val map = mutableMapOf<UseCase, SurfaceEdge>()
-        map[child1] = createSurfaceEdge()
-        map[child2] = createSurfaceEdge()
         // Act: update children with the map.
-        virtualCamera.setChildrenEdges(map)
+        virtualCamera.setChildrenEdges(childrenEdges)
         // Assert: surface size propagated to children
         assertThat(child1.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
         assertThat(child2.attachedStreamSpec!!.resolution).isEqualTo(INPUT_SIZE)
@@ -83,4 +154,9 @@
             PREVIEW, StreamSpec.builder(INPUT_SIZE).build(), Matrix(), true, Rect(), 0, false
         )
     }
+
+    private fun verifyEdge(child: UseCase, isClosed: Boolean, hasProvider: Boolean) {
+        assertThat(childrenEdges[child]!!.deferrableSurfaceForTesting.isClosed).isEqualTo(isClosed)
+        assertThat(childrenEdges[child]!!.hasProvider()).isEqualTo(hasProvider)
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
index 618ef08..2335fb2 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
@@ -180,4 +180,39 @@
     public void setSessionConfigSupplier(@NonNull Supplier<SessionConfig> sessionConfigSupplier) {
         mSessionConfigSupplier = sessionConfigSupplier;
     }
+
+    /**
+     * Calls the protected method {@link UseCase#updateSessionConfig}.
+     */
+    public void updateSessionConfigForTesting(@NonNull SessionConfig sessionConfig) {
+        updateSessionConfig(sessionConfig);
+    }
+
+    /**
+     * Calls the protected method {@link UseCase#notifyActive()}.
+     */
+    public void notifyActiveForTesting() {
+        notifyActive();
+    }
+
+    /**
+     * Calls the protected method {@link UseCase#notifyInactive()}.
+     */
+    public void notifyInactiveForTesting() {
+        notifyInactive();
+    }
+
+    /**
+     * Calls the protected method {@link UseCase#notifyUpdated()}.
+     */
+    public void notifyUpdatedForTesting() {
+        notifyUpdated();
+    }
+
+    /**
+     * Calls the protected method {@link UseCase#notifyReset()}.
+     */
+    public void notifyResetForTesting() {
+        notifyReset();
+    }
 }