Implement v1.4 features in testlibs, Add ExtensionsTestlibControl to switch basic and advanced.

- upgrade testlibs to Extensions-Interface v1.4
- implement postview and onCaptureProcessProgressed in HDR extensions
- Add ExtensionsTestlibControl to be able to switch between basic and advanced.

Bug: 285451054
Test: camera-extensions test.
Change-Id: I1008941e97df4c2ea6fe752d58e5f348047af985
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index 849e115..7e445ca 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
+    implementation(project(":camera:camera-core"))
 }
 
 android {
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index eabb3d2..930f5a2 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -247,7 +247,7 @@
         @Override
         public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            process(results);
         }
 
         @Override
@@ -264,7 +264,7 @@
         public void processWithPostview(
                 @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            throw new UnsupportedOperationException("Postview is not supported");
         }
     }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 12a4a61..0d74482 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -264,6 +264,7 @@
         @Override
         public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
         }
 
         @Override
@@ -290,7 +291,7 @@
         public void processWithPostview(
                 @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            throw new UnsupportedOperationException("Postview is not supported");
         }
     }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index fbceb95..962d4bf 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -238,7 +238,7 @@
         @Override
         public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            process(results);
         }
 
         @Override
@@ -265,7 +265,7 @@
         public void processWithPostview(
                 @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            throw new UnsupportedOperationException("Postview is not supported");
         }
     }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
index 212bea0..5235e59 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionVersionImpl.java
@@ -32,7 +32,7 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExtensionVersionImpl {
     private static final String TAG = "ExtenderVersionImpl";
-    private static final String VERSION = "1.2.0";
+    private static final String VERSION = "1.4.0";
 
     public ExtensionVersionImpl() {
     }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java
new file mode 100644
index 0000000..6cf35b5
--- /dev/null
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ExtensionsTestlibControl.java
@@ -0,0 +1,68 @@
+/*
+ * 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.extensions.impl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * An internal utility class that allows tests to specify whether to enable basic extender or
+ * advanced extender of this testlib.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ExtensionsTestlibControl {
+    public enum ImplementationType {
+        ADVANCED_EXTENDER,
+        BASIC_EXTENDER
+    }
+
+    private static ExtensionsTestlibControl sInstance;
+    private static Object sLock = new Object();
+
+    private ExtensionsTestlibControl() {
+    }
+
+    /**
+     * Gets the singleton instance.
+     */
+    @NonNull
+    public static ExtensionsTestlibControl getInstance() {
+        synchronized (sLock) {
+            if (sInstance == null) {
+                sInstance = new ExtensionsTestlibControl();
+            }
+            return sInstance;
+        }
+    }
+
+    private ImplementationType mImplementationType = ImplementationType.BASIC_EXTENDER;
+
+    /**
+     * Set the implementation type.
+     */
+    public void setImplementationType(@NonNull ImplementationType type) {
+        mImplementationType = type;
+    }
+
+    /**
+     * Gets the implementation type;
+     */
+    @NonNull
+    public ImplementationType getImplementationType() {
+        return mImplementationType;
+    }
+}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 8b7abda..0ba4752 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -19,6 +19,7 @@
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
@@ -39,6 +40,7 @@
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -205,6 +207,7 @@
     @RequiresApi(23)
     static final class HdrImageCaptureExtenderCaptureProcessorImpl implements CaptureProcessorImpl {
         private ImageWriter mImageWriter;
+        private Surface mPostViewSurface;
 
         @Override
         public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
@@ -213,8 +216,22 @@
 
         @Override
         public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results) {
+            processInternal(results, null, null, false);
+        }
+
+        @Override
+        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            processInternal(results, resultCallback, executor, false);
+        }
+
+        public void processInternal(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
+                @Nullable ProcessResultImpl resultCallback, @Nullable Executor executor,
+                boolean hasPostview) {
             Log.d(TAG, "Started HDR CaptureProcessor");
 
+            Executor executorForCallback = executor != null ? executor : (cmd) -> cmd.run();
+
             // Check for availability of all requested images
             if (!results.containsKey(UNDER_STAGE_ID)) {
                 Log.w(TAG,
@@ -246,6 +263,9 @@
             // Do processing here
             // The sample here simply returns the normal image result
             Image normalImage = imageDataPairs.get(NORMAL_STAGE_ID).first;
+            if (hasPostview) {
+                YuvToJpegConverter.writeYuvToJpegSurface(normalImage, mPostViewSurface);
+            }
 
             if (outputImage.getWidth() != normalImage.getWidth()
                     || outputImage.getHeight() != normalImage.getHeight()) {
@@ -256,6 +276,12 @@
                         outputImage.getHeight()));
             }
 
+            if (resultCallback != null) {
+                executorForCallback.execute(() -> {
+                    resultCallback.onCaptureProcessProgressed(10);
+                });
+            }
+
             try {
                 // copy y plane
                 Image.Plane inYPlane = normalImage.getPlanes()[0];
@@ -274,6 +300,11 @@
                     }
                 }
 
