[camera] Expand CameraImage DTO with properties for lens aperture, exposure time and ISO.  (#4256)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 68188d6..73cce2c 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.1
+
+* Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto.
+
 ## 0.9.0
 
 * Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues.
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 4724d22..43479ac 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -61,6 +61,7 @@
 import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
 import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
 import io.flutter.plugins.camera.media.MediaRecorderBuilder;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
 import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
 import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
 import java.io.File;
@@ -130,6 +131,8 @@
 
   /** Holds the current capture timeouts */
   private CaptureTimeoutsWrapper captureTimeouts;
+  /** Holds the last known capture properties */
+  private CameraCaptureProperties captureProps;
 
   private MethodChannel.Result flutterResult;
 
@@ -158,7 +161,8 @@
 
     // Create capture callback.
     captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000);
-    cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts);
+    captureProps = new CameraCaptureProperties();
+    cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts, captureProps);
 
     startBackgroundThread();
   }
@@ -1042,6 +1046,11 @@
           imageBuffer.put("height", img.getHeight());
           imageBuffer.put("format", img.getFormat());
           imageBuffer.put("planes", planes);
+          imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture());
+          imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime());
+          Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity();
+          imageBuffer.put(
+              "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity);
 
           final Handler handler = new Handler(Looper.getMainLooper());
           handler.post(() -> imageStreamSink.success(imageBuffer));
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java
index 21dcb60..805f182 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java
@@ -11,6 +11,7 @@
 import android.hardware.camera2.TotalCaptureResult;
 import android.util.Log;
 import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
 import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
 
 /**
@@ -22,13 +23,16 @@
   private final CameraCaptureStateListener cameraStateListener;
   private CameraState cameraState;
   private final CaptureTimeoutsWrapper captureTimeouts;
+  private final CameraCaptureProperties captureProps;
 
   private CameraCaptureCallback(
       @NonNull CameraCaptureStateListener cameraStateListener,
-      @NonNull CaptureTimeoutsWrapper captureTimeouts) {
+      @NonNull CaptureTimeoutsWrapper captureTimeouts,
+      @NonNull CameraCaptureProperties captureProps) {
     cameraState = CameraState.STATE_PREVIEW;
     this.cameraStateListener = cameraStateListener;
     this.captureTimeouts = captureTimeouts;
+    this.captureProps = captureProps;
   }
 
   /**
@@ -41,8 +45,9 @@
    */
   public static CameraCaptureCallback create(
       @NonNull CameraCaptureStateListener cameraStateListener,
-      @NonNull CaptureTimeoutsWrapper captureTimeouts) {
-    return new CameraCaptureCallback(cameraStateListener, captureTimeouts);
+      @NonNull CaptureTimeoutsWrapper captureTimeouts,
+      @NonNull CameraCaptureProperties captureProps) {
+    return new CameraCaptureCallback(cameraStateListener, captureTimeouts, captureProps);
   }
 
   /**
@@ -67,6 +72,16 @@
     Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
     Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
 
+    // Update capture properties
+    if (result instanceof TotalCaptureResult) {
+      Float lensAperture = result.get(CaptureResult.LENS_APERTURE);
+      Long sensorExposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);
+      Integer sensorSensitivity = result.get(CaptureResult.SENSOR_SENSITIVITY);
+      this.captureProps.setLastLensAperture(lensAperture);
+      this.captureProps.setLastSensorExposureTime(sensorExposureTime);
+      this.captureProps.setLastSensorSensitivity(sensorSensitivity);
+    }
+
     if (cameraState != CameraState.STATE_PREVIEW) {
       Log.d(
           TAG,
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java
new file mode 100644
index 0000000..68177f4
--- /dev/null
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java
@@ -0,0 +1,67 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.types;
+
+public class CameraCaptureProperties {
+
+  private Float lastLensAperture;
+  private Long lastSensorExposureTime;
+  private Integer lastSensorSensitivity;
+
+  /**
+   * Gets the last known lens aperture. (As f-stop value)
+   *
+   * @return the last known lens aperture. (As f-stop value)
+   */
+  public Float getLastLensAperture() {
+    return lastLensAperture;
+  }
+
+  /**
+   * Sets the last known lens aperture. (As f-stop value)
+   *
+   * @param lastLensAperture - The last known lens aperture to set. (As f-stop value)
+   */
+  public void setLastLensAperture(Float lastLensAperture) {
+    this.lastLensAperture = lastLensAperture;
+  }
+
+  /**
+   * Gets the last known sensor exposure time in nanoseconds.
+   *
+   * @return the last known sensor exposure time in nanoseconds.
+   */
+  public Long getLastSensorExposureTime() {
+    return lastSensorExposureTime;
+  }
+
+  /**
+   * Sets the last known sensor exposure time in nanoseconds.
+   *
+   * @param lastSensorExposureTime - The last known sensor exposure time to set, in nanoseconds.
+   */
+  public void setLastSensorExposureTime(Long lastSensorExposureTime) {
+    this.lastSensorExposureTime = lastSensorExposureTime;
+  }
+
+  /**
+   * Gets the last known sensor sensitivity in ISO arithmetic units.
+   *
+   * @return the last known sensor sensitivity in ISO arithmetic units.
+   */
+  public Integer getLastSensorSensitivity() {
+    return lastSensorSensitivity;
+  }
+
+  /**
+   * Sets the last known sensor sensitivity in ISO arithmetic units.
+   *
+   * @param lastSensorSensitivity - The last known sensor sensitivity to set, in ISO arithmetic
+   *     units.
+   */
+  public void setLastSensorSensitivity(Integer lastSensorSensitivity) {
+    this.lastSensorSensitivity = lastSensorSensitivity;
+  }
+}
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java
index 4964aef..934aff8 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java
@@ -17,6 +17,7 @@
 import android.hardware.camera2.CaptureResult.Key;
 import android.hardware.camera2.TotalCaptureResult;
 import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
 import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
 import io.flutter.plugins.camera.types.Timeout;
 import io.flutter.plugins.camera.utils.TestUtils;
