Do not trigger onInvalidate callback if the SurfacEdge is closed.
Currently post-processing pipeline causes an infinite loop on SurfaceView. The root cause is that the SurfaceRequest.invalidate is called on a closed edge, which makes Preview sending a new SurfaceRequest for the closed edge.
- Separate disconnect() from close(). Disconnected edge can be connected again.
- Closed edge should never be called again. Added checks to enforce that.
- Once closed, invalidating SurfaceRequest won't trigger onInvalidateCallback.
Bug:266743420
Test: manual test and ./gradlew bOS
Change-Id: I6b6c1f9aba65c2e8af492943499482ef3b6c50db
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 ca3c2df..fa70c2e 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
@@ -41,6 +41,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraEffect;
import androidx.camera.core.SurfaceOutput;
import androidx.camera.core.SurfaceProcessor;
@@ -124,6 +125,11 @@
@NonNull
private final Set<Runnable> mOnInvalidatedListeners = new HashSet<>();
+ // Guarded by main thread.
+ // Tombstone flag indicates whether the edge has been closed. Once closed, the edge should
+ // never be used again.
+ private boolean mIsClosed = false;
+
/**
* Please see the getters to understand the parameters.
*/
@@ -156,6 +162,7 @@
@MainThread
public void addOnInvalidatedListener(@NonNull Runnable onInvalidated) {
checkMainThread();
+ checkNotClosed();
mOnInvalidatedListeners.add(onInvalidated);
}
@@ -170,6 +177,7 @@
@MainThread
public DeferrableSurface getDeferrableSurface() {
checkMainThread();
+ checkNotClosed();
checkAndSetHasConsumer();
return mSettableSurface;
}
@@ -209,6 +217,7 @@
public void setProvider(@NonNull DeferrableSurface provider)
throws DeferrableSurface.SurfaceClosedException {
checkMainThread();
+ checkNotClosed();
mSettableSurface.setProvider(provider);
}
@@ -248,10 +257,15 @@
public SurfaceRequest createSurfaceRequest(@NonNull CameraInternal cameraInternal,
@Nullable Range<Integer> expectedFpsRange) {
checkMainThread();
+ checkNotClosed();
// TODO(b/238230154) figure out how to support HDR.
SurfaceRequest surfaceRequest = new SurfaceRequest(mStreamSpec.getResolution(),
cameraInternal, expectedFpsRange,
- () -> mainThreadExecutor().execute(this::invalidate));
+ () -> mainThreadExecutor().execute(() -> {
+ if (!mIsClosed) {
+ invalidate();
+ }
+ }));
try {
DeferrableSurface deferrableSurface = surfaceRequest.getDeferrableSurface();
if (mSettableSurface.setProvider(deferrableSurface)) {
@@ -299,6 +313,7 @@
public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size inputSize,
@NonNull Rect cropRect, int rotationDegrees, boolean mirroring) {
checkMainThread();
+ checkNotClosed();
checkAndSetHasConsumer();
SettableSurface settableSurface = mSettableSurface;
return transformAsync(mSettableSurface.getSurface(),
@@ -321,7 +336,7 @@
}
/**
- * Closes the current connection and notifies that a new connection is ready.
+ * Resets connection and notifies that a new connection is ready.
*
* <p>Call this method to notify that the {@link Surface} previously provided via
* {@link #createSurfaceRequest} or {@link #setProvider} should no longer be used. The
@@ -337,7 +352,12 @@
@MainThread
public void invalidate() {
checkMainThread();
- close();
+ checkNotClosed();
+ if (mSettableSurface.canSetProvider()) {
+ // If the edge is still connectable, no-ops.
+ return;
+ }
+ disconnectWithoutCheckingClosed();
mHasConsumer = false;
mSettableSurface = new SettableSurface(mStreamSpec.getResolution());
for (Runnable onInvalidated : mOnInvalidatedListeners) {
@@ -346,20 +366,41 @@
}
/**
- * Closes the current connection.
+ * Closes the edge.
*
- * <p>This method uses the mechanism in {@link DeferrableSurface} and/or
- * {@link SurfaceOutputImpl} to notify the upstream pipeline that the {@link Surface}
- * previously provided via {@link #createSurfaceRequest} or {@link #setProvider} should no
- * longer be used. The upstream pipeline will stops writing to the {@link Surface}, and the
- * downstream pipeline can choose to release the {@link Surface} once the writing stops.
+ * <p> Disconnects the edge and sets a tombstone so it will never be used again. This method
+ * is idempotent.
+ *
+ * @see #disconnect()
+ */
+ @MainThread
+ public final void close() {
+ checkMainThread();
+ disconnectWithoutCheckingClosed();
+ mIsClosed = true;
+ }
+
+ /**
+ * Disconnects the edge.
+ *
+ * <p> Once disconnected, upstream should stop sending images to the edge, and downstream
+ * should stop expecting images from the edge.
+ *
+ * <p> This method notifies the upstream via {@link SettableSurface#close()}/
+ * {@link SurfaceOutputImpl}. By calling {@link SettableSurface#close()}, it also decrements the
+ * ref-count on downstream Surfaces so they can be released.
*
* @see DeferrableSurface#close().
* @see #invalidate()
*/
@MainThread
- public final void close() {
+ public final void disconnect() {
checkMainThread();
+ checkNotClosed();
+ disconnectWithoutCheckingClosed();
+ }
+
+ private void disconnectWithoutCheckingClosed() {
mSettableSurface.close();
if (mConsumerToNotify != null) {
mConsumerToNotify.requestClose();
@@ -476,6 +517,16 @@
return mStreamSpec;
}
+ private void checkNotClosed() {
+ checkState(!mIsClosed, "Edge is already closed.");
+ }
+
+ @VisibleForTesting
+ @NonNull
+ DeferrableSurface getDeferrableSurfaceForTesting() {
+ return mSettableSurface;
+ }
+
/**
* A {@link DeferrableSurface} that sets another {@link DeferrableSurface} as the source.
*
@@ -504,6 +555,12 @@
return mSurfaceFuture;
}
+ @MainThread
+ boolean canSetProvider() {
+ checkMainThread();
+ return mProvider == null && !isClosed();
+ }
+
/**
* Sets the {@link DeferrableSurface} that provides the surface.
*
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
index 60e066b..6aca2c6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEdgeTest.kt
@@ -93,6 +93,49 @@
}
@Test
+ fun closeEdgeThenInvalidate_callbackNotInvoked() {
+ // Arrange.
+ var invalidated = false
+ surfaceEdge.addOnInvalidatedListener {
+ invalidated = true
+ }
+ val surfaceRequest = surfaceEdge.createSurfaceRequest(FakeCamera())
+ // Act.
+ surfaceEdge.close()
+ surfaceRequest.invalidate()
+ shadowOf(getMainLooper()).idle()
+ // Assert.
+ assertThat(invalidated).isFalse()
+ }
+
+ @Test
+ fun invalidateWithoutProvider_settableSurfaceNotRecreated() {
+ // Arrange.
+ val deferrableSurface = surfaceEdge.deferrableSurfaceForTesting
+ // Act.
+ surfaceEdge.invalidate()
+ // Assert.
+ assertThat(surfaceEdge.deferrableSurfaceForTesting).isSameInstanceAs(deferrableSurface)
+ }
+
+ @Test
+ fun invalidateWithProvider_settableSurfaceNotRecreated() {
+ // Arrange.
+ val deferrableSurface = surfaceEdge.deferrableSurfaceForTesting
+ surfaceEdge.createSurfaceRequest(FakeCamera())
+ // Act.
+ surfaceEdge.invalidate()
+ // Assert.
+ assertThat(surfaceEdge.deferrableSurfaceForTesting).isNotSameInstanceAs(deferrableSurface)
+ }
+
+ @Test
+ fun callCloseTwice_noException() {
+ surfaceEdge.close()
+ surfaceEdge.close()
+ }
+
+ @Test
fun createWithStreamSpec_canGetStreamSpec() {
val edge = SurfaceEdge(
PREVIEW, FRAME_SPEC, Matrix(), true, Rect(), 0, false
@@ -106,6 +149,24 @@
surfaceEdge.setProvider(provider)
}
+ @Test(expected = IllegalStateException::class)
+ fun getDeferrableSurfaceOnClosedEdge_throwsException() {
+ surfaceEdge.close()
+ surfaceEdge.deferrableSurface
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun getSurfaceOutputOnClosedEdge_throwsException() {
+ surfaceEdge.close()
+ createSurfaceOutputFuture(surfaceEdge)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun createSurfaceRequest_throwsException() {
+ surfaceEdge.close()
+ surfaceEdge.createSurfaceRequest(FakeCamera())
+ }
+
@Test
fun provideSurfaceThenImmediatelyInvalidate_surfaceOutputFails() {
// Arrange: create SurfaceOutput and set provider.
@@ -189,10 +250,10 @@
}
@Test
- fun createSurfaceRequestThenClose_surfaceRequestCancelled() {
+ fun createSurfaceRequestThenDisconnect_surfaceRequestCancelled() {
// Arrange: create a SurfaceRequest then close.
val surfaceRequest = surfaceEdge.createSurfaceRequest(FakeCamera())
- surfaceEdge.close()
+ surfaceEdge.disconnect()
// Act: provide a Surface and get the result.
var result: SurfaceRequest.Result? = null
@@ -206,9 +267,9 @@
}
@Test
- fun createSurfaceOutputWithClosedInstance_surfaceOutputNotCreated() {
+ fun createSurfaceOutputWithDisconnectedEdge_surfaceOutputNotCreated() {
// Arrange: create a SurfaceOutput future from a closed LinkableSurface
- surfaceEdge.close()
+ surfaceEdge.disconnect()
val surfaceOutput = createSurfaceOutputFuture(surfaceEdge)
// Act: wait for the SurfaceOutput to return.
@@ -338,7 +399,7 @@
assertThat(isSurfaceReleased).isEqualTo(false)
// Act: close the LinkableSurface, signaling the intention to close the Surface.
- surfaceEdge.close()
+ surfaceEdge.disconnect()
shadowOf(getMainLooper()).idle()
// Assert: The close is propagated to the SurfaceRequest.