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;
+    }
 }