@@ -40,6 +41,7 @@
   private CaptureRequest mockCaptureRequest;
   private CaptureResult mockPartialCaptureResult;
   private CaptureTimeoutsWrapper mockCaptureTimeouts;
+  private CameraCaptureProperties mockCaptureProps;
   private TotalCaptureResult mockTotalCaptureResult;
   private MockedStatic<Timeout> mockedStaticTimeout;
   private Timeout mockTimeout;
@@ -83,6 +85,7 @@
     mockTotalCaptureResult = mock(TotalCaptureResult.class);
     mockTimeout = mock(Timeout.class);
     mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class);
+    mockCaptureProps = mock(CameraCaptureProperties.class);
     when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout);
     when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout);
 
@@ -95,7 +98,8 @@
     mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout);
 
     cameraCaptureCallback =
-        CameraCaptureCallback.create(mockCaptureStateListener, mockCaptureTimeouts);
+        CameraCaptureCallback.create(
+            mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps);
   }
 
   @Override
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java
new file mode 100644
index 0000000..75a5b25
--- /dev/null
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java
@@ -0,0 +1,72 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
+import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class CameraCaptureCallbackTest {
+
+  private CameraCaptureCallback cameraCaptureCallback;
+  private CameraCaptureProperties mockCaptureProps;
+
+  @Before
+  public void setUp() {
+    CameraCaptureCallback.CameraCaptureStateListener mockCaptureStateListener =
+        mock(CameraCaptureCallback.CameraCaptureStateListener.class);
+    CaptureTimeoutsWrapper mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class);
+    mockCaptureProps = mock(CameraCaptureProperties.class);
+    cameraCaptureCallback =
+        CameraCaptureCallback.create(
+            mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps);
+  }
+
+  @Test
+  public void onCaptureProgressed_doesNotUpdateCameraCaptureProperties() {
+    CameraCaptureSession mockSession = mock(CameraCaptureSession.class);
+    CaptureRequest mockRequest = mock(CaptureRequest.class);
+    CaptureResult mockResult = mock(CaptureResult.class);
+
+    cameraCaptureCallback.onCaptureProgressed(mockSession, mockRequest, mockResult);
+
+    verify(mockCaptureProps, never()).setLastLensAperture(anyFloat());
+    verify(mockCaptureProps, never()).setLastSensorExposureTime(anyLong());
+    verify(mockCaptureProps, never()).setLastSensorSensitivity(anyInt());
+  }
+
+  @Test
+  public void onCaptureCompleted_updatesCameraCaptureProperties() {
+    CameraCaptureSession mockSession = mock(CameraCaptureSession.class);
+    CaptureRequest mockRequest = mock(CaptureRequest.class);
+    TotalCaptureResult mockResult = mock(TotalCaptureResult.class);
+    when(mockResult.get(CaptureResult.LENS_APERTURE)).thenReturn(1.0f);
+    when(mockResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(2L);
+    when(mockResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(3);
+
+    cameraCaptureCallback.onCaptureCompleted(mockSession, mockRequest, mockResult);
+
+    verify(mockCaptureProps, times(1)).setLastLensAperture(1.0f);
+    verify(mockCaptureProps, times(1)).setLastSensorExposureTime(2L);
+    verify(mockCaptureProps, times(1)).setLastSensorSensitivity(3);
+  }
+}
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index d88eb45..ea03ce5 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -661,6 +661,11 @@
       imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight];
       imageBuffer[@"format"] = @(videoFormat);
       imageBuffer[@"planes"] = planes;