+                if (resultCallback != null) {
+                    executorForCallback.execute(
+                            () -> resultCallback.onCaptureProcessProgressed(50));
+                }
+
                 // Copy UV
                 for (int i = 1; i < 3; i++) {
                     Image.Plane inPlane = normalImage.getPlanes()[i];
@@ -302,16 +333,35 @@
             }
 
             mImageWriter.queueInputImage(outputImage);
+            if (resultCallback != null) {
+                executorForCallback.execute(
+                        () -> resultCallback.onCaptureProcessProgressed(100));
+            }
+
+            TotalCaptureResult captureResult = results.get(NORMAL_STAGE_ID).second;
+
+            if (resultCallback != null) {
+                executorForCallback.execute(
+                        () -> resultCallback.onCaptureCompleted(
+                                captureResult.get(CaptureResult.SENSOR_TIMESTAMP),
+                                getFilteredResults(captureResult)));
+            }
 
             Log.d(TAG, "Completed HDR CaptureProcessor");
         }
 
-        @Override
-        public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
-                @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+        @SuppressWarnings("unchecked")
+        private List<Pair<CaptureResult.Key, Object>> getFilteredResults(
+                TotalCaptureResult captureResult) {
+            List<Pair<CaptureResult.Key, Object>> list = new ArrayList<>();
+            for (CaptureResult.Key key : captureResult.getKeys()) {
+                list.add(new Pair<>(key, captureResult.get(key)));
+            }
 
+            return list;
         }
 
+
         @Override
         public void onResolutionUpdate(@NonNull Size size) {
 
@@ -324,19 +374,20 @@
 
         @Override
         public void onPostviewOutputSurface(@NonNull Surface surface) {
-
+            mPostViewSurface = surface;
         }
 
         @Override
         public void onResolutionUpdate(@NonNull Size size, @NonNull Size postviewSize) {
-
+            onResolutionUpdate(size);
         }
 
         @Override
         public void processWithPostview(
                 @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            Log.d(TAG, "processWithPostview");
+            processInternal(results, resultCallback, executor, true);
         }
     }
 
@@ -354,12 +405,13 @@
     @Nullable
     @Override
     public List<Pair<Integer, Size[]>> getSupportedPostviewResolutions(@NonNull Size captureSize) {
-        return null;
+        Pair<Integer, Size[]> pair = new Pair<>(ImageFormat.JPEG, new Size[] {captureSize});
+        return Arrays.asList(pair);
     }
 
     @Override
     public boolean isCaptureProcessProgressAvailable() {
-        return false;
+        return true;
     }
 
     @Nullable
