Set TagBundle to the CaptureRequest

The TagBundle can be set to the "extras" which is a
map of keys of type Metadata.Key<T>.

Update CaptureConfigAdapter to set the TagBundle from
CaptureConfig to the Request.

Test: manually run CaptureConfigAdapterDeviceTest on lab devices
Change-Id: I6ba724614879395939334dfe1b0375c0d64dbcca
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 22e07ea..db18e12 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -75,6 +75,7 @@
     androidTestImplementation(libs.truth)
     androidTestImplementation(project(":annotation:annotation-experimental"))
     androidTestImplementation(project(":camera:camera-lifecycle"))
+    androidTestImplementation(project(":camera:camera-testing"))
     androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
 }
 
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
new file mode 100644
index 0000000..8ef8557
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2021 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.camera2.pipe.integration
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraDevice
+import android.view.Surface
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.fakes.FakeUseCase
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+private const val DEFAULT_LENS_FACING_SELECTOR = CameraSelector.LENS_FACING_BACK
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class CaptureConfigAdapterDeviceTest {
+
+    @get:Rule
+    val useCamera: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    private var cameraControl: CameraControlAdapter? = null
+    private var camera: CameraUseCaseAdapter? = null
+    private val testDeferrableSurface = TestDeferrableSurface()
+    private val fakeUseCase = FakeTestUseCase(
+        FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig
+    ).apply {
+        setupSessionConfig(
+            SessionConfig.Builder().also { sessionConfigBuilder ->
+                sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+                sessionConfigBuilder.addSurface(testDeferrableSurface)
+            }
+        )
+    }
+
+    @Before
+    fun setUp() = runBlocking {
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING_SELECTOR))
+
+        val context: Context = ApplicationProvider.getApplicationContext()
+        CameraX.initialize(context, CameraPipeConfig.defaultConfig())
+        camera = CameraUtil.createCameraUseCaseAdapter(
+            context,
+            CameraSelector.Builder().requireLensFacing(
+                DEFAULT_LENS_FACING_SELECTOR
+            ).build()
+        ).apply {
+            withContext(Dispatchers.Main) {
+                addUseCases(listOf(fakeUseCase))
+            }
+        }
+
+        cameraControl = camera!!.cameraControl as CameraControlAdapter
+    }
+
+    @After
+    fun tearDown() {
+        camera?.detachUseCases()
+        testDeferrableSurface.close()
+        CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
+    }
+
+    @Test
+    fun tagBundleTest() = runBlocking {
+        // Arrange
+        val deferred = CompletableDeferred<CameraCaptureResult>()
+        val tagKey = "TestTagBundleKey"
+        val tagValue = "testing"
+        val captureConfig = CaptureConfig.Builder()
+            .apply {
+                templateType = CameraDevice.TEMPLATE_PREVIEW
+                addTag(tagKey, tagValue)
+                addSurface(testDeferrableSurface)
+                addCameraCaptureCallback(object : CameraCaptureCallback() {
+                    override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
+                        deferred.complete(cameraCaptureResult)
+                    }
+
+                    override fun onCaptureFailed(failure: CameraCaptureFailure) {
+                        deferred.completeExceptionally(Throwable(failure.reason.toString()))
+                    }
+
+                    override fun onCaptureCancelled() {
+                        deferred.cancel()
+                    }
+                })
+            }.build()
+
+        // Act
+        cameraControl!!.submitCaptureRequests(listOf(captureConfig))
+
+        // Assert
+        Truth.assertThat(
+            withTimeoutOrNull(timeMillis = 5000) {
+                deferred.await()
+            }!!.tagBundle.getTag(tagKey)
+        ).isEqualTo(tagValue)
+    }
+}
+
+private class FakeTestUseCase(
+    config: FakeUseCaseConfig,
+) : FakeUseCase(config) {
+
+    fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
+        updateSessionConfig(sessionConfigBuilder.build())
+        notifyActive()
+    }
+}
+
+private class TestDeferrableSurface : DeferrableSurface() {
+    init {
+        terminationFuture.addListener(
+            { cleanUp() },
+            Dispatchers.IO.asExecutor()
+        )
+    }
+
+    private val surfaceTexture = SurfaceTexture(0).also {
+        it.setDefaultBufferSize(640, 480)
+    }
+    val testSurface = Surface(surfaceTexture)
+
+    override fun provideSurface(): ListenableFuture<Surface> {
+        return Futures.immediateFuture(testSurface)
+    }
+
+    fun cleanUp() {
+        testSurface.release()
+        surfaceTexture.release()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
index 4da13dc..1fea658 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
@@ -20,6 +20,7 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
 import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.DeferrableSurface
@@ -74,12 +75,11 @@
                 configOptions.retrieveOption(CaptureConfig.OPTION_JPEG_QUALITY)!!.toByte()
         }
 
-        // TODO: When adding support for extensions, also add support for passing capture request
-        //  tags with each request, since extensions may rely on these tags.
         return Request(
             streams = listOf(streamId),
             listeners = listOf(callbacks),
             parameters = parameters,
+            extras = mapOf(CAMERAX_TAG_BUNDLE to captureConfig.tagBundle),
             template = RequestTemplate(captureConfig.templateType)
         )
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
index 7e82479..69fcd62 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
@@ -20,9 +20,11 @@
 import android.os.Build
 import android.view.Surface
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
 import androidx.camera.core.impl.CameraCaptureCallback
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.TagBundle
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
@@ -129,6 +131,31 @@
         val rotation = request.parameters[CaptureRequest.JPEG_ORIENTATION]
         assertThat(rotation).isEqualTo(90)
     }
+
+    @Test
+    fun shouldSetTagBundleToTheRequest() {
+        // Arrange
+        val surface = FakeSurface()
+        val configAdapter = CaptureConfigAdapter(
+            surfaceToStreamMap = mapOf(surface to StreamId(0)),
+            callbackExecutor = Executors.newSingleThreadExecutor()
+        )
+
+        val tagKey = "testTagKey"
+        val tagValue = "testTagValue"
+        val captureConfig = CaptureConfig.Builder().apply {
+            addSurface(surface)
+            addTag(tagKey, tagValue)
+        }.build()
+
+        // Act
+        val request = configAdapter.mapToRequest(captureConfig)
+
+        // Assert
+        assertThat(request.extras).containsKey(CAMERAX_TAG_BUNDLE)
+        val tagBundle = request.extras[CAMERAX_TAG_BUNDLE] as TagBundle
+        assertThat(tagBundle.getTag(tagKey)).isEqualTo(tagValue)
+    }
 }
 
 private class FakeSurface : DeferrableSurface() {