Handle children UseCase state changes.
Updated VirtualCamera to listen to children UseCase state changes, and connect/disconnect edges.
- Updated SurfaceEdge to expose internal state for testing.
- In VirutalCamera, track UseCase activeness with a hashmap. This is similar to how the real camera track it with UseCaseAttachInfo.
- Connect/disconnect/invalidate edge based on UseCase state.
Bug: 265818567
Test: manual test and ./gradlew bOS
Change-Id: I814c9606d13d396fed3928c1431c3cffb0cdb7e5
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();
+ }
}