@@ -370,7 +422,7 @@
 
     @Override
     public boolean isPostviewAvailable() {
-        return false;
+        return true;
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index e0cab8d..60d2888 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -48,6 +48,8 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class HdrPreviewExtenderImpl implements PreviewExtenderImpl {
+    private static final String TAG = "HdrPreviewExtenderImpl";
+
     private static final int DEFAULT_STAGE_ID = 0;
 
     @Nullable
@@ -175,7 +177,7 @@
         @Override
         public void process(Image image, TotalCaptureResult result,
                 ProcessResultImpl resultCallback, Executor executor) {
-
+            process(image, result);
         }
 
         @Override
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index fbb9b03..b3e38b7 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -239,6 +239,7 @@
         @Override
         public void process(@NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
+            process(results);
         }
 
         @Override
@@ -263,7 +264,7 @@
         public void processWithPostview(
                 @NonNull Map<Integer, Pair<Image, TotalCaptureResult>> results,
                 @NonNull ProcessResultImpl resultCallback, @Nullable Executor executor) {
-
+            throw new UnsupportedOperationException("Postview is not supported");
         }
     }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java
new file mode 100644
index 0000000..0681b97
--- /dev/null
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/YuvToJpegConverter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.extensions.impl;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.media.Image;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageProcessingUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+@RequiresApi(23)
+class YuvToJpegConverter {
+    private YuvToJpegConverter() {}
+    public static void writeYuvToJpegSurface(@NonNull Image yuvMediaImage,
+            @NonNull Surface jpegSurface) {
+        byte[] yuvBytes = yuv_420_888toNv21(yuvMediaImage);
+        YuvImage yuvImage = new YuvImage(yuvBytes, ImageFormat.NV21, yuvMediaImage.getWidth(),
+                yuvMediaImage.getHeight(), null);
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        yuvImage.compressToJpeg(
+                new Rect(0, 0, yuvMediaImage.getWidth(), yuvMediaImage.getHeight()),
+                95, byteArrayOutputStream);
+        ImageProcessingUtil.writeJpegBytesToSurface(jpegSurface,
+                byteArrayOutputStream.toByteArray());
+        yuvMediaImage.getPlanes()[0].getBuffer().rewind();
+    }
+
+    @NonNull
+    private static byte[] yuv_420_888toNv21(@NonNull Image image) {
+        Image.Plane yPlane = image.getPlanes()[0];
+        Image.Plane uPlane = image.getPlanes()[1];
+        Image.Plane vPlane = image.getPlanes()[2];
+
+        ByteBuffer yBuffer = yPlane.getBuffer();
+        ByteBuffer uBuffer = uPlane.getBuffer();
+        ByteBuffer vBuffer = vPlane.getBuffer();
+        yBuffer.rewind();
+        uBuffer.rewind();
+        vBuffer.rewind();
+
+        int ySize = yBuffer.remaining();
+
+        int position = 0;
+        byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)];
+
+        // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+        for (int row = 0; row < image.getHeight(); row++) {
+            yBuffer.get(nv21, position, image.getWidth());
+            position += image.getWidth();
+            yBuffer.position(
+                    Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride()));
+        }
+
+        int chromaHeight = image.getHeight() / 2;
+        int chromaWidth = image.getWidth() / 2;
+        int vRowStride = vPlane.getRowStride();
+        int uRowStride = uPlane.getRowStride();
+        int vPixelStride = vPlane.getPixelStride();
+        int uPixelStride = uPlane.getPixelStride();
+
+        // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+        // perform faster bulk gets from the byte buffers.
+        byte[] vLineBuffer = new byte[vRowStride];
+        byte[] uLineBuffer = new byte[uRowStride];
+        for (int row = 0; row < chromaHeight; row++) {
+            vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining()));
+            uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining()));
+            int vLineBufferPosition = 0;
+            int uLineBufferPosition = 0;
+            for (int col = 0; col < chromaWidth; col++) {
+                nv21[position++] = vLineBuffer[vLineBufferPosition];
+                nv21[position++] = uLineBuffer[uLineBufferPosition];
+                vLineBufferPosition += vPixelStride;
+                uLineBufferPosition += uPixelStride;
+            }
+        }
+
+        return nv21;
+    }
+}