Add a tone-mapping ImageProcessor in view test app

Bug: 249593716
Test: manual test and ./gradlew bOS
Change-Id: I9b7a2a710a7ee42975ab7186fafbaed41704e59e
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
index 388a07b..a087c88 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
@@ -35,7 +35,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ExperimentalGetImage;
 import androidx.camera.core.ImageInfo;
 import androidx.camera.core.ImageProxy;
@@ -91,7 +90,6 @@
      *
      * <p>The {@link Bitmap} must be {@link Bitmap.Config#ARGB_8888}.
      */
-    @VisibleForTesting
     public RgbaImageProxy(@NonNull Bitmap bitmap, @NonNull Rect cropRect, int rotationDegrees,
             @NonNull Matrix sensorToBuffer, long timestamp) {
         this(createDirectByteBuffer(bitmap),
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index b3c9695..28e1fde 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -133,7 +133,7 @@
     }
 
     @Test
-    fun enableEffect_effectIsEnabled() {
+    fun enableEffect_previewEffectIsEnabled() {
         // Arrange: launch app and verify effect is inactive.
         fragment.assertPreviewIsStreaming()
         val processor =
@@ -150,6 +150,23 @@
     }
 
     @Test
+    fun enableEffect_imageCaptureEffectIsEnabled() {
+        // Arrange: launch app and verify effect is inactive.
+        fragment.assertPreviewIsStreaming()
+        val effect = fragment.mToneMappingImageEffect as ToneMappingImageEffect
+        assertThat(effect.isInvoked()).isFalse()
+
+        // Act: turn on effect.
+        val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+        uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+        instrumentation.waitForIdleSync()
+        fragment.assertCanTakePicture()
+
+        // Assert: verify that effect is active.
+        assertThat(effect.isInvoked()).isTrue()
+    }
+
+    @Test
     fun controllerBound_canGetCameraControl() {
         fragment.assertPreviewIsStreaming()
         instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index 3881ff3..79c1b1c 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -20,8 +20,8 @@
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
 
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
@@ -150,6 +150,7 @@
 
     @VisibleForTesting
     ToneMappingPreviewEffect mToneMappingPreviewEffect;
+    ToneMappingImageEffect mToneMappingImageEffect;
 
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
@@ -221,6 +222,7 @@
 
         // Set up post-processing effects.
         mToneMappingPreviewEffect = new ToneMappingPreviewEffect();
+        mToneMappingImageEffect = new ToneMappingImageEffect();
         mEffectToggle = view.findViewById(R.id.effect_toggle);
         mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
         onEffectsToggled();
@@ -372,7 +374,8 @@
 
     private void onEffectsToggled() {
         if (mEffectToggle.isChecked()) {
-            mCameraController.setEffects(singletonList(mToneMappingPreviewEffect));
+            mCameraController.setEffects(
+                    asList(mToneMappingPreviewEffect, mToneMappingImageEffect));
         } else {
             mCameraController.setEffects(emptyList());
         }
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
new file mode 100644
index 0000000..846fa0c
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 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.integration.view
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.ImageProcessor
+import androidx.camera.core.ImageProcessor.Response
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.imagecapture.RgbaImageProxy
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+
+/**
+ * A image effect that applies the same tone mapping as [ToneMappingSurfaceProcessor].
+ */
+class ToneMappingImageEffect : CameraEffect(
+    IMAGE_CAPTURE, mainThreadExecutor(), ToneMappingImageProcessor()
+) {
+
+    fun isInvoked(): Boolean {
+        return (imageProcessor as ToneMappingImageProcessor).processoed
+    }
+
+    private class ToneMappingImageProcessor : ImageProcessor {
+
+        var processoed = false
+
+        override fun process(request: ImageProcessor.Request): Response {
+            processoed = true
+            val inputImage = request.inputImages.single() as RgbaImageProxy
+            val bitmap = inputImage.createBitmap()
+            applyToneMapping(bitmap)
+            val outputImage = createOutputImage(bitmap, inputImage)
+            inputImage.close()
+            return Response { outputImage }
+        }
+
+        /**
+         * Creates output image
+         */
+        private fun createOutputImage(newBitmap: Bitmap, imageIn: ImageProxy): ImageProxy {
+            return RgbaImageProxy(
+                newBitmap,
+                imageIn.cropRect,
+                imageIn.imageInfo.rotationDegrees,
+                imageIn.imageInfo.sensorToBufferTransformMatrix,
+                imageIn.imageInfo.timestamp
+            )
+        }
+
+        /**
+         * Applies the same color matrix as [ToneMappingSurfaceProcessor].
+         */
+        private fun applyToneMapping(bitmap: Bitmap) {
+            val paint = Paint()
+            paint.colorFilter = ColorMatrixColorFilter(
+                floatArrayOf(
+                    0.5F, 0.8F, 0.3F, 0F, 0F,
+                    0.4F, 0.7F, 0.2F, 0F, 0F,
+                    0.3F, 0.5F, 0.1F, 0F, 0F,
+                    0F, 0F, 0F, 1F, 0F,
+                )
+            )
+            val canvas = Canvas(bitmap)
+            canvas.drawBitmap(bitmap, 0F, 0F, paint)
+        }
+    }
+}