+      imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]];
+      Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]);
+      Float64 nsExposureDuration = 1000000000 * exposureDuration;
+      imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration];
+      imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]];
 
       _imageStreamHandler.eventSink(imageBuffer);
 
diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart
index 411c7e8..43fa763 100644
--- a/packages/camera/camera/lib/src/camera_image.dart
+++ b/packages/camera/camera/lib/src/camera_image.dart
@@ -100,6 +100,9 @@
       : format = ImageFormat._fromPlatformData(data['format']),
         height = data['height'],
         width = data['width'],
+        lensAperture = data['lensAperture'],
+        sensorExposureTime = data['sensorExposureTime'],
+        sensorSensitivity = data['sensorSensitivity'],
         planes = List<Plane>.unmodifiable(data['planes']
             .map((dynamic planeData) => Plane._fromPlatformData(planeData)));
 
@@ -125,4 +128,15 @@
   ///
   /// The number of planes is determined by the format of the image.
   final List<Plane> planes;
+
+  /// The aperture settings for this image.
+  ///
+  /// Represented as an f-stop value.
+  final double? lensAperture;
+
+  /// The sensor exposure time for this image in nanoseconds.
+  final int? sensorExposureTime;
+
+  /// The sensor sensitivity in standard ISO arithmetic units.
+  final double? sensorSensitivity;
 }
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index a7c6a61..08d1e3e 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
   and streaming image buffers to dart.
 repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.0
+version: 0.9.1
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart
index 2d827d9..85d613f 100644
--- a/packages/camera/camera/test/camera_image_test.dart
+++ b/packages/camera/camera/test/camera_image_test.dart
@@ -18,6 +18,9 @@
         'format': 35,
         'height': 1,
         'width': 4,
+        'lensAperture': 1.8,
+        'sensorExposureTime': 9991324,
+        'sensorSensitivity': 92.0,
         'planes': [
           {
             'bytes': Uint8List.fromList([1, 2, 3, 4]),
@@ -41,6 +44,9 @@
         'format': 875704438,
         'height': 1,
         'width': 4,
+        'lensAperture': 1.8,
+        'sensorExposureTime': 9991324,
+        'sensorSensitivity': 92.0,
         'planes': [
           {
             'bytes': Uint8List.fromList([1, 2, 3, 4]),
@@ -61,6 +67,9 @@
         'format': 35,
         'height': 1,
         'width': 4,
+        'lensAperture': 1.8,
+        'sensorExposureTime': 9991324,
+        'sensorSensitivity': 92.0,
         'planes': [
           {
             'bytes': Uint8List.fromList([1, 2, 3, 4]),
@@ -81,6 +90,9 @@
         'format': 1111970369,
         'height': 1,
         'width': 4,
+        'lensAperture': 1.8,
+        'sensorExposureTime': 9991324,
+        'sensorSensitivity': 92.0,
         'planes': [
           {
             'bytes': Uint8List.fromList([1, 2, 3, 4]),
@@ -98,6 +110,9 @@
         'format': null,
         'height': 1,
         'width': 4,
+        'lensAperture': 1.8,
+        'sensorExposureTime': 9991324,
+        'sensorSensitivity': 92.0,
         'planes': [
           {
             'bytes': Uint8List.fromList([1, 2, 3, 4]),