Forward parent camera's metadata to children
- Create VirutalCameraCaptureResult that wraps around a real CameraCaptureResult with tag bundle overriden.
- In VirtualCamrea, forward parent metadata to children with tag bundle overriden.
- Update FakeUseCase to track received metadata for testing.
Bug: 265818567
Test: manual test and ./gradlew bOS
Change-Id: Ibea9998907b69792adeb569a1ab09ff8888518d2
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 0447e07..0339e04 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
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.camera.core.streamsharing;
import static androidx.camera.core.CameraEffect.PREVIEW;
@@ -37,6 +36,7 @@
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.processing.SurfaceEdge;
@@ -60,9 +60,7 @@
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class VirtualCamera implements CameraInternal {
-
private static final String UNSUPPORTED_MESSAGE = "Operation not supported by VirtualCamera.";
-
// Children UseCases associated with this virtual camera.
@NonNull
final Set<UseCase> mChildren;
@@ -94,7 +92,6 @@
}
// --- API for StreamSharing ---
-
void mergeChildrenConfigs(@NonNull MutableConfig mutableConfig) {
Set<UseCaseConfig<?>> childrenConfigs = new HashSet<>();
for (UseCase useCase : mChildren) {
@@ -188,31 +185,24 @@
}
// --- Handle children state change ---
-
// TODO(b/264936250): Handle children state changes.
-
@Override
public void onUseCaseActive(@NonNull UseCase useCase) {
-
}
@Override
public void onUseCaseInactive(@NonNull UseCase useCase) {
-
}
@Override
public void onUseCaseUpdated(@NonNull UseCase useCase) {
-
}
@Override
public void onUseCaseReset(@NonNull UseCase useCase) {
-
}
// --- Forward parent camera properties and events ---
-
@NonNull
@Override
public CameraControlInternal getCameraControlInternal() {
@@ -234,20 +224,29 @@
}
// --- private methods ---
-
@NonNull
CameraCaptureCallback createCameraCaptureCallback() {
return new CameraCaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
super.onCaptureCompleted(cameraCaptureResult);
- // TODO(b/265818567): forward metadata to children.
+ for (UseCase child : mChildren) {
+ sendCameraCaptureResultToChild(cameraCaptureResult, child.getSessionConfig());
+ }
}
};
}
- // --- Unused overrides ---
+ static void sendCameraCaptureResultToChild(
+ @NonNull CameraCaptureResult cameraCaptureResult,
+ @NonNull SessionConfig sessionConfig) {
+ for (CameraCaptureCallback callback : sessionConfig.getRepeatingCameraCaptureCallbacks()) {
+ callback.onCaptureCompleted(new VirtualCameraCaptureResult(cameraCaptureResult,
+ sessionConfig.getRepeatingCaptureConfig().getTagBundle()));
+ }
+ }
+ // --- Unused overrides ---
@Override
public void open() {
throw new UnsupportedOperationException(UNSUPPORTED_MESSAGE);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResult.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResult.java
new file mode 100644
index 0000000..3e96bc0
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraCaptureResult.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 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.core.streamsharing;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraCaptureMetaData;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.TagBundle;
+
+/**
+ * A virtual {@link CameraCaptureResult} which based on a real instance with some fields
+ * overridden.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class VirtualCameraCaptureResult implements CameraCaptureResult {
+
+ @NonNull
+ private final CameraCaptureResult mBaseCameraCaptureResult;
+ @NonNull
+ private final TagBundle mTagBundle;
+
+ /**
+ * @param baseCameraCaptureResult Most of the fields return the value of the base instance.
+ * @param tagBundle the overridden value for the {@link #getTagBundle()} field.
+ */
+ VirtualCameraCaptureResult(
+ @NonNull CameraCaptureResult baseCameraCaptureResult,
+ @NonNull TagBundle tagBundle) {
+ mBaseCameraCaptureResult = baseCameraCaptureResult;
+ mTagBundle = tagBundle;
+ }
+
+ @NonNull
+ @Override
+ public TagBundle getTagBundle() {
+ // Returns the overridden value.
+ return mTagBundle;
+ }
+
+ @NonNull
+ @Override
+ public CameraCaptureMetaData.AfMode getAfMode() {
+ return mBaseCameraCaptureResult.getAfMode();
+ }
+
+ @NonNull
+ @Override
+ public CameraCaptureMetaData.AfState getAfState() {
+ return mBaseCameraCaptureResult.getAfState();
+ }
+
+ @NonNull
+ @Override
+ public CameraCaptureMetaData.AeState getAeState() {
+ return mBaseCameraCaptureResult.getAeState();
+ }
+
+ @NonNull
+ @Override
+ public CameraCaptureMetaData.AwbState getAwbState() {
+ return mBaseCameraCaptureResult.getAwbState();
+ }
+
+ @NonNull
+ @Override
+ public CameraCaptureMetaData.FlashState getFlashState() {
+ return mBaseCameraCaptureResult.getFlashState();
+ }
+
+ @Override
+ public long getTimestamp() {
+ return mBaseCameraCaptureResult.getTimestamp();
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index e6a2da4..ec9354c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -17,23 +17,33 @@
package androidx.camera.core.streamsharing
import android.os.Build
+import android.os.Looper.getMainLooper
import android.util.Size
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureResult
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
import androidx.camera.core.processing.DefaultSurfaceProcessor
import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCaptureResult
import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
import androidx.camera.testing.fakes.FakeUseCase
import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@@ -45,13 +55,13 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class StreamSharingTest {
- private val parentCamera = FakeCamera()
private val child1 = FakeUseCase()
private val child2 = FakeUseCase()
private val useCaseConfigFactory = FakeUseCaseConfigFactory()
private val camera = FakeCamera()
private lateinit var streamSharing: StreamSharing
private val size = Size(800, 600)
+ private lateinit var defaultConfig: UseCaseConfig<*>
@Before
fun setUp() {
@@ -60,17 +70,60 @@
mainThreadExecutor()
)
}
- streamSharing = StreamSharing(parentCamera, setOf(child1, child2), useCaseConfigFactory)
+ streamSharing = StreamSharing(camera, setOf(child1, child2), useCaseConfigFactory)
+ defaultConfig = streamSharing.getDefaultConfig(true, useCaseConfigFactory)!!
+ }
+
+ @After
+ fun tearDown() {
+ if (streamSharing.camera != null) {
+ streamSharing.unbindFromCamera(streamSharing.camera!!)
+ }
+ shadowOf(getMainLooper()).idle()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun feedMetadataToParent_propagatesToChildren() {
+ // Arrange: set tag bundle in children SessionConfig.
+ val key = "key"
+ val value = "value"
+ val result1 = child1.setTagBundleOnSessionConfigAsync(key, value)
+ val result2 = child2.setTagBundleOnSessionConfigAsync(key, value)
+ streamSharing.bindToCamera(camera, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+
+ // Act: feed metadata to the parent.
+ streamSharing.sessionConfig.repeatingCameraCaptureCallbacks.single()
+ .onCaptureCompleted(FakeCameraCaptureResult())
+
+ // Assert: children receives the metadata with the tag bundle overridden.
+ assertThat(result1.getCompleted().tagBundle.getTag(key)).isEqualTo(value)
+ assertThat(result2.getCompleted().tagBundle.getTag(key)).isEqualTo(value)
+ }
+
+ private fun FakeUseCase.setTagBundleOnSessionConfigAsync(
+ key: String,
+ value: String
+ ): Deferred<CameraCaptureResult> {
+ val deferredResult = CompletableDeferred<CameraCaptureResult>()
+ this.setSessionConfigSupplier {
+ val builder = SessionConfig.Builder()
+ builder.addTag(key, value)
+ builder.addRepeatingCameraCaptureCallback(object : CameraCaptureCallback() {
+ override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
+ deferredResult.complete(cameraCaptureResult)
+ }
+ })
+ builder.build()
+ }
+ return deferredResult
}
@Test
fun updateStreamSpec_propagatesToChildren() {
// Arrange: bind StreamSharing to the camera.
- streamSharing.bindToCamera(
- camera,
- null,
- streamSharing.getDefaultConfig(true, useCaseConfigFactory)
- )
+ streamSharing.bindToCamera(camera, null, defaultConfig)
// Act: update suggested specs.
streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
@@ -100,11 +153,7 @@
@Test
fun onError_restartsPipeline() {
// Arrange: bind stream sharing and update specs.
- streamSharing.bindToCamera(
- camera,
- null,
- streamSharing.getDefaultConfig(true, useCaseConfigFactory)
- )
+ streamSharing.bindToCamera(camera, null, defaultConfig)
streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
val cameraEdge = streamSharing.cameraEdge
val node = streamSharing.node
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 9c396e3..618ef08 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
@@ -22,12 +22,15 @@
import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
+import androidx.core.util.Supplier;
import java.util.concurrent.atomic.AtomicInteger;
@@ -36,11 +39,13 @@
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class FakeUseCase extends UseCase {
+
private volatile boolean mIsDetached = false;
private final AtomicInteger mStateAttachedCount = new AtomicInteger(0);
private final CaptureType mCaptureType;
private boolean mMergedConfigRetrieved = false;
private int mPipelineCreationCount = 0;
+ private Supplier<SessionConfig> mSessionConfigSupplier;
/**
* Creates a new instance of a {@link FakeUseCase} with a given configuration and capture type.
@@ -123,10 +128,24 @@
@Override
@NonNull
protected StreamSpec onSuggestedStreamSpecUpdated(@NonNull StreamSpec suggestedStreamSpec) {
- mPipelineCreationCount++;
+ SessionConfig sessionConfig = createPipeline();
+ if (sessionConfig != null) {
+ updateSessionConfig(sessionConfig);
+ }
return suggestedStreamSpec;
}
+ @Nullable
+ SessionConfig createPipeline() {
+ mPipelineCreationCount++;
+ if (mSessionConfigSupplier != null) {
+ return mSessionConfigSupplier.get();
+ } else {
+ return null;
+ }
+ }
+
+
/**
* Returns true if {@link #onUnbind()} has been called previously.
*/
@@ -154,4 +173,11 @@
public int getPipelineCreationCount() {
return mPipelineCreationCount;
}
+
+ /**
+ * Returns {@link CameraCaptureResult} received by this use case.
+ */
+ public void setSessionConfigSupplier(@NonNull Supplier<SessionConfig> sessionConfigSupplier) {
+ mSessionConfigSupplier = sessionConfigSupplier;
+ }
}