diff --git a/.ci.yaml b/.ci.yaml
index 0b277d28053b..93e3d4035346 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -1472,6 +1472,7 @@ targets:
- name: Windows_arm64 windows-build_all_packages master
recipe: packages/packages
+ presubmit: false
timeout: 30
bringup: true # https://github.com/flutter/flutter/issues/134083
properties:
@@ -1506,6 +1507,7 @@ targets:
- name: Windows_arm64 windows-build_all_packages stable
recipe: packages/packages
+ presubmit: false
timeout: 30
bringup: true
properties:
diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version
index 359ddb64593e..ac3ccc411fdc 100644
--- a/.ci/flutter_master.version
+++ b/.ci/flutter_master.version
@@ -1 +1 @@
-064c340baf0e23790374f5b34ea067c2478e7fd1
+d12ba5c270d83c63cd3c1e89c1cd1f279bbb5696
diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version
index 845fae768cba..304f1de6baf4 100644
--- a/.ci/flutter_stable.version
+++ b/.ci/flutter_stable.version
@@ -1 +1 @@
-bae5e49bc2a867403c43b2aae2de8f8c33b037e4
+300451adae589accbece3490f4396f10bdf15e6e
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d9157022847f..d1f55572e699 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -395,6 +395,34 @@ updates:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+ - package-ecosystem: "gradle"
+ directory: "/packages/interactive_media_ads/android"
+ commit-message:
+ prefix: "[interactive_media_ads]"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "com.android.tools.build:gradle"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+ - dependency-name: "junit:junit"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+ - dependency-name: "org.mockito:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+ - dependency-name: "androidx.test:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/interactive_media_ads/example/android/app"
+ commit-message:
+ prefix: "[interactive_media_ads]"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/image_picker/image_picker/example/android/app"
commit-message:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index adf983fdc5e5..777e15bcc6e9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,13 +36,20 @@ jobs:
cd $GITHUB_WORKSPACE
# Checks out a copy of the repo.
- name: Check out code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+ uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633
with:
fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version.
- name: Set up tools
run: dart pub get
working-directory: ${{ github.workspace }}/script/tool
+ # Give some time for LUCI checks to start becoming populated.
+ # Because of latency in Github Webhooks, we need to wait for a while
+ # before being able to look at checks scheduled by LUCI.
+ - name: Give webhooks a minute
+ run: sleep 60s
+ shell: bash
+
# The next step waits for all tests, but when there are issues with the
# hooks it can take a long time for the tests to even be registered. If
# "Wait on all tests" runs before that happens, it will pass immediately
diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml
index 6f3299ad195d..2cf05fe701cd 100644
--- a/.github/workflows/scorecards-analysis.yml
+++ b/.github/workflows/scorecards-analysis.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v2.4.0
+ uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v2.4.0
with:
persist-credentials: false
@@ -49,6 +49,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@379614612a29c9e28f31f39a59013eb8012a51f0 # v1.0.26
+ uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v1.0.26
with:
sarif_file: results.sarif
diff --git a/CODEOWNERS b/CODEOWNERS
index 7ccddb125462..77f7adb749f2 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -23,6 +23,7 @@ packages/google_identity_services_web/** @ditman
packages/google_maps_flutter/** @stuartmorgan
packages/google_sign_in/** @stuartmorgan
packages/image_picker/** @tarrinneal
+packages/interactive_media_ads/** @bparrishMines
packages/in_app_purchase/** @bparrishMines
packages/local_auth/** @stuartmorgan
packages/metrics_center/** @keyonghan
@@ -88,8 +89,9 @@ packages/google_sign_in/google_sign_in_ios/** @vashworth
packages/image_picker/image_picker_ios/** @vashworth
packages/in_app_purchase/in_app_purchase_storekit/** @louisehsu
packages/ios_platform_images/** @jmagman
-packages/local_auth/local_auth_darwin/** @louisehsu
+packages/local_auth/local_auth_darwin/** @louisehsu
packages/path_provider/path_provider_foundation/** @jmagman
+packages/pigeon/**/ios/**/* @hellohuanlin
packages/pointer_interceptor/pointer_interceptor_ios/** @ditman
packages/quick_actions/quick_actions_ios/** @hellohuanlin
packages/shared_preferences/shared_preferences_foundation/** @tarrinneal
diff --git a/README.md b/README.md
index ac0078ff5f72..1996143171fb 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ These are the packages hosted in this repository:
| [google\_maps\_flutter](./packages/google_maps_flutter/) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | [![pub points](https://img.shields.io/pub/points/google_maps_flutter)](https://pub.dev/packages/google_maps_flutter/score) | [![popularity](https://img.shields.io/pub/popularity/google_maps_flutter)](https://pub.dev/packages/google_maps_flutter/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20maps?label=)](https://github.com/flutter/flutter/labels/p%3A%20maps) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20google_maps_flutter?label=)](https://github.com/flutter/packages/labels/p%3A%20google_maps_flutter) |
| [google\_sign\_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | [![pub points](https://img.shields.io/pub/points/google_sign_in)](https://pub.dev/packages/google_sign_in/score) | [![popularity](https://img.shields.io/pub/popularity/google_sign_in)](https://pub.dev/packages/google_sign_in/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20google_sign_in?label=)](https://github.com/flutter/flutter/labels/p%3A%20google_sign_in) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20google_sign_in?label=)](https://github.com/flutter/packages/labels/p%3A%20google_sign_in) |
| [image\_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | [![pub points](https://img.shields.io/pub/points/image_picker)](https://pub.dev/packages/image_picker/score) | [![popularity](https://img.shields.io/pub/popularity/image_picker)](https://pub.dev/packages/image_picker/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20image_picker?label=)](https://github.com/flutter/flutter/labels/p%3A%20image_picker) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20image_picker?label=)](https://github.com/flutter/packages/labels/p%3A%20image_picker) |
+| [interactive\_media\_ads](./packages/interactive_media_ads/) | [![pub package](https://img.shields.io/pub/v/interactive_media_ads.svg)](https://pub.dev/packages/interactive_media_ads) | [![pub points](https://img.shields.io/pub/points/interactive_media_ads)](https://pub.dev/packages/interactive_media_ads/score) | [![popularity](https://img.shields.io/pub/popularity/interactive_media_ads)](https://pub.dev/packages/interactive_media_ads/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20interactive_media_ads?label=)](https://github.com/flutter/flutter/labels/p%3A%20interactive_media_ads) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20interactive_media_ads?label=)](https://github.com/flutter/packages/labels/p%3A%20interactive_media_ads) |
| [in\_app\_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | [![pub points](https://img.shields.io/pub/points/in_app_purchase)](https://pub.dev/packages/in_app_purchase/score) | [![popularity](https://img.shields.io/pub/popularity/in_app_purchase)](https://pub.dev/packages/in_app_purchase/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20in_app_purchase?label=)](https://github.com/flutter/flutter/labels/p%3A%20in_app_purchase) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20in_app_purchase?label=)](https://github.com/flutter/packages/labels/p%3A%20in_app_purchase) |
| [ios\_platform\_images](./packages/ios_platform_images/) | [![pub package](https://img.shields.io/pub/v/ios_platform_images.svg)](https://pub.dev/packages/ios_platform_images) | [![pub points](https://img.shields.io/pub/points/ios_platform_images)](https://pub.dev/packages/ios_platform_images/score) | [![popularity](https://img.shields.io/pub/popularity/ios_platform_images)](https://pub.dev/packages/ios_platform_images/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20ios_platform_images?label=)](https://github.com/flutter/flutter/labels/p%3A%20ios_platform_images) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20ios_platform_images?label=)](https://github.com/flutter/packages/labels/p%3A%20ios_platform_images) |
| [local\_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | [![pub points](https://img.shields.io/pub/points/local_auth)](https://pub.dev/packages/local_auth/score) | [![popularity](https://img.shields.io/pub/popularity/local_auth)](https://pub.dev/packages/local_auth/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20local_auth?label=)](https://github.com/flutter/flutter/labels/p%3A%20local_auth) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20local_auth?label=)](https://github.com/flutter/packages/labels/p%3A%20local_auth) |
diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md
index d7bbc2bdecd5..82d27a401e85 100644
--- a/packages/camera/camera_android/CHANGELOG.md
+++ b/packages/camera/camera_android/CHANGELOG.md
@@ -1,7 +1,12 @@
-## NEXT
+## 0.10.8+18
+
+* Updates annotations lib to 1.7.1.
+
+## 0.10.8+17
* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1.
* Updates compileSdk version to 34.
+* Updates `README.md` to encourage developers to opt into `camera_android_camerax`.
## 0.10.8+16
diff --git a/packages/camera/camera_android/README.md b/packages/camera/camera_android/README.md
index 509d5b880226..31f2d66ae4e4 100644
--- a/packages/camera/camera_android/README.md
+++ b/packages/camera/camera_android/README.md
@@ -2,6 +2,12 @@
The Android implementation of [`camera`][1].
+*Note*: [`camera_android_camerax`][3] will become the default implementation of
+`camera` on Android by May 2024, so **we strongly encourage you to opt into it**
+by using [these instructions][4]. If any [limitations][5] of `camera_android_camerax`
+prevent you from using it or if you run into any problems, please report these
+issues under [`flutter/flutter`][5] with `[camerax]` in the title.
+
## Usage
This package is [endorsed][2], which means you can simply use `camera`
@@ -13,3 +19,6 @@ should add it to your `pubspec.yaml` as usual.
[1]: https://pub.dev/packages/camera
[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
+[3]: https://pub.dev/packages/camera_android_camerax
+[4]: https://pub.dev/packages/camera_android_camerax#usage
+[5]: https://pub.dev/packages/camera_android_camerax#limitations
diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle
index 824931536ec8..dfa21b72bb86 100644
--- a/packages/camera/camera_android/android/build.gradle
+++ b/packages/camera/camera_android/android/build.gradle
@@ -65,7 +65,7 @@ buildFeatures {
}
dependencies {
- implementation 'androidx.annotation:annotation:1.7.0'
+ implementation 'androidx.annotation:annotation:1.7.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-inline:5.0.0'
testImplementation 'androidx.test:core:1.4.0'
diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml
index 48a04d46ef30..e45547bc0a92 100644
--- a/packages/camera/camera_android/pubspec.yaml
+++ b/packages/camera/camera_android/pubspec.yaml
@@ -3,7 +3,7 @@ description: Android implementation of the camera plugin.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.10.8+16
+version: 0.10.8+18
environment:
sdk: ^3.1.0
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index ce4475ebf0fc..83ad40f8f5b5 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -1,5 +1,38 @@
-## NEXT
+## 0.6.2
+* Move integration_test dependency to test.
+
+## 0.6.1
+
+* Modifies resolution selection logic to use an `AspectRatioStrategy` for all aspect ratios supported by CameraX.
+* Adds `ResolutionFilter` to resolution selection logic to prioritize resolutions that match
+ the defined `ResolutionPreset`s.
+
+## 0.6.0+1
+
+* Updates `README.md` to encourage developers to opt into this implementation of the camera plugin.
+
+## 0.6.0
+
+* Implements `setFocusMode`, which makes this plugin reach feature parity with camera_android.
+* Fixes `setExposureCompensationIndex` return value to use index returned by CameraX.
+
+## 0.5.0+36
+
+* Implements `setExposureMode`.
+
+## 0.5.0+35
+
+* Modifies `CameraInitializedEvent` that is sent when the camera is initialized to indicate that the initial focus
+ and exposure modes are auto and that developers may set focus and exposure points.
+
+## 0.5.0+34
+
+* Implements `setFocusPoint`, `setExposurePoint`, and `setExposureOffset`.
+
+## 0.5.0+33
+
+* Fixes typo in `README.md`.
* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1.
## 0.5.0+32
diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md
index 3a2e49d7e60d..64a56f1a3b54 100644
--- a/packages/camera/camera_android_camerax/README.md
+++ b/packages/camera/camera_android_camerax/README.md
@@ -2,28 +2,24 @@
An Android implementation of [`camera`][1] that uses the [CameraX library][2].
-*Note*: This package is under development, so please note the
-[missing features and limitations](#missing-features-and-limitations), but
-otherwise feel free to try out the current implementation and provide any
-feedback by filing issues under [`flutter/flutter`][5] with `[camerax]` in
-the title, which will be actively triaged.
+*Note*: This implementation will become the default implementation of `camera`
+on Android by May 2024, so **we strongly encourage you to opt into it**
+by using [the instructions](#usage) below. If any of [the limitations](#limitations)
+prevent you from using `camera_android_camerax` or if you run into any problems,
+please report these issues under [`flutter/flutter`][5] with `[camerax]` in
+the title.
## Usage
-This package is [non-endorsed][3]; the endorsed Android implementation of `camera`
-is [`camera_android`][4]. To use this implementation of the plugin instead of
-`camera_android`, you will need to specify it in your `pubsepc.yaml` file as a
-dependency in addition to `camera`:
+To use this plugin instead of [`camera_android`][4], run
-```yaml
-dependencies:
- # ...along with your other dependencies
- camera: ^0.10.4
- camera_android_camerax: ^0.5.0
+```sh
+$ flutter pub add camera_android_camerax
```
-## Missing features and limitations
+from your project's root directory.
+## Limitations
### 240p resolution configuration for video recording
@@ -31,14 +27,6 @@ dependencies:
and thus, the plugin will fall back to 480p if configured with a
`ResolutionPreset`.
-### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\]
-
-`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.
-
-### Focus mode & point configuration \[[Issue #120467][120467]\]
-
-`setFocusMode` & `setFocusPoint` are unimplemented.
-
### Setting maximum duration and stream options for video capture
Calling `startVideoCapturing` with `VideoCaptureOptions` configured with
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
index b0e0493cbe2a..d281bbe65a8c 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java
@@ -27,6 +27,10 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
@VisibleForTesting @Nullable public ImageCaptureHostApiImpl imageCaptureHostApiImpl;
@VisibleForTesting @Nullable public CameraControlHostApiImpl cameraControlHostApiImpl;
@VisibleForTesting @Nullable public SystemServicesHostApiImpl systemServicesHostApiImpl;
+ @VisibleForTesting @Nullable public MeteringPointHostApiImpl meteringPointHostApiImpl;
+
+ @VisibleForTesting @Nullable
+ public Camera2CameraControlHostApiImpl camera2CameraControlHostApiImpl;
@VisibleForTesting
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;
@@ -119,6 +123,19 @@ public void setUp(
cameraControlHostApiImpl =
new CameraControlHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
+ camera2CameraControlHostApiImpl = new Camera2CameraControlHostApiImpl(instanceManager, context);
+ GeneratedCameraXLibrary.Camera2CameraControlHostApi.setup(
+ binaryMessenger, camera2CameraControlHostApiImpl);
+ GeneratedCameraXLibrary.CaptureRequestOptionsHostApi.setup(
+ binaryMessenger, new CaptureRequestOptionsHostApiImpl(instanceManager));
+ GeneratedCameraXLibrary.FocusMeteringActionHostApi.setup(
+ binaryMessenger, new FocusMeteringActionHostApiImpl(instanceManager));
+ GeneratedCameraXLibrary.FocusMeteringResultHostApi.setup(
+ binaryMessenger, new FocusMeteringResultHostApiImpl(instanceManager));
+ meteringPointHostApiImpl = new MeteringPointHostApiImpl(instanceManager);
+ GeneratedCameraXLibrary.MeteringPointHostApi.setup(binaryMessenger, meteringPointHostApiImpl);
+ GeneratedCameraXLibrary.ResolutionFilterHostApi.setup(
+ binaryMessenger, new ResolutionFilterHostApiImpl(instanceManager));
}
@Override
@@ -210,6 +227,9 @@ public void updateContext(@NonNull Context context) {
if (cameraControlHostApiImpl != null) {
cameraControlHostApiImpl.setContext(context);
}
+ if (camera2CameraControlHostApiImpl != null) {
+ camera2CameraControlHostApiImpl.setContext(context);
+ }
}
/** Sets {@code LifecycleOwner} that is used to control the lifecycle of the camera by CameraX. */
@@ -238,5 +258,8 @@ public void updateActivity(@Nullable Activity activity) {
if (deviceOrientationManagerHostApiImpl != null) {
deviceOrientationManagerHostApiImpl.setActivity(activity);
}
+ if (meteringPointHostApiImpl != null) {
+ meteringPointHostApiImpl.setActivity(activity);
+ }
}
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java
index ba26e91c9622..ba4318a8927f 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java
@@ -83,6 +83,12 @@ public void onSuccess(Void voidResult) {
}
public void onFailure(Throwable t) {
+ if (t instanceof CameraControl.OperationCanceledException) {
+ // Operation was canceled due to camera being closed or a new request was submitted, which
+ // is not actionable and should not block a new value from potentially being submitted.
+ result.success(null);
+ return;
+ }
result.error(t);
}
},
@@ -94,6 +100,9 @@ public void onFailure(Throwable t) {
*
*
Will trigger an auto focus action and enable auto focus/auto exposure/auto white balance
* metering regions.
+ *
+ *
Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
+ * canceled.
*/
public void startFocusAndMetering(
@NonNull CameraControl cameraControl,
@@ -117,6 +126,12 @@ public void onSuccess(FocusMeteringResult focusMeteringResult) {
}
public void onFailure(Throwable t) {
+ if (t instanceof CameraControl.OperationCanceledException) {
+ // Operation was canceled due to camera being closed or a new request was submitted, which
+ // is not actionable and should not block a new value from potentially being submitted.
+ result.success(null);
+ return;
+ }
result.error(t);
}
},
@@ -152,6 +167,9 @@ public void onFailure(Throwable t) {
*
The exposure compensation value set on the camera must be within the range of {@code
* ExposureState#getExposureCompensationRange()} for the current {@code ExposureState} for the
* call to succeed.
+ *
+ *
Will send a {@link GeneratedCameraXLibrary.Result} with a null result if operation was
+ * canceled.
*/
public void setExposureCompensationIndex(
@NonNull CameraControl cameraControl, @NonNull Long index, @NonNull Result result) {
@@ -166,6 +184,12 @@ public void onSuccess(Integer integerResult) {
}
public void onFailure(Throwable t) {
+ if (t instanceof CameraControl.OperationCanceledException) {
+ // Operation was canceled due to camera being closed or a new request was submitted, which
+ // is not actionable and should not block a new value from potentially being submitted.
+ result.success(null);
+ return;
+ }
result.error(t);
}
},
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java
index 6f8d8c91dda6..f76dd5422e72 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java
@@ -110,8 +110,8 @@ public void create(@NonNull Long identifier, @NonNull Map options)
Map decodedOptions =
new HashMap();
for (Map.Entry option : options.entrySet()) {
- decodedOptions.put(
- CaptureRequestKeySupportedType.values()[option.getKey().intValue()], option.getValue());
+ Integer index = ((Number) option.getKey()).intValue();
+ decodedOptions.put(CaptureRequestKeySupportedType.values()[index], option.getValue());
}
instanceManager.addDartCreatedInstance(proxy.create(decodedOptions), identifier);
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FocusMeteringActionHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FocusMeteringActionHostApiImpl.java
index 5eeedd15211c..dcda333c2e90 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FocusMeteringActionHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/FocusMeteringActionHostApiImpl.java
@@ -5,6 +5,7 @@
package io.flutter.plugins.camerax;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.MeteringPoint;
@@ -29,7 +30,9 @@ public class FocusMeteringActionHostApiImpl implements FocusMeteringActionHostAp
public static class FocusMeteringActionProxy {
/** Creates an instance of {@link FocusMeteringAction}. */
public @NonNull FocusMeteringAction create(
- @NonNull List meteringPoints, @NonNull List meteringPointModes) {
+ @NonNull List meteringPoints,
+ @NonNull List meteringPointModes,
+ @Nullable Boolean disableAutoCancel) {
if (meteringPoints.size() >= 1 && meteringPoints.size() != meteringPointModes.size()) {
throw new IllegalArgumentException(
"One metering point must be specified and the number of specified metering points must match the number of specified metering point modes.");
@@ -59,6 +62,10 @@ public static class FocusMeteringActionProxy {
}
}
+ if (disableAutoCancel != null && disableAutoCancel == true) {
+ focusMeteringActionBuilder.disableAutoCancel();
+ }
+
return focusMeteringActionBuilder.build();
}
@@ -100,7 +107,9 @@ public FocusMeteringActionHostApiImpl(@NonNull InstanceManager instanceManager)
@Override
public void create(
- @NonNull Long identifier, @NonNull List meteringPointInfos) {
+ @NonNull Long identifier,
+ @NonNull List meteringPointInfos,
+ @Nullable Boolean disableAutoCancel) {
final List meteringPoints = new ArrayList();
final List meteringPointModes = new ArrayList();
for (MeteringPointInfo meteringPointInfo : meteringPointInfos) {
@@ -110,6 +119,6 @@ public void create(
}
instanceManager.addDartCreatedInstance(
- proxy.create(meteringPoints, meteringPointModes), identifier);
+ proxy.create(meteringPoints, meteringPointModes, disableAutoCancel), identifier);
}
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
index 08f8ea2da9ff..fa6be7920961 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
@@ -83,14 +83,14 @@ private CameraStateType(final int index) {
* If you need to add another type to support a type S to use a LiveData in this plugin,
* ensure the following is done on the Dart side:
*
- *
* In `../lib/src/live_data.dart`, add new cases for S in
+ *
* In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in
* `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of type S from a
* LiveData instance and in `LiveDataFlutterApiImpl#create` to create the expected type of
* LiveData when requested.
*
*
On the native side, ensure the following is done:
*
- *
* Update `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
+ *
* Make sure `LiveDataHostApiImpl#getValue` is updated to properly return identifiers for
* instances of type S. * Update `ObserverFlutterApiWrapper#onChanged` to properly handle
* receiving calls with instances of type S if a LiveData instance is observed.
*/
@@ -146,6 +146,24 @@ private VideoResolutionFallbackRule(final int index) {
}
}
+ /**
+ * The types of capture request options this plugin currently supports.
+ *
+ *
If you need to add another option to support, ensure the following is done on the Dart side:
+ *
+ *
* In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this
+ * option in `_CaptureRequestOptionsHostApiImpl#createFromInstances` to create the expected Map
+ * entry of option key index and value to send to the native side.
+ *
+ *
On the native side, ensure the following is done:
+ *
+ *
* Update `CaptureRequestOptionsHostApiImpl#create` to set the correct `CaptureRequest` key
+ * with a valid value type for this option.
+ *
+ *
See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest for the
+ * sorts of capture request options that can be supported via CameraX's interoperability with
+ * Camera2.
+ */
public enum CaptureRequestKeySupportedType {
CONTROL_AE_LOCK(0);
@@ -2489,6 +2507,7 @@ public interface ResolutionSelectorHostApi {
void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
+ @Nullable Long resolutionSelectorIdentifier,
@Nullable Long aspectRatioStrategyIdentifier);
/** The codec used by ResolutionSelectorHostApi. */
@@ -2512,13 +2531,17 @@ static void setup(
ArrayList args = (ArrayList) message;
Number identifierArg = (Number) args.get(0);
Number resolutionStrategyIdentifierArg = (Number) args.get(1);
- Number aspectRatioStrategyIdentifierArg = (Number) args.get(2);
+ Number resolutionSelectorIdentifierArg = (Number) args.get(2);
+ Number aspectRatioStrategyIdentifierArg = (Number) args.get(3);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
(resolutionStrategyIdentifierArg == null)
? null
: resolutionStrategyIdentifierArg.longValue(),
+ (resolutionSelectorIdentifierArg == null)
+ ? null
+ : resolutionSelectorIdentifierArg.longValue(),
(aspectRatioStrategyIdentifierArg == null)
? null
: aspectRatioStrategyIdentifierArg.longValue());
@@ -3778,7 +3801,10 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface FocusMeteringActionHostApi {
- void create(@NonNull Long identifier, @NonNull List meteringPointInfos);
+ void create(
+ @NonNull Long identifier,
+ @NonNull List meteringPointInfos,
+ @Nullable Boolean disableAutoCancel);
/** The codec used by FocusMeteringActionHostApi. */
static @NonNull MessageCodec getCodec() {
@@ -3804,10 +3830,12 @@ static void setup(
Number identifierArg = (Number) args.get(0);
List meteringPointInfosArg =
(List) args.get(1);
+ Boolean disableAutoCancelArg = (Boolean) args.get(2);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
- meteringPointInfosArg);
+ meteringPointInfosArg,
+ disableAutoCancelArg);
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList wrappedError = wrapError(exception);
@@ -3899,7 +3927,11 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) {
public interface MeteringPointHostApi {
void create(
- @NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size);
+ @NonNull Long identifier,
+ @NonNull Double x,
+ @NonNull Double y,
+ @Nullable Double size,
+ @NonNull Long cameraInfoId);
@NonNull
Double getDefaultPointSize();
@@ -3927,12 +3959,14 @@ static void setup(
Double xArg = (Double) args.get(1);
Double yArg = (Double) args.get(2);
Double sizeArg = (Double) args.get(3);
+ Number cameraInfoIdArg = (Number) args.get(4);
try {
api.create(
(identifierArg == null) ? null : identifierArg.longValue(),
xArg,
yArg,
- sizeArg);
+ sizeArg,
+ (cameraInfoIdArg == null) ? null : cameraInfoIdArg.longValue());
wrapped.add(0, null);
} catch (Throwable exception) {
ArrayList wrappedError = wrapError(exception);
@@ -4160,4 +4194,77 @@ public void error(Throwable error) {
}
}
}
+
+ private static class ResolutionFilterHostApiCodec extends StandardMessageCodec {
+ public static final ResolutionFilterHostApiCodec INSTANCE = new ResolutionFilterHostApiCodec();
+
+ private ResolutionFilterHostApiCodec() {}
+
+ @Override
+ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
+ switch (type) {
+ case (byte) 128:
+ return ResolutionInfo.fromList((ArrayList) readValue(buffer));
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+
+ @Override
+ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
+ if (value instanceof ResolutionInfo) {
+ stream.write(128);
+ writeValue(stream, ((ResolutionInfo) value).toList());
+ } else {
+ super.writeValue(stream, value);
+ }
+ }
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface ResolutionFilterHostApi {
+
+ void createWithOnePreferredSize(
+ @NonNull Long identifier, @NonNull ResolutionInfo preferredResolution);
+
+ /** The codec used by ResolutionFilterHostApi. */
+ static @NonNull MessageCodec getCodec() {
+ return ResolutionFilterHostApiCodec.INSTANCE;
+ }
+ /**
+ * Sets up an instance of `ResolutionFilterHostApi` to handle messages through the
+ * `binaryMessenger`.
+ */
+ static void setup(
+ @NonNull BinaryMessenger binaryMessenger, @Nullable ResolutionFilterHostApi api) {
+ {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.ResolutionFilterHostApi.createWithOnePreferredSize",
+ getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ ArrayList wrapped = new ArrayList();
+ ArrayList args = (ArrayList) message;
+ Number identifierArg = (Number) args.get(0);
+ ResolutionInfo preferredResolutionArg = (ResolutionInfo) args.get(1);
+ try {
+ api.createWithOnePreferredSize(
+ (identifierArg == null) ? null : identifierArg.longValue(),
+ preferredResolutionArg);
+ wrapped.add(0, null);
+ } catch (Throwable exception) {
+ ArrayList wrappedError = wrapError(exception);
+ wrapped = wrappedError;
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java
index 56ffcfb2e391..36306253ed13 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/MeteringPointHostApiImpl.java
@@ -4,13 +4,20 @@
package io.flutter.plugins.camerax;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.view.Display;
+import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
-import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.MeteringPointHostApi;
+import java.util.Objects;
/**
* Host API implementation for {@link MeteringPoint}.
@@ -25,17 +32,32 @@ public class MeteringPointHostApiImpl implements MeteringPointHostApi {
/** Proxy for constructor and static methods of {@link MeteringPoint}. */
@VisibleForTesting
public static class MeteringPointProxy {
+ Activity activity;
/**
* Creates a surface oriented {@link MeteringPoint} with the specified x, y, and size.
*
- * A {@link SurfaceOrientedMeteringPointFactory} is used to construct the {@link
- * MeteringPoint} because underlying the camera preview that this plugin uses is a Flutter
- * texture that is backed by a {@link Surface} created by the Flutter Android embedding.
+ *
A {@link DisplayOrientedMeteringPointFactory} is used to construct the {@link
+ * MeteringPoint} because this factory handles the transformation of specified coordinates based
+ * on camera information and the device orientation automatically.
*/
@NonNull
- public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Double size) {
- SurfaceOrientedMeteringPointFactory factory = getSurfaceOrientedMeteringPointFactory(1f, 1f);
+ public MeteringPoint create(
+ @NonNull Double x,
+ @NonNull Double y,
+ @Nullable Double size,
+ @NonNull CameraInfo cameraInfo) {
+ Display display = null;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ display = activity.getDisplay();
+ } else {
+ display = getDefaultDisplayForAndroidVersionBelowR(activity);
+ }
+
+ DisplayOrientedMeteringPointFactory factory =
+ getDisplayOrientedMeteringPointFactory(display, cameraInfo, 1f, 1f);
+
if (size == null) {
return factory.createPoint(x.floatValue(), y.floatValue());
} else {
@@ -43,11 +65,18 @@ public MeteringPoint create(@NonNull Double x, @NonNull Double y, @Nullable Doub
}
}
+ @NonNull
+ @SuppressWarnings("deprecation")
+ private Display getDefaultDisplayForAndroidVersionBelowR(@NonNull Activity activity) {
+ return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ }
+
@VisibleForTesting
@NonNull
- public SurfaceOrientedMeteringPointFactory getSurfaceOrientedMeteringPointFactory(
- float width, float height) {
- return new SurfaceOrientedMeteringPointFactory(width, height);
+ public DisplayOrientedMeteringPointFactory getDisplayOrientedMeteringPointFactory(
+ @NonNull Display display, @NonNull CameraInfo cameraInfo, float width, float height) {
+ return new DisplayOrientedMeteringPointFactory(display, cameraInfo, width, height);
}
/**
@@ -81,10 +110,23 @@ public MeteringPointHostApiImpl(@NonNull InstanceManager instanceManager) {
this.proxy = proxy;
}
+ public void setActivity(@NonNull Activity activity) {
+ this.proxy.activity = activity;
+ }
+
@Override
public void create(
- @NonNull Long identifier, @NonNull Double x, @NonNull Double y, @Nullable Double size) {
- MeteringPoint meteringPoint = proxy.create(x, y, size);
+ @NonNull Long identifier,
+ @NonNull Double x,
+ @NonNull Double y,
+ @Nullable Double size,
+ @NonNull Long cameraInfoId) {
+ MeteringPoint meteringPoint =
+ proxy.create(
+ x,
+ y,
+ size,
+ (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId)));
instanceManager.addDartCreatedInstance(meteringPoint, identifier);
}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionFilterHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionFilterHostApiImpl.java
new file mode 100644
index 000000000000..b2c3c9e69123
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionFilterHostApiImpl.java
@@ -0,0 +1,94 @@
+// 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.camerax;
+
+import android.util.Size;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionFilterHostApi;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
+import java.util.List;
+
+/**
+ * Host API implementation for {@link ResolutionFilter}.
+ *
+ *
This class handles instantiating and adding native object instances that are attached to a
+ * Dart instance or handle method calls on the associated native class or an instance of the class.
+ */
+public class ResolutionFilterHostApiImpl implements ResolutionFilterHostApi {
+ private final InstanceManager instanceManager;
+ private final ResolutionFilterFactory resolutionFilterFactory;
+
+ /**
+ * Proxy for constructing {@link ResolutionFilter}s with particular attributes, as detailed by
+ * documentation below.
+ */
+ @VisibleForTesting
+ public static class ResolutionFilterFactory {
+ /**
+ * Creates an instance of {@link ResolutionFilter} that moves the {@code preferredSize} to the
+ * front of the list of supported resolutions so that it can be prioritized by CameraX.
+ *
+ *
If the preferred {@code Size} is not found, then this creates a {@link ResolutionFilter}
+ * that leaves the priority of supported resolutions unadjusted.
+ */
+ @NonNull
+ public ResolutionFilter createWithOnePreferredSize(@NonNull Size preferredSize) {
+ return new ResolutionFilter() {
+ @Override
+ @NonNull
+ public List filter(@NonNull List supportedSizes, int rotationDegrees) {
+ int preferredSizeIndex = supportedSizes.indexOf(preferredSize);
+
+ if (preferredSizeIndex > -1) {
+ supportedSizes.remove(preferredSizeIndex);
+ supportedSizes.add(0, preferredSize);
+ }
+
+ return supportedSizes;
+ }
+ };
+ }
+ }
+
+ /**
+ * Constructs a {@link ResolutionFilterHostApiImpl}.
+ *
+ * @param instanceManager maintains instances stored to communicate with attached Dart objects
+ */
+ public ResolutionFilterHostApiImpl(@NonNull InstanceManager instanceManager) {
+ this(instanceManager, new ResolutionFilterFactory());
+ }
+
+ /**
+ * Constructs a {@link ResolutionFilterHostApiImpl}.
+ *
+ * @param instanceManager maintains instances stored to communicate with attached Dart objects
+ * @param resolutionFilterFactory proxy for constructing different kinds of {@link
+ * ResolutionFilter}s
+ */
+ @VisibleForTesting
+ ResolutionFilterHostApiImpl(
+ @NonNull InstanceManager instanceManager,
+ @NonNull ResolutionFilterFactory resolutionFilterFactory) {
+ this.instanceManager = instanceManager;
+ this.resolutionFilterFactory = resolutionFilterFactory;
+ }
+
+ /**
+ * Creates a {@link ResolutionFilter} that prioritizes the specified {@code preferredResolution}
+ * over all other resolutions.
+ */
+ @Override
+ public void createWithOnePreferredSize(
+ @NonNull Long identifier, @NonNull ResolutionInfo preferredResolution) {
+ Size preferredSize =
+ new Size(
+ preferredResolution.getWidth().intValue(), preferredResolution.getHeight().intValue());
+ instanceManager.addDartCreatedInstance(
+ resolutionFilterFactory.createWithOnePreferredSize(preferredSize), identifier);
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionSelectorHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionSelectorHostApiImpl.java
index 4aa11cd593f2..0a5fe750d163 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionSelectorHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ResolutionSelectorHostApiImpl.java
@@ -8,6 +8,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionSelectorHostApi;
@@ -30,7 +31,8 @@ public static class ResolutionSelectorProxy {
@NonNull
public ResolutionSelector create(
@Nullable ResolutionStrategy resolutionStrategy,
- @Nullable AspectRatioStrategy aspectRatioStrategy) {
+ @Nullable AspectRatioStrategy aspectRatioStrategy,
+ @Nullable ResolutionFilter resolutionFilter) {
final ResolutionSelector.Builder builder = new ResolutionSelector.Builder();
if (resolutionStrategy != null) {
builder.setResolutionStrategy(resolutionStrategy);
@@ -38,6 +40,9 @@ public ResolutionSelector create(
if (aspectRatioStrategy != null) {
builder.setAspectRatioStrategy(aspectRatioStrategy);
}
+ if (resolutionFilter != null) {
+ builder.setResolutionFilter(resolutionFilter);
+ }
return builder.build();
}
}
@@ -65,13 +70,14 @@ public ResolutionSelectorHostApiImpl(@NonNull InstanceManager instanceManager) {
}
/**
- * Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy} and {@link
- * AspectRatio} that have the identifiers specified if provided.
+ * Creates a {@link ResolutionSelector} instance with the {@link ResolutionStrategy}, {@link
+ * ResolutionFilter}, and {@link AspectRatio} that have the identifiers specified if provided.
*/
@Override
public void create(
@NonNull Long identifier,
@Nullable Long resolutionStrategyIdentifier,
+ @Nullable Long resolutionFilterIdentifier,
@Nullable Long aspectRatioStrategyIdentifier) {
instanceManager.addDartCreatedInstance(
proxy.create(
@@ -81,7 +87,10 @@ public void create(
aspectRatioStrategyIdentifier == null
? null
: Objects.requireNonNull(
- instanceManager.getInstance(aspectRatioStrategyIdentifier))),
+ instanceManager.getInstance(aspectRatioStrategyIdentifier)),
+ resolutionFilterIdentifier == null
+ ? null
+ : Objects.requireNonNull(instanceManager.getInstance(resolutionFilterIdentifier))),
identifier);
}
}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java
index 8c9daf8e0143..fcca0f8eb0bc 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java
@@ -92,6 +92,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(SystemServicesHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
+ final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
+ mock(MeteringPointHostApiImpl.class);
final ArgumentCaptor permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);
@@ -103,6 +105,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
+ plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onAttachedToActivity(activityPluginBinding);
@@ -110,6 +113,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
// Check Activity references are set.
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
+ verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);
// Check permissions registry reference is set.
verify(mockSystemServicesHostApiImpl)
@@ -129,11 +133,14 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(SystemServicesHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
+ final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
+ mock(MeteringPointHostApiImpl.class);
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
+ plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
@@ -142,6 +149,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
verify(mockSystemServicesHostApiImpl).setActivity(null);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
+ verify(mockMeteringPointHostApiImpl).setActivity(null);
}
@Test
@@ -161,6 +169,8 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
mock(ImageAnalysisHostApiImpl.class);
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
mock(CameraControlHostApiImpl.class);
+ final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
+ mock(Camera2CameraControlHostApiImpl.class);
when(flutterPluginBinding.getApplicationContext()).thenReturn(mockContext);
@@ -172,6 +182,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
+ plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
@@ -183,6 +194,7 @@ public void onAttachedToActivity_setsActivityAsNeededAndPermissionsRegistry() {
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
+ verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}
@Test
@@ -251,6 +263,10 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
mock(CameraControlHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
+ final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
+ mock(Camera2CameraControlHostApiImpl.class);
+ final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
+ mock(MeteringPointHostApiImpl.class);
final ArgumentCaptor permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);
@@ -265,7 +281,9 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
+ plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
+ plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onReattachedToActivityForConfigChanges(activityPluginBinding);
@@ -273,6 +291,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
// Check Activity references are set.
verify(mockSystemServicesHostApiImpl).setActivity(mockActivity);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(mockActivity);
+ verify(mockMeteringPointHostApiImpl).setActivity(mockActivity);
// Check Activity as Context references are set.
verify(mockProcessCameraProviderHostApiImpl).setContext(mockActivity);
@@ -282,6 +301,7 @@ public void onReattachedToActivityForConfigChanges_setsActivityAndPermissionsReg
verify(mockImageCaptureHostApiImpl).setContext(mockActivity);
verify(mockImageAnalysisHostApiImpl).setContext(mockActivity);
verify(mockCameraControlHostApiImpl).setContext(mockActivity);
+ verify(mockCamera2CameraControlHostApiImpl).setContext(mockActivity);
// Check permissions registry reference is set.
verify(mockSystemServicesHostApiImpl)
@@ -300,11 +320,14 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
final LiveDataHostApiImpl mockLiveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
final DeviceOrientationManagerHostApiImpl mockDeviceOrientationManagerHostApiImpl =
mock(DeviceOrientationManagerHostApiImpl.class);
+ final MeteringPointHostApiImpl mockMeteringPointHostApiImpl =
+ mock(MeteringPointHostApiImpl.class);
plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl;
plugin.systemServicesHostApiImpl = mockSystemServicesHostApiImpl;
plugin.deviceOrientationManagerHostApiImpl = mockDeviceOrientationManagerHostApiImpl;
+ plugin.meteringPointHostApiImpl = mockMeteringPointHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivityForConfigChanges();
@@ -313,6 +336,7 @@ public void onDetachedFromActivity_removesReferencesToActivityPluginBindingAndAc
verify(mockLiveDataHostApiImpl).setLifecycleOwner(null);
verify(mockSystemServicesHostApiImpl).setActivity(null);
verify(mockDeviceOrientationManagerHostApiImpl).setActivity(null);
+ verify(mockMeteringPointHostApiImpl).setActivity(null);
}
@Test
@@ -331,6 +355,8 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
final ImageCaptureHostApiImpl mockImageCaptureHostApiImpl = mock(ImageCaptureHostApiImpl.class);
final CameraControlHostApiImpl mockCameraControlHostApiImpl =
mock(CameraControlHostApiImpl.class);
+ final Camera2CameraControlHostApiImpl mockCamera2CameraControlHostApiImpl =
+ mock(Camera2CameraControlHostApiImpl.class);
final ArgumentCaptor permissionsRegistryCaptor =
ArgumentCaptor.forClass(PermissionsRegistry.class);
@@ -344,6 +370,7 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
plugin.imageAnalysisHostApiImpl = mockImageAnalysisHostApiImpl;
plugin.cameraControlHostApiImpl = mockCameraControlHostApiImpl;
plugin.liveDataHostApiImpl = mock(LiveDataHostApiImpl.class);
+ plugin.camera2CameraControlHostApiImpl = mockCamera2CameraControlHostApiImpl;
plugin.onAttachedToEngine(flutterPluginBinding);
plugin.onDetachedFromActivity();
@@ -355,5 +382,6 @@ public void onDetachedFromActivity_setsContextReferencesBasedOnFlutterPluginBind
verify(mockImageCaptureHostApiImpl).setContext(mockContext);
verify(mockImageAnalysisHostApiImpl).setContext(mockContext);
verify(mockCameraControlHostApiImpl).setContext(mockContext);
+ verify(mockCamera2CameraControlHostApiImpl).setContext(mockContext);
}
}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java
index 42ace7a51fb5..a0fe8f977ee0 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java
@@ -149,6 +149,22 @@ public void setZoomRatio_setsZoomAsExpected() {
failedSetZoomRatioCallback.onFailure(testThrowable);
verify(failedMockResult).error(testThrowable);
+
+ // Test response to canceled operation.
+ @SuppressWarnings("unchecked")
+ final GeneratedCameraXLibrary.Result canceledOpResult =
+ mock(GeneratedCameraXLibrary.Result.class);
+ final CameraControl.OperationCanceledException canceledOpThrowable =
+ mock(CameraControl.OperationCanceledException.class);
+ cameraControlHostApiImpl.setZoomRatio(cameraControlIdentifier, zoomRatio, canceledOpResult);
+ mockedFutures.verify(
+ () -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any()));
+ mockedFutures.clearInvocations();
+
+ FutureCallback canceledOpCallback = futureCallbackCaptor.getValue();
+
+ canceledOpCallback.onFailure(canceledOpThrowable);
+ verify(canceledOpResult).success(null);
}
}
@@ -212,6 +228,25 @@ public void startFocusAndMetering_startsFocusAndMeteringAsExpected() {
failedCallback.onFailure(testThrowable);
verify(failedMockResult).error(testThrowable);
+
+ // Test response to canceled operation.
+ @SuppressWarnings("unchecked")
+ final GeneratedCameraXLibrary.Result canceledOpResult =
+ mock(GeneratedCameraXLibrary.Result.class);
+ final CameraControl.OperationCanceledException canceledOpThrowable =
+ mock(CameraControl.OperationCanceledException.class);
+ cameraControlHostApiImpl.startFocusAndMetering(
+ cameraControlIdentifier, mockActionId, canceledOpResult);
+ mockedFutures.verify(
+ () ->
+ Futures.addCallback(
+ eq(startFocusAndMeteringFuture), futureCallbackCaptor.capture(), any()));
+ mockedFutures.clearInvocations();
+
+ FutureCallback canceledOpCallback = futureCallbackCaptor.getValue();
+
+ canceledOpCallback.onFailure(canceledOpThrowable);
+ verify(canceledOpResult).success(null);
}
}
@@ -326,6 +361,25 @@ public void setExposureCompensationIndex_setsExposureCompensationIndexAsExpected
failedCallback.onFailure(testThrowable);
verify(failedMockResult).error(testThrowable);
+
+ // Test response to canceled operation.
+ @SuppressWarnings("unchecked")
+ final GeneratedCameraXLibrary.Result canceledOpResult =
+ mock(GeneratedCameraXLibrary.Result.class);
+ final CameraControl.OperationCanceledException canceledOpThrowable =
+ mock(CameraControl.OperationCanceledException.class);
+ cameraControlHostApiImpl.setExposureCompensationIndex(
+ cameraControlIdentifier, index, canceledOpResult);
+ mockedFutures.verify(
+ () ->
+ Futures.addCallback(
+ eq(setExposureCompensationIndexFuture), futureCallbackCaptor.capture(), any()));
+ mockedFutures.clearInvocations();
+
+ FutureCallback canceledOpCallback = futureCallbackCaptor.getValue();
+
+ canceledOpCallback.onFailure(canceledOpThrowable);
+ verify(canceledOpResult).success(null);
}
}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FocusMeteringActionTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FocusMeteringActionTest.java
index 64db80541619..6d96d3036cd7 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FocusMeteringActionTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/FocusMeteringActionTest.java
@@ -6,6 +6,7 @@
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -43,7 +44,7 @@ public void tearDown() {
}
@Test
- public void hostApiCreatecreatesExpectedFocusMeteringActionWithInitialPointThatHasMode() {
+ public void hostApiCreate_createsExpectedFocusMeteringActionWithInitialPointThatHasMode() {
FocusMeteringActionHostApiImpl.FocusMeteringActionProxy proxySpy =
spy(new FocusMeteringActionHostApiImpl.FocusMeteringActionProxy());
FocusMeteringActionHostApiImpl hostApi =
@@ -89,7 +90,7 @@ public void hostApiCreatecreatesExpectedFocusMeteringActionWithInitialPointThatH
List mockMeteringPointInfos =
Arrays.asList(fakeMeteringPointInfo1, fakeMeteringPointInfo2, fakeMeteringPointInfo3);
- hostApi.create(focusMeteringActionIdentifier, mockMeteringPointInfos);
+ hostApi.create(focusMeteringActionIdentifier, mockMeteringPointInfos, null);
verify(mockFocusMeteringActionBuilder).addPoint(mockMeteringPoint2, mockMeteringPoint2Mode);
verify(mockFocusMeteringActionBuilder).addPoint(mockMeteringPoint3);
@@ -98,7 +99,8 @@ public void hostApiCreatecreatesExpectedFocusMeteringActionWithInitialPointThatH
}
@Test
- public void hostApiCreatecreatesExpectedFocusMeteringActionWithInitialPointThatDoesNotHaveMode() {
+ public void
+ hostApiCreate_createsExpectedFocusMeteringActionWithInitialPointThatDoesNotHaveMode() {
FocusMeteringActionHostApiImpl.FocusMeteringActionProxy proxySpy =
spy(new FocusMeteringActionHostApiImpl.FocusMeteringActionProxy());
FocusMeteringActionHostApiImpl hostApi =
@@ -142,11 +144,49 @@ public void hostApiCreatecreatesExpectedFocusMeteringActionWithInitialPointThatD
List mockMeteringPointInfos =
Arrays.asList(fakeMeteringPointInfo1, fakeMeteringPointInfo2, fakeMeteringPointInfo3);
- hostApi.create(focusMeteringActionIdentifier, mockMeteringPointInfos);
+ hostApi.create(focusMeteringActionIdentifier, mockMeteringPointInfos, null);
verify(mockFocusMeteringActionBuilder).addPoint(mockMeteringPoint2, mockMeteringPoint2Mode);
verify(mockFocusMeteringActionBuilder).addPoint(mockMeteringPoint3);
assertEquals(
testInstanceManager.getInstance(focusMeteringActionIdentifier), focusMeteringAction);
}
+
+ @Test
+ public void hostApiCreate_disablesAutoCancelAsExpected() {
+ FocusMeteringActionHostApiImpl.FocusMeteringActionProxy proxySpy =
+ spy(new FocusMeteringActionHostApiImpl.FocusMeteringActionProxy());
+ FocusMeteringActionHostApiImpl hostApi =
+ new FocusMeteringActionHostApiImpl(testInstanceManager, proxySpy);
+
+ FocusMeteringAction.Builder mockFocusMeteringActionBuilder =
+ mock(FocusMeteringAction.Builder.class);
+ final MeteringPoint mockMeteringPoint = mock(MeteringPoint.class);
+ final Long mockMeteringPointId = 47L;
+
+ MeteringPointInfo fakeMeteringPointInfo =
+ new MeteringPointInfo.Builder()
+ .setMeteringPointId(mockMeteringPointId)
+ .setMeteringMode(null)
+ .build();
+
+ testInstanceManager.addDartCreatedInstance(mockMeteringPoint, mockMeteringPointId);
+
+ when(proxySpy.getFocusMeteringActionBuilder(mockMeteringPoint))
+ .thenReturn(mockFocusMeteringActionBuilder);
+ when(mockFocusMeteringActionBuilder.build()).thenReturn(focusMeteringAction);
+
+ List mockMeteringPointInfos = Arrays.asList(fakeMeteringPointInfo);
+
+ // Test not disabling auto cancel.
+ hostApi.create(73L, mockMeteringPointInfos, /* disableAutoCancel */ null);
+ verify(mockFocusMeteringActionBuilder, never()).disableAutoCancel();
+
+ hostApi.create(74L, mockMeteringPointInfos, /* disableAutoCancel */ false);
+ verify(mockFocusMeteringActionBuilder, never()).disableAutoCancel();
+
+ // Test disabling auto cancel.
+ hostApi.create(75L, mockMeteringPointInfos, /* disableAutoCancel */ true);
+ verify(mockFocusMeteringActionBuilder).disableAutoCancel();
+ }
}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java
index f245eb53124e..0734f6ba6a9d 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/MeteringPointTest.java
@@ -10,21 +10,30 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.Activity;
+import android.content.Context;
+import android.view.Display;
+import android.view.WindowManager;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
-import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import io.flutter.plugin.common.BinaryMessenger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+@RunWith(RobolectricTestRunner.class)
public class MeteringPointTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@@ -44,49 +53,158 @@ public void tearDown() {
}
@Test
- public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified() {
- MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
+ @Config(sdk = 30)
+ public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified_AboveAndroid30() {
+ final MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
spy(new MeteringPointHostApiImpl.MeteringPointProxy());
- MeteringPointHostApiImpl hostApi = new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
+ final MeteringPointHostApiImpl hostApi =
+ new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
final Long meteringPointIdentifier = 78L;
- final Float x = 0.3f;
- final Float y = 0.2f;
- final Float size = 6f;
+ final Float x = 0.25f;
+ final Float y = 0.18f;
+ final Float size = 0.6f;
final Float surfaceWidth = 1f;
final Float surfaceHeight = 1f;
- SurfaceOrientedMeteringPointFactory mockSurfaceOrientedMeteringPointFactory =
- mock(SurfaceOrientedMeteringPointFactory.class);
-
- when(proxySpy.getSurfaceOrientedMeteringPointFactory(surfaceWidth, surfaceHeight))
- .thenReturn(mockSurfaceOrientedMeteringPointFactory);
- when(mockSurfaceOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint);
-
- hostApi.create(meteringPointIdentifier, x.doubleValue(), y.doubleValue(), size.doubleValue());
-
- verify(mockSurfaceOrientedMeteringPointFactory).createPoint(x, y, size);
+ final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory =
+ mock(DisplayOrientedMeteringPointFactory.class);
+ final Activity mockActivity = mock(Activity.class);
+ final Display mockDisplay = mock(Display.class);
+ final CameraInfo mockCameraInfo = mock(CameraInfo.class);
+ final long mockCameraInfoId = 55L;
+
+ hostApi.setActivity(mockActivity);
+ testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId);
+
+ when(mockActivity.getDisplay()).thenReturn(mockDisplay);
+ when(proxySpy.getDisplayOrientedMeteringPointFactory(
+ mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight))
+ .thenReturn(mockDisplayOrientedMeteringPointFactory);
+ when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint);
+
+ hostApi.create(
+ meteringPointIdentifier,
+ x.doubleValue(),
+ y.doubleValue(),
+ size.doubleValue(),
+ mockCameraInfoId);
+
+ verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y, size);
assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint);
}
@Test
- public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified() {
- MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
+ @Config(sdk = 29)
+ @SuppressWarnings("deprecation")
+ public void hostApiCreate_createsExpectedMeteringPointWithSizeSpecified_BelowAndroid30() {
+ final MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
spy(new MeteringPointHostApiImpl.MeteringPointProxy());
- MeteringPointHostApiImpl hostApi = new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
+ final MeteringPointHostApiImpl hostApi =
+ new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
final Long meteringPointIdentifier = 78L;
final Float x = 0.3f;
final Float y = 0.2f;
+ final Float size = 6f;
final Float surfaceWidth = 1f;
final Float surfaceHeight = 1f;
- SurfaceOrientedMeteringPointFactory mockSurfaceOrientedMeteringPointFactory =
- mock(SurfaceOrientedMeteringPointFactory.class);
-
- when(proxySpy.getSurfaceOrientedMeteringPointFactory(surfaceWidth, surfaceHeight))
- .thenReturn(mockSurfaceOrientedMeteringPointFactory);
- when(mockSurfaceOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint);
+ final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory =
+ mock(DisplayOrientedMeteringPointFactory.class);
+ final Activity mockActivity = mock(Activity.class);
+ final WindowManager mockWindowManager = mock(WindowManager.class);
+ final Display mockDisplay = mock(Display.class);
+ final CameraInfo mockCameraInfo = mock(CameraInfo.class);
+ final long mockCameraInfoId = 5L;
+
+ hostApi.setActivity(mockActivity);
+ testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId);
+
+ when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager);
+ when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay);
+ when(proxySpy.getDisplayOrientedMeteringPointFactory(
+ mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight))
+ .thenReturn(mockDisplayOrientedMeteringPointFactory);
+ when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y, size)).thenReturn(meteringPoint);
+
+ hostApi.create(
+ meteringPointIdentifier,
+ x.doubleValue(),
+ y.doubleValue(),
+ size.doubleValue(),
+ mockCameraInfoId);
+
+ verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y, size);
+ assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint);
+ }
- hostApi.create(meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null);
+ @Test
+ @Config(sdk = 30)
+ public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified_AboveAndroid30() {
+ final MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
+ spy(new MeteringPointHostApiImpl.MeteringPointProxy());
+ final MeteringPointHostApiImpl hostApi =
+ new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
+ final Long meteringPointIdentifier = 78L;
+ final Float x = 0.23f;
+ final Float y = 0.32f;
+ final Float surfaceWidth = 1f;
+ final Float surfaceHeight = 1f;
+ final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory =
+ mock(DisplayOrientedMeteringPointFactory.class);
+ final Activity mockActivity = mock(Activity.class);
+ final Display mockDisplay = mock(Display.class);
+ final CameraInfo mockCameraInfo = mock(CameraInfo.class);
+ final long mockCameraInfoId = 6L;
+
+ hostApi.setActivity(mockActivity);
+ testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId);
+
+ when(mockActivity.getDisplay()).thenReturn(mockDisplay);
+ when(proxySpy.getDisplayOrientedMeteringPointFactory(
+ mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight))
+ .thenReturn(mockDisplayOrientedMeteringPointFactory);
+ when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint);
+
+ hostApi.create(
+ meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null, mockCameraInfoId);
+
+ verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y);
+ assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint);
+ }
- verify(mockSurfaceOrientedMeteringPointFactory).createPoint(x, y);
+ @Test
+ @Config(sdk = 29)
+ @SuppressWarnings("deprecation")
+ public void hostApiCreate_createsExpectedMeteringPointWithoutSizeSpecified_BelowAndroid30() {
+ final MeteringPointHostApiImpl.MeteringPointProxy proxySpy =
+ spy(new MeteringPointHostApiImpl.MeteringPointProxy());
+ final MeteringPointHostApiImpl hostApi =
+ new MeteringPointHostApiImpl(testInstanceManager, proxySpy);
+ final Long meteringPointIdentifier = 78L;
+ final Float x = 0.1f;
+ final Float y = 0.8f;
+ final Float surfaceWidth = 1f;
+ final Float surfaceHeight = 1f;
+ final DisplayOrientedMeteringPointFactory mockDisplayOrientedMeteringPointFactory =
+ mock(DisplayOrientedMeteringPointFactory.class);
+ final Activity mockActivity = mock(Activity.class);
+ final WindowManager mockWindowManager = mock(WindowManager.class);
+ final Display mockDisplay = mock(Display.class);
+ final CameraInfo mockCameraInfo = mock(CameraInfo.class);
+ final long mockCameraInfoId = 7L;
+
+ hostApi.setActivity(mockActivity);
+ testInstanceManager.addDartCreatedInstance(mockCameraInfo, mockCameraInfoId);
+
+ when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager);
+ when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay);
+ when(proxySpy.getDisplayOrientedMeteringPointFactory(
+ mockDisplay, mockCameraInfo, surfaceWidth, surfaceHeight))
+ .thenReturn(mockDisplayOrientedMeteringPointFactory);
+ when(mockDisplayOrientedMeteringPointFactory.createPoint(x, y)).thenReturn(meteringPoint);
+
+ hostApi.create(
+ meteringPointIdentifier, x.doubleValue(), y.doubleValue(), null, mockCameraInfoId);
+
+ verify(mockDisplayOrientedMeteringPointFactory).createPoint(x, y);
assertEquals(testInstanceManager.getInstance(meteringPointIdentifier), meteringPoint);
}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionFilterTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionFilterTest.java
new file mode 100644
index 000000000000..150f5676739a
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionFilterTest.java
@@ -0,0 +1,76 @@
+// 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.camerax;
+
+import static org.junit.Assert.assertEquals;
+
+import android.util.Size;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ResolutionFilterTest {
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ InstanceManager instanceManager;
+
+ @Before
+ public void setUp() {
+ instanceManager = InstanceManager.create(identifier -> {});
+ }
+
+ @After
+ public void tearDown() {
+ instanceManager.stopFinalizationListener();
+ }
+
+ @Test
+ public void hostApiCreateWithOnePreferredSize_createsExpectedResolutionFilterInstance() {
+ final ResolutionFilterHostApiImpl hostApi = new ResolutionFilterHostApiImpl(instanceManager);
+ final long instanceIdentifier = 50;
+ final long preferredResolutionWidth = 20;
+ final long preferredResolutionHeight = 80;
+ final ResolutionInfo preferredResolution =
+ new ResolutionInfo.Builder()
+ .setWidth(preferredResolutionWidth)
+ .setHeight(preferredResolutionHeight)
+ .build();
+
+ hostApi.createWithOnePreferredSize(instanceIdentifier, preferredResolution);
+
+ // Test that instance filters supported resolutions as expected.
+ final ResolutionFilter resolutionFilter = instanceManager.getInstance(instanceIdentifier);
+ final Size fakeSupportedSize1 = new Size(720, 480);
+ final Size fakeSupportedSize2 = new Size(20, 80);
+ final Size fakeSupportedSize3 = new Size(2, 8);
+ final Size preferredSize =
+ new Size((int) preferredResolutionWidth, (int) preferredResolutionHeight);
+
+ final ArrayList fakeSupportedSizes = new ArrayList();
+ fakeSupportedSizes.add(fakeSupportedSize1);
+ fakeSupportedSizes.add(fakeSupportedSize2);
+ fakeSupportedSizes.add(preferredSize);
+ fakeSupportedSizes.add(fakeSupportedSize3);
+
+ // Test the case where preferred resolution is supported.
+ List filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
+ assertEquals(filteredSizes.get(0), preferredSize);
+
+ // Test the case where preferred resolution is not supported.
+ fakeSupportedSizes.remove(0);
+ filteredSizes = resolutionFilter.filter(fakeSupportedSizes, 90);
+ assertEquals(filteredSizes, fakeSupportedSizes);
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionSelectorTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionSelectorTest.java
index f323e4706c9b..0f45f07b4f7a 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionSelectorTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ResolutionSelectorTest.java
@@ -9,6 +9,7 @@
import static org.mockito.Mockito.when;
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import org.junit.After;
@@ -46,13 +47,21 @@ public void hostApiCreate_createsExpectedResolutionSelectorInstance() {
final long aspectRatioStrategyIdentifier = 15;
instanceManager.addDartCreatedInstance(mockAspectRatioStrategy, aspectRatioStrategyIdentifier);
- when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy))
+ final ResolutionFilter mockResolutionFilter = mock(ResolutionFilter.class);
+ final long resolutionFilterIdentifier = 33;
+ instanceManager.addDartCreatedInstance(mockResolutionFilter, resolutionFilterIdentifier);
+
+ when(mockProxy.create(mockResolutionStrategy, mockAspectRatioStrategy, mockResolutionFilter))
.thenReturn(mockResolutionSelector);
final ResolutionSelectorHostApiImpl hostApi =
new ResolutionSelectorHostApiImpl(instanceManager, mockProxy);
final long instanceIdentifier = 0;
- hostApi.create(instanceIdentifier, resolutionStrategyIdentifier, aspectRatioStrategyIdentifier);
+ hostApi.create(
+ instanceIdentifier,
+ resolutionStrategyIdentifier,
+ resolutionFilterIdentifier,
+ aspectRatioStrategyIdentifier);
assertEquals(instanceManager.getInstance(instanceIdentifier), mockResolutionSelector);
}
diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart
index 4484c5dac982..22221fd1de96 100644
--- a/packages/camera/camera_android_camerax/example/lib/main.dart
+++ b/packages/camera/camera_android_camerax/example/lib/main.dart
@@ -282,14 +282,15 @@ class _CameraExampleHomeState extends State
IconButton(
icon: const Icon(Icons.exposure),
color: Colors.blue,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
+ onPressed: controller != null
+ ? onExposureModeButtonPressed
+ : null,
),
IconButton(
icon: const Icon(Icons.filter_center_focus),
color: Colors.blue,
onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
+ controller != null ? onFocusModeButtonPressed : null,
)
]
: [],
@@ -392,22 +393,32 @@ class _CameraExampleHomeState extends State
children: [
TextButton(
style: styleAuto,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
- onLongPress:
- () {}, // TODO(camsim99): Add functionality back here.,
+ onPressed: controller != null
+ ? () =>
+ onSetExposureModeButtonPressed(ExposureMode.auto)
+ : null,
+ onLongPress: () {
+ if (controller != null) {
+ CameraPlatform.instance
+ .setExposurePoint(controller!.cameraId, null);
+ showInSnackBar('Resetting exposure point');
+ }
+ },
child: const Text('AUTO'),
),
TextButton(
style: styleLocked,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
+ onPressed: controller != null
+ ? () =>
+ onSetExposureModeButtonPressed(ExposureMode.locked)
+ : null,
child: const Text('LOCKED'),
),
TextButton(
style: styleLocked,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
+ onPressed: controller != null
+ ? () => controller!.setExposureOffset(0.0)
+ : null,
child: const Text('RESET OFFSET'),
),
],
@@ -466,16 +477,23 @@ class _CameraExampleHomeState extends State
children: [
TextButton(
style: styleAuto,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
- onLongPress:
- () {}, // TODO(camsim99): Add functionality back here.
+ onPressed: controller != null
+ ? () => onSetFocusModeButtonPressed(FocusMode.auto)
+ : null,
+ onLongPress: () {
+ if (controller != null) {
+ CameraPlatform.instance
+ .setFocusPoint(controller!.cameraId, null);
+ }
+ showInSnackBar('Resetting focus point');
+ },
child: const Text('AUTO'),
),
TextButton(
style: styleLocked,
- onPressed:
- () {}, // TODO(camsim99): Add functionality back here.
+ onPressed: controller != null
+ ? () => onSetFocusModeButtonPressed(FocusMode.locked)
+ : null,
child: const Text('LOCKED'),
),
],
diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
index b05788d68a43..19aaea83fa3d 100644
--- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
+++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
@@ -3,28 +3,37 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:math' show Point;
import 'package:async/async.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
-import 'package:flutter/services.dart' show DeviceOrientation;
-import 'package:flutter/widgets.dart';
+import 'package:flutter/services.dart'
+ show DeviceOrientation, PlatformException;
+import 'package:flutter/widgets.dart'
+ show Size, Texture, Widget, visibleForTesting;
import 'package:stream_transform/stream_transform.dart';
import 'analyzer.dart';
+import 'aspect_ratio_strategy.dart';
import 'camera.dart';
+import 'camera2_camera_control.dart';
import 'camera_control.dart';
import 'camera_info.dart';
import 'camera_selector.dart';
import 'camera_state.dart';
import 'camerax_library.g.dart';
import 'camerax_proxy.dart';
+import 'capture_request_options.dart';
import 'device_orientation_manager.dart';
import 'exposure_state.dart';
import 'fallback_strategy.dart';
+import 'focus_metering_action.dart';
+import 'focus_metering_result.dart';
import 'image_analysis.dart';
import 'image_capture.dart';
import 'image_proxy.dart';
import 'live_data.dart';
+import 'metering_point.dart';
import 'observer.dart';
import 'pending_recording.dart';
import 'plane_proxy.dart';
@@ -33,6 +42,7 @@ import 'process_camera_provider.dart';
import 'quality_selector.dart';
import 'recorder.dart';
import 'recording.dart';
+import 'resolution_filter.dart';
import 'resolution_selector.dart';
import 'resolution_strategy.dart';
import 'surface.dart';
@@ -69,6 +79,9 @@ class AndroidCameraCameraX extends CameraPlatform {
@visibleForTesting
CameraInfo? cameraInfo;
+ /// The [CameraControl] instance that corresponds to the [camera] instance.
+ late CameraControl cameraControl;
+
/// The [LiveData] of the [CameraState] that represents the state of the
/// [camera] instance.
LiveData? liveCameraState;
@@ -180,6 +193,38 @@ class AndroidCameraCameraX extends CameraPlatform {
/// for an example on how setting target rotations for [UseCase]s works.
bool shouldSetDefaultRotation = false;
+ /// Error code indicating that an exposure offset value failed to be set.
+ static const String setExposureOffsetFailedErrorCode =
+ 'setExposureOffsetFailed';
+
+ /// The currently set [FocusMeteringAction] used to enable auto-focus and
+ /// auto-exposure.
+ @visibleForTesting
+ FocusMeteringAction? currentFocusMeteringAction;
+
+ /// Current focus mode set via [setFocusMode].
+ ///
+ /// CameraX defaults to auto focus mode.
+ FocusMode _currentFocusMode = FocusMode.auto;
+
+ /// Current exposure mode set via [setExposureMode].
+ ///
+ /// CameraX defaults to auto exposure mode.
+ ExposureMode _currentExposureMode = ExposureMode.auto;
+
+ /// Whether or not a default focus point of the entire sensor area was focused
+ /// and locked.
+ ///
+ /// This should only be true if [setExposureMode] was called to set
+ /// [FocusMode.locked] and no previous focus point was set via
+ /// [setFocusPoint].
+ bool _defaultFocusPointLocked = false;
+
+ /// Error code indicating that exposure compensation is not supported by
+ /// CameraX for the device.
+ static const String exposureCompensationNotSupported =
+ 'exposureCompensationNotSupported';
+
/// Returns list of all available cameras and their descriptions.
@override
Future> availableCameras() async {
@@ -330,16 +375,13 @@ class AndroidCameraCameraX extends CameraPlatform {
final ResolutionInfo previewResolutionInfo =
await preview!.getResolutionInfo();
- // Retrieve exposure and focus mode configurations:
- // TODO(camsim99): Implement support for retrieving exposure mode configuration.
- // https://github.com/flutter/flutter/issues/120468
+ // Mark auto-focus, auto-exposure and setting points for focus & exposure
+ // as available operations as CameraX does its best across devices to
+ // support these by default.
const ExposureMode exposureMode = ExposureMode.auto;
- const bool exposurePointSupported = false;
-
- // TODO(camsim99): Implement support for retrieving focus mode configuration.
- // https://github.com/flutter/flutter/issues/120467
const FocusMode focusMode = FocusMode.auto;
- const bool focusPointSupported = false;
+ const bool exposurePointSupported = true;
+ const bool focusPointSupported = true;
cameraEventStreamController.add(CameraInitializedEvent(
cameraId,
@@ -430,6 +472,28 @@ class AndroidCameraCameraX extends CameraPlatform {
captureOrientationLocked = false;
}
+ /// Sets the exposure point for automatically determining the exposure values.
+ ///
+ /// Supplying `null` for the [point] argument will result in resetting to the
+ /// original exposure point value.
+ ///
+ /// Supplied non-null point must be mapped to the entire un-altered preview
+ /// surface for the exposure point to be applied accurately.
+ ///
+ /// [cameraId] is not used.
+ @override
+ Future setExposurePoint(int cameraId, Point? point) async {
+ // We lock the new focus and metering action if focus mode has been locked
+ // to ensure that the current focus point remains locked. Any exposure mode
+ // setting will not be impacted by this lock (setting an exposure mode
+ // is implemented with Camera2 interop that will override settings to
+ // achieve the expected exposure mode as needed).
+ await _startFocusAndMeteringForPoint(
+ point: point,
+ meteringMode: FocusMeteringAction.flagAe,
+ disableAutoCancel: _currentFocusMode == FocusMode.locked);
+ }
+
/// Gets the minimum supported exposure offset for the selected camera in EV units.
///
/// [cameraId] not used.
@@ -450,15 +514,190 @@ class AndroidCameraCameraX extends CameraPlatform {
exposureState.exposureCompensationStep;
}
+ /// Sets the focus mode for taking pictures.
+ ///
+ /// Setting [FocusMode.locked] will lock the current focus point if one exists
+ /// or the center of entire sensor area if not, and will stay locked until
+ /// either:
+ /// * Another focus point is set via [setFocusPoint] (which will then become
+ /// the locked focus point), or
+ /// * Locked focus mode is unset by setting [FocusMode.auto].
+ @override
+ Future setFocusMode(int cameraId, FocusMode mode) async {
+ if (_currentFocusMode == mode) {
+ // Desired focus mode is already set.
+ return;
+ }
+
+ MeteringPoint? autoFocusPoint;
+ bool? disableAutoCancel;
+ switch (mode) {
+ case FocusMode.auto:
+ // Determine auto-focus point to restore, if any. We do not restore
+ // default auto-focus point if set previously to lock focus.
+ final MeteringPoint? unLockedFocusPoint = _defaultFocusPointLocked
+ ? null
+ : currentFocusMeteringAction!.meteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAf)
+ .toList()
+ .first
+ .$1;
+ _defaultFocusPointLocked = false;
+ autoFocusPoint = unLockedFocusPoint;
+ disableAutoCancel = false;
+ case FocusMode.locked:
+ MeteringPoint? lockedFocusPoint;
+
+ // Determine if there is an auto-focus point set currently to lock.
+ if (currentFocusMeteringAction != null) {
+ final List<(MeteringPoint, int?)> possibleCurrentAfPoints =
+ currentFocusMeteringAction!.meteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAf)
+ .toList();
+ lockedFocusPoint = possibleCurrentAfPoints.isEmpty
+ ? null
+ : possibleCurrentAfPoints.first.$1;
+ }
+
+ // If there isn't, lock center of entire sensor area by default.
+ if (lockedFocusPoint == null) {
+ lockedFocusPoint =
+ proxy.createMeteringPoint(0.5, 0.5, 1, cameraInfo!);
+ _defaultFocusPointLocked = true;
+ }
+
+ autoFocusPoint = lockedFocusPoint;
+ disableAutoCancel = true;
+ }
+ // Start appropriate focus and metering action.
+ final bool focusAndMeteringWasSuccessful = await _startFocusAndMeteringFor(
+ meteringPoint: autoFocusPoint,
+ meteringMode: FocusMeteringAction.flagAf,
+ disableAutoCancel: disableAutoCancel);
+
+ if (!focusAndMeteringWasSuccessful) {
+ // Do not update current focus mode.
+ return;
+ }
+
+ // Update current focus mode.
+ _currentFocusMode = mode;
+
+ // If focus mode was just locked and exposure mode is not, set auto exposure
+ // mode to ensure that disabling auto-cancel does not interfere with
+ // automatic exposure metering.
+ if (_currentExposureMode == ExposureMode.auto &&
+ _currentFocusMode == FocusMode.locked) {
+ await setExposureMode(cameraId, _currentExposureMode);
+ }
+ }
+
/// Gets the supported step size for exposure offset for the selected camera in EV units.
///
- /// Returns 0 when exposure compensation is not supported.
+ /// Returns -1 if exposure compensation is not supported for the device.
///
/// [cameraId] not used.
@override
Future getExposureOffsetStepSize(int cameraId) async {
final ExposureState exposureState = await cameraInfo!.getExposureState();
- return exposureState.exposureCompensationStep;
+ final double exposureOffsetStepSize =
+ exposureState.exposureCompensationStep;
+ if (exposureOffsetStepSize == 0) {
+ // CameraX returns a step size of 0 if exposure compensation is not
+ // supported for the device.
+ return -1;
+ }
+ return exposureOffsetStepSize;
+ }
+
+ /// Sets the exposure offset for the selected camera.
+ ///
+ /// The supplied [offset] value should be in EV units. 1 EV unit represents a
+ /// doubling in brightness. It should be between the minimum and maximum offsets
+ /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively.
+ /// Throws a `CameraException` when trying to set exposure offset on a device
+ /// that doesn't support exposure compensationan or if setting the offset fails,
+ /// like in the case that an illegal offset is supplied.
+ ///
+ /// When the supplied [offset] value does not align with the step size obtained
+ /// through `getExposureStepSize`, it will automatically be rounded to the nearest step.
+ ///
+ /// Returns the (rounded) offset value that was set.
+ @override
+ Future setExposureOffset(int cameraId, double offset) async {
+ final double exposureOffsetStepSize =
+ (await cameraInfo!.getExposureState()).exposureCompensationStep;
+ if (exposureOffsetStepSize == 0) {
+ throw CameraException(exposureCompensationNotSupported,
+ 'Exposure compensation not supported');
+ }
+
+ // (Exposure compensation index) * (exposure offset step size) =
+ // (exposure offset).
+ final int roundedExposureCompensationIndex =
+ (offset / exposureOffsetStepSize).round();
+
+ try {
+ final int? newIndex = await cameraControl
+ .setExposureCompensationIndex(roundedExposureCompensationIndex);
+ if (newIndex == null) {
+ throw CameraException(setExposureOffsetFailedErrorCode,
+ 'Setting exposure compensation index was canceled due to the camera being closed or a new request being submitted.');
+ }
+
+ return newIndex.toDouble();
+ } on PlatformException catch (e) {
+ throw CameraException(
+ setExposureOffsetFailedErrorCode,
+ e.message ??
+ 'Setting the camera exposure compensation index failed.');
+ }
+ }
+
+ /// Sets the focus point for automatically determining the focus values.
+ ///
+ /// Supplying `null` for the [point] argument will result in resetting to the
+ /// original focus point value.
+ ///
+ /// Supplied non-null point must be mapped to the entire un-altered preview
+ /// surface for the focus point to be applied accurately.
+ ///
+ /// [cameraId] is not used.
+ @override
+ Future setFocusPoint(int cameraId, Point? point) async {
+ // We lock the new focus and metering action if focus mode has been locked
+ // to ensure that the current focus point remains locked. Any exposure mode
+ // setting will not be impacted by this lock (setting an exposure mode
+ // is implemented with Camera2 interop that will override settings to
+ // achieve the expected exposure mode as needed).
+ await _startFocusAndMeteringForPoint(
+ point: point,
+ meteringMode: FocusMeteringAction.flagAf,
+ disableAutoCancel: _currentFocusMode == FocusMode.locked);
+ }
+
+ /// Sets the exposure mode for taking pictures.
+ ///
+ /// Setting [ExposureMode.locked] will lock current exposure point until it
+ /// is unset by setting [ExposureMode.auto].
+ ///
+ /// [cameraId] is not used.
+ @override
+ Future setExposureMode(int cameraId, ExposureMode mode) async {
+ final Camera2CameraControl camera2Control =
+ proxy.getCamera2CameraControl(cameraControl);
+ final bool lockExposureMode = mode == ExposureMode.locked;
+
+ final CaptureRequestOptions captureRequestOptions = proxy
+ .createCaptureRequestOptions(<(
+ CaptureRequestKeySupportedType,
+ Object?
+ )>[(CaptureRequestKeySupportedType.controlAeLock, lockExposureMode)]);
+
+ await camera2Control.addCaptureRequestOptions(captureRequestOptions);
+ _currentExposureMode = mode;
}
/// Gets the maximum supported zoom level for the selected camera.
@@ -502,7 +741,6 @@ class AndroidCameraCameraX extends CameraPlatform {
/// Throws a `CameraException` when an illegal zoom level is supplied.
@override
Future setZoomLevel(int cameraId, double zoom) async {
- final CameraControl cameraControl = await camera!.getCameraControl();
await cameraControl.setZoomRatio(zoom);
}
@@ -587,10 +825,8 @@ class AndroidCameraCameraX extends CameraPlatform {
/// respectively.
@override
Future setFlashMode(int cameraId, FlashMode mode) async {
- CameraControl? cameraControl;
// Turn off torch mode if it is enabled and not being redundantly set.
if (mode != FlashMode.torch && torchEnabled) {
- cameraControl = await camera!.getCameraControl();
await cameraControl.enableTorch(false);
torchEnabled = false;
}
@@ -608,7 +844,6 @@ class AndroidCameraCameraX extends CameraPlatform {
// Torch mode enabled already.
return;
}
- cameraControl = await camera!.getCameraControl();
await cameraControl.enableTorch(true);
torchEnabled = true;
}
@@ -833,14 +1068,15 @@ class AndroidCameraCameraX extends CameraPlatform {
// Methods concerning camera state:
- /// Updates [cameraInfo] to the information corresponding to [camera] and
- /// adds observers to the [LiveData] of the [CameraState] of the current
- /// [camera], saved as [liveCameraState].
+ /// Updates [cameraInfo] and [cameraControl] to the information corresponding
+ /// to [camera] and adds observers to the [LiveData] of the [CameraState] of
+ /// the current [camera], saved as [liveCameraState].
///
/// If a previous [liveCameraState] was stored, existing observers are
/// removed, as well.
Future _updateCameraInfoAndLiveCameraState(int cameraId) async {
cameraInfo = await camera!.getCameraInfo();
+ cameraControl = await camera!.getCameraControl();
await liveCameraState?.removeObservers();
liveCameraState = await cameraInfo!.getCameraState();
await liveCameraState!.observe(_createCameraClosingObserver(cameraId));
@@ -917,23 +1153,29 @@ class AndroidCameraCameraX extends CameraPlatform {
ResolutionStrategy.fallbackRuleClosestLowerThenHigher;
Size? boundSize;
+ int? aspectRatio;
ResolutionStrategy? resolutionStrategy;
switch (preset) {
case ResolutionPreset.low:
boundSize = const Size(320, 240);
+ aspectRatio = AspectRatio.ratio4To3;
case ResolutionPreset.medium:
boundSize = const Size(720, 480);
case ResolutionPreset.high:
boundSize = const Size(1280, 720);
+ aspectRatio = AspectRatio.ratio16To9;
case ResolutionPreset.veryHigh:
boundSize = const Size(1920, 1080);
+ aspectRatio = AspectRatio.ratio16To9;
case ResolutionPreset.ultraHigh:
boundSize = const Size(3840, 2160);
+ aspectRatio = AspectRatio.ratio16To9;
case ResolutionPreset.max:
// Automatically set strategy to choose highest available.
resolutionStrategy =
proxy.createResolutionStrategy(highestAvailable: true);
- return proxy.createResolutionSelector(resolutionStrategy);
+ return proxy.createResolutionSelector(resolutionStrategy,
+ /* ResolutionFilter */ null, /* AspectRatioStrategy */ null);
case null:
// If no preset is specified, default to CameraX's default behavior
// for each UseCase.
@@ -942,7 +1184,14 @@ class AndroidCameraCameraX extends CameraPlatform {
resolutionStrategy = proxy.createResolutionStrategy(
boundSize: boundSize, fallbackRule: fallbackRule);
- return proxy.createResolutionSelector(resolutionStrategy);
+ final ResolutionFilter resolutionFilter =
+ proxy.createResolutionFilterWithOnePreferredSize(boundSize);
+ final AspectRatioStrategy? aspectRatioStrategy = aspectRatio == null
+ ? null
+ : proxy.createAspectRatioStrategy(
+ aspectRatio, AspectRatioStrategy.fallbackRuleAuto);
+ return proxy.createResolutionSelector(
+ resolutionStrategy, resolutionFilter, aspectRatioStrategy);
}
/// Returns the [QualitySelector] that maps to the specified resolution
@@ -981,4 +1230,108 @@ class AndroidCameraCameraX extends CameraPlatform {
return proxy.createQualitySelector(
videoQuality: videoQuality, fallbackStrategy: fallbackStrategy);
}
+
+ // Methods for configuring auto-focus and auto-exposure:
+
+ Future _startFocusAndMeteringForPoint(
+ {required Point? point,
+ required int meteringMode,
+ bool disableAutoCancel = false}) async {
+ return _startFocusAndMeteringFor(
+ meteringPoint: point == null
+ ? null
+ : proxy.createMeteringPoint(
+ point.x, point.y, /* size */ null, cameraInfo!),
+ meteringMode: meteringMode,
+ disableAutoCancel: disableAutoCancel);
+ }
+
+ /// Starts a focus and metering action and returns whether or not it was
+ /// successful.
+ ///
+ /// This method will modify and start the current action's [MeteringPoint]s
+ /// overriden with the [meteringPoint] provided for the specified
+ /// [meteringMode] type only, with all other metering points of other modes
+ /// left untouched. If no current action exists, only the specified
+ /// [meteringPoint] will be set. Thus, the focus and metering action started
+ /// will only contain at most the one most recently set metering point for
+ /// each metering mode: AF, AE, AWB.
+ ///
+ /// Thus, if [meteringPoint] is non-null, this action includes:
+ /// * metering points and their modes previously added to
+ /// [currentFocusMeteringAction] that do not share a metering mode with
+ /// [meteringPoint] (if [currentFocusMeteringAction] is non-null) and
+ /// * [meteringPoint] with the specified [meteringMode].
+ /// If [meteringPoint] is null and [currentFocusMeteringAction] is non-null,
+ /// this action includes only metering points and their modes previously added
+ /// to [currentFocusMeteringAction] that do not share a metering mode with
+ /// [meteringPoint]. If [meteringPoint] and [currentFocusMeteringAction] are
+ /// null, then focus and metering will be canceled.
+ Future _startFocusAndMeteringFor(
+ {required MeteringPoint? meteringPoint,
+ required int meteringMode,
+ bool disableAutoCancel = false}) async {
+ if (meteringPoint == null) {
+ // Try to clear any metering point from previous action with the specified
+ // meteringMode.
+ if (currentFocusMeteringAction == null) {
+ // Attempting to clear a metering point from a previous action, but no
+ // such action exists.
+ return false;
+ }
+
+ // Remove metering point with specified meteringMode from current focus
+ // and metering action, as only one focus or exposure point may be set
+ // at once in this plugin.
+ final List<(MeteringPoint, int?)> newMeteringPointInfos =
+ currentFocusMeteringAction!.meteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ // meteringPointInfo may technically include points without a
+ // mode specified, but this logic is safe because this plugin
+ // only uses points that explicitly have mode
+ // FocusMeteringAction.flagAe or FocusMeteringAction.flagAf.
+ meteringPointInfo.$2 != meteringMode)
+ .toList();
+
+ if (newMeteringPointInfos.isEmpty) {
+ // If no other metering points were specified, cancel any previously
+ // started focus and metering actions.
+ await cameraControl.cancelFocusAndMetering();
+ currentFocusMeteringAction = null;
+ return true;
+ }
+ currentFocusMeteringAction = proxy.createFocusMeteringAction(
+ newMeteringPointInfos, disableAutoCancel);
+ } else if (meteringPoint.x < 0 ||
+ meteringPoint.x > 1 ||
+ meteringPoint.y < 0 ||
+ meteringPoint.y > 1) {
+ throw CameraException('pointInvalid',
+ 'The coordinates of a metering point for an auto-focus or auto-exposure action must be within (0,0) and (1,1), but a point with coordinates (${meteringPoint.x}, ${meteringPoint.y}) was provided for metering mode $meteringMode.');
+ } else {
+ // Add new metering point with specified meteringMode, which may involve
+ // replacing a metering point with the same specified meteringMode from
+ // the current focus and metering action.
+ List<(MeteringPoint, int?)> newMeteringPointInfos =
+ <(MeteringPoint, int?)>[];
+
+ if (currentFocusMeteringAction != null) {
+ newMeteringPointInfos = currentFocusMeteringAction!.meteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ // meteringPointInfo may technically include points without a
+ // mode specified, but this logic is safe because this plugin
+ // only uses points that explicitly have mode
+ // FocusMeteringAction.flagAe or FocusMeteringAction.flagAf.
+ meteringPointInfo.$2 != meteringMode)
+ .toList();
+ }
+ newMeteringPointInfos.add((meteringPoint, meteringMode));
+ currentFocusMeteringAction = proxy.createFocusMeteringAction(
+ newMeteringPointInfos, disableAutoCancel);
+ }
+
+ final FocusMeteringResult? result =
+ await cameraControl.startFocusAndMetering(currentFocusMeteringAction!);
+ return await result?.isFocusSuccessful() ?? false;
+ }
}
diff --git a/packages/camera/camera_android_camerax/lib/src/camera_control.dart b/packages/camera/camera_android_camerax/lib/src/camera_control.dart
index 9c50ffa161fd..0a307f3afc4a 100644
--- a/packages/camera/camera_android_camerax/lib/src/camera_control.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camera_control.dart
@@ -56,6 +56,10 @@ class CameraControl extends JavaObject {
/// Will trigger an auto focus action and enable auto focus/auto exposure/
/// auto white balance metering regions.
///
+ /// Only one [FocusMeteringAction] is allowed to run at a time; if multiple
+ /// are executed in a row, only the latest one will work and other actions
+ /// will be canceled.
+ ///
/// Returns null if focus and metering could not be started.
Future startFocusAndMetering(
FocusMeteringAction action) {
@@ -74,6 +78,10 @@ class CameraControl extends JavaObject {
/// of the current [ExposureState]'s `exposureCompensationRange` for the call
/// to succeed.
///
+ /// Only one [setExposureCompensationIndex] is allowed to run at a time; if
+ /// multiple are executed in a row, only the latest setting will be kept in
+ /// the camera.
+ ///
/// Returns null if the exposure compensation index failed to be set.
Future setExposureCompensationIndex(int index) async {
return _api.setExposureCompensationIndexFromInstance(this, index);
@@ -133,14 +141,21 @@ class _CameraControlHostApiImpl extends CameraControlHostApi {
instanceManager.getIdentifier(instance)!;
final int actionIdentifier = instanceManager.getIdentifier(action)!;
try {
- final int focusMeteringResultId = await startFocusAndMetering(
+ final int? focusMeteringResultId = await startFocusAndMetering(
cameraControlIdentifier, actionIdentifier);
+ if (focusMeteringResultId == null) {
+ SystemServices.cameraErrorStreamController.add(
+ 'Starting focus and metering was canceled due to the camera being closed or a new request being submitted.');
+ return Future.value();
+ }
return instanceManager.getInstanceWithWeakReference(
focusMeteringResultId);
} on PlatformException catch (e) {
SystemServices.cameraErrorStreamController
.add(e.message ?? 'Starting focus and metering failed.');
- return Future.value();
+ // Surfacing error to differentiate an operation cancellation from an
+ // illegal argument exception at a plugin layer.
+ rethrow;
}
}
@@ -158,11 +173,20 @@ class _CameraControlHostApiImpl extends CameraControlHostApi {
CameraControl instance, int index) async {
final int identifier = instanceManager.getIdentifier(instance)!;
try {
- return setExposureCompensationIndex(identifier, index);
+ final int? exposureCompensationIndex =
+ await setExposureCompensationIndex(identifier, index);
+ if (exposureCompensationIndex == null) {
+ SystemServices.cameraErrorStreamController.add(
+ 'Setting exposure compensation index was canceled due to the camera being closed or a new request being submitted.');
+ return Future.value();
+ }
+ return exposureCompensationIndex;
} on PlatformException catch (e) {
SystemServices.cameraErrorStreamController.add(e.message ??
'Setting the camera exposure compensation index failed.');
- return Future.value();
+ // Surfacing error to plugin layer to maintain consistency of
+ // setExposureOffset implementation across platform implementations.
+ rethrow;
}
}
}
diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
index 07515c0c14fd..ac66dfd0ae77 100644
--- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
@@ -27,14 +27,14 @@ enum CameraStateType {
/// If you need to add another type to support a type S to use a LiveData in
/// this plugin, ensure the following is done on the Dart side:
///
-/// * In `../lib/src/live_data.dart`, add new cases for S in
+/// * In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in
/// `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of
/// type S from a LiveData instance and in `LiveDataFlutterApiImpl#create`
/// to create the expected type of LiveData when requested.
///
/// On the native side, ensure the following is done:
///
-/// * Update `LiveDataHostApiImpl#getValue` is updated to properly return
+/// * Make sure `LiveDataHostApiImpl#getValue` is updated to properly return
/// identifiers for instances of type S.
/// * Update `ObserverFlutterApiWrapper#onChanged` to properly handle receiving
/// calls with instances of type S if a LiveData instance is observed.
@@ -68,6 +68,24 @@ enum VideoResolutionFallbackRule {
lowerQualityThan,
}
+/// The types of capture request options this plugin currently supports.
+///
+/// If you need to add another option to support, ensure the following is done
+/// on the Dart side:
+///
+/// * In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this
+/// option in `_CaptureRequestOptionsHostApiImpl#createFromInstances`
+/// to create the expected Map entry of option key index and value to send to
+/// the native side.
+///
+/// On the native side, ensure the following is done:
+///
+/// * Update `CaptureRequestOptionsHostApiImpl#create` to set the correct
+/// `CaptureRequest` key with a valid value type for this option.
+///
+/// See https://developer.android.com/reference/android/hardware/camera2/CaptureRequest
+/// for the sorts of capture request options that can be supported via CameraX's
+/// interoperability with Camera2.
enum CaptureRequestKeySupportedType {
controlAeLock,
}
@@ -1896,7 +1914,10 @@ class ResolutionSelectorHostApi {
static const MessageCodec codec = StandardMessageCodec();
- Future create(int arg_identifier, int? arg_resolutionStrategyIdentifier,
+ Future create(
+ int arg_identifier,
+ int? arg_resolutionStrategyIdentifier,
+ int? arg_resolutionSelectorIdentifier,
int? arg_aspectRatioStrategyIdentifier) async {
final BasicMessageChannel channel = BasicMessageChannel(
'dev.flutter.pigeon.ResolutionSelectorHostApi.create', codec,
@@ -1904,6 +1925,7 @@ class ResolutionSelectorHostApi {
final List? replyList = await channel.send([
arg_identifier,
arg_resolutionStrategyIdentifier,
+ arg_resolutionSelectorIdentifier,
arg_aspectRatioStrategyIdentifier
]) as List?;
if (replyList == null) {
@@ -2884,7 +2906,7 @@ class CameraControlHostApi {
}
}
- Future startFocusAndMetering(
+ Future startFocusAndMetering(
int arg_identifier, int arg_focusMeteringActionId) async {
final BasicMessageChannel channel = BasicMessageChannel(
'dev.flutter.pigeon.CameraControlHostApi.startFocusAndMetering', codec,
@@ -2903,13 +2925,8 @@ class CameraControlHostApi {
message: replyList[1] as String?,
details: replyList[2],
);
- } else if (replyList[0] == null) {
- throw PlatformException(
- code: 'null-error',
- message: 'Host platform returned null value for non-null return value.',
- );
} else {
- return (replyList[0] as int?)!;
+ return (replyList[0] as int?);
}
}
@@ -2935,7 +2952,7 @@ class CameraControlHostApi {
}
}
- Future setExposureCompensationIndex(
+ Future setExposureCompensationIndex(
int arg_identifier, int arg_index) async {
final BasicMessageChannel channel = BasicMessageChannel(
'dev.flutter.pigeon.CameraControlHostApi.setExposureCompensationIndex',
@@ -2954,13 +2971,8 @@ class CameraControlHostApi {
message: replyList[1] as String?,
details: replyList[2],
);
- } else if (replyList[0] == null) {
- throw PlatformException(
- code: 'null-error',
- message: 'Host platform returned null value for non-null return value.',
- );
} else {
- return (replyList[0] as int?)!;
+ return (replyList[0] as int?);
}
}
}
@@ -3027,14 +3039,18 @@ class FocusMeteringActionHostApi {
static const MessageCodec codec = _FocusMeteringActionHostApiCodec();
- Future create(int arg_identifier,
- List arg_meteringPointInfos) async {
+ Future create(
+ int arg_identifier,
+ List arg_meteringPointInfos,
+ bool? arg_disableAutoCancel) async {
final BasicMessageChannel channel = BasicMessageChannel(
'dev.flutter.pigeon.FocusMeteringActionHostApi.create', codec,
binaryMessenger: _binaryMessenger);
- final List? replyList =
- await channel.send([arg_identifier, arg_meteringPointInfos])
- as List?;
+ final List? replyList = await channel.send([
+ arg_identifier,
+ arg_meteringPointInfos,
+ arg_disableAutoCancel
+ ]) as List?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
@@ -3130,14 +3146,14 @@ class MeteringPointHostApi {
static const MessageCodec codec = StandardMessageCodec();
- Future create(
- int arg_identifier, double arg_x, double arg_y, double? arg_size) async {
+ Future create(int arg_identifier, double arg_x, double arg_y,
+ double? arg_size, int arg_cameraInfoId) async {
final BasicMessageChannel channel = BasicMessageChannel(
'dev.flutter.pigeon.MeteringPointHostApi.create', codec,
binaryMessenger: _binaryMessenger);
- final List? replyList =
- await channel.send([arg_identifier, arg_x, arg_y, arg_size])
- as List?;
+ final List? replyList = await channel.send(
+ [arg_identifier, arg_x, arg_y, arg_size, arg_cameraInfoId])
+ as List?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
@@ -3328,3 +3344,62 @@ class Camera2CameraControlHostApi {
}
}
}
+
+class _ResolutionFilterHostApiCodec extends StandardMessageCodec {
+ const _ResolutionFilterHostApiCodec();
+ @override
+ void writeValue(WriteBuffer buffer, Object? value) {
+ if (value is ResolutionInfo) {
+ buffer.putUint8(128);
+ writeValue(buffer, value.encode());
+ } else {
+ super.writeValue(buffer, value);
+ }
+ }
+
+ @override
+ Object? readValueOfType(int type, ReadBuffer buffer) {
+ switch (type) {
+ case 128:
+ return ResolutionInfo.decode(readValue(buffer)!);
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+}
+
+class ResolutionFilterHostApi {
+ /// Constructor for [ResolutionFilterHostApi]. The [binaryMessenger] named argument is
+ /// available for dependency injection. If it is left null, the default
+ /// BinaryMessenger will be used which routes to the host platform.
+ ResolutionFilterHostApi({BinaryMessenger? binaryMessenger})
+ : _binaryMessenger = binaryMessenger;
+ final BinaryMessenger? _binaryMessenger;
+
+ static const MessageCodec codec = _ResolutionFilterHostApiCodec();
+
+ Future createWithOnePreferredSize(
+ int arg_identifier, ResolutionInfo arg_preferredResolution) async {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.ResolutionFilterHostApi.createWithOnePreferredSize',
+ codec,
+ binaryMessenger: _binaryMessenger);
+ final List? replyList =
+ await channel.send([arg_identifier, arg_preferredResolution])
+ as List?;
+ if (replyList == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ );
+ } else if (replyList.length > 1) {
+ throw PlatformException(
+ code: replyList[0]! as String,
+ message: replyList[1] as String?,
+ details: replyList[2],
+ );
+ } else {
+ return;
+ }
+ }
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
index 87d3680ff2c3..6fec50ce3983 100644
--- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
@@ -5,19 +5,27 @@
import 'dart:ui' show Size;
import 'analyzer.dart';
+import 'aspect_ratio_strategy.dart';
+import 'camera2_camera_control.dart';
+import 'camera_control.dart';
+import 'camera_info.dart';
import 'camera_selector.dart';
import 'camera_state.dart';
import 'camerax_library.g.dart';
+import 'capture_request_options.dart';
import 'device_orientation_manager.dart';
import 'fallback_strategy.dart';
+import 'focus_metering_action.dart';
import 'image_analysis.dart';
import 'image_capture.dart';
import 'image_proxy.dart';
+import 'metering_point.dart';
import 'observer.dart';
import 'preview.dart';
import 'process_camera_provider.dart';
import 'quality_selector.dart';
import 'recorder.dart';
+import 'resolution_filter.dart';
import 'resolution_selector.dart';
import 'resolution_strategy.dart';
import 'system_services.dart';
@@ -49,6 +57,13 @@ class CameraXProxy {
_startListeningForDeviceOrientationChange,
this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider,
this.getDefaultDisplayRotation = _getDefaultDisplayRotation,
+ this.getCamera2CameraControl = _getCamera2CameraControl,
+ this.createCaptureRequestOptions = _createAttachedCaptureRequestOptions,
+ this.createMeteringPoint = _createAttachedMeteringPoint,
+ this.createFocusMeteringAction = _createAttachedFocusMeteringAction,
+ this.createAspectRatioStrategy = _createAttachedAspectRatioStrategy,
+ this.createResolutionFilterWithOnePreferredSize =
+ _createAttachedResolutionFilterWithOnePreferredSize,
});
/// Returns a [ProcessCameraProvider] instance.
@@ -105,9 +120,11 @@ class CameraXProxy {
int? fallbackRule}) createResolutionStrategy;
/// Returns a [ResolutionSelector] configured with the specified
- /// [ResolutionStrategy].
- ResolutionSelector Function(ResolutionStrategy resolutionStrategy)
- createResolutionSelector;
+ /// [ResolutionStrategy], [ResolutionFilter], and [AspectRatioStrategy].
+ ResolutionSelector Function(
+ ResolutionStrategy resolutionStrategy,
+ ResolutionFilter? resolutionFilter,
+ AspectRatioStrategy? aspectRatioStrategy) createResolutionSelector;
/// Returns a [FallbackStrategy] configured with the specified [VideoQuality]
/// and [VideoResolutionFallbackRule].
@@ -137,6 +154,35 @@ class CameraXProxy {
/// rotation constants.
Future Function() getDefaultDisplayRotation;
+ /// Get [Camera2CameraControl] instance from [cameraControl].
+ Camera2CameraControl Function(CameraControl cameraControl)
+ getCamera2CameraControl;
+
+ /// Creates a [CaptureRequestOptions] with specified options.
+ CaptureRequestOptions Function(
+ List<(CaptureRequestKeySupportedType, Object?)> options)
+ createCaptureRequestOptions;
+
+ /// Returns a [MeteringPoint] with the specified coordinates based on
+ /// [cameraInfo].
+ MeteringPoint Function(
+ double x, double y, double? size, CameraInfo cameraInfo)
+ createMeteringPoint;
+
+ /// Returns a [FocusMeteringAction] based on the specified metering points
+ /// and their modes.
+ FocusMeteringAction Function(List<(MeteringPoint, int?)> meteringPointInfos,
+ bool? disableAutoCancel) createFocusMeteringAction;
+
+ /// Creates an [AspectRatioStrategy] with specified aspect ratio and fallback
+ /// rule.
+ AspectRatioStrategy Function(int aspectRatio, int fallbackRule)
+ createAspectRatioStrategy;
+
+ /// Creates a [ResolutionFilter] that prioritizes specified resolution.
+ ResolutionFilter Function(Size preferredResolution)
+ createResolutionFilterWithOnePreferredSize;
+
static Future _getProcessCameraProvider() {
return ProcessCameraProvider.getInstance();
}
@@ -204,8 +250,13 @@ class CameraXProxy {
}
static ResolutionSelector _createAttachedResolutionSelector(
- ResolutionStrategy resolutionStrategy) {
- return ResolutionSelector(resolutionStrategy: resolutionStrategy);
+ ResolutionStrategy resolutionStrategy,
+ ResolutionFilter? resolutionFilter,
+ AspectRatioStrategy? aspectRatioStrategy) {
+ return ResolutionSelector(
+ resolutionStrategy: resolutionStrategy,
+ resolutionFilter: resolutionFilter,
+ aspectRatioStrategy: aspectRatioStrategy);
}
static FallbackStrategy _createAttachedFallbackStrategy(
@@ -239,4 +290,38 @@ class CameraXProxy {
static Future _getDefaultDisplayRotation() async {
return DeviceOrientationManager.getDefaultDisplayRotation();
}
+
+ static Camera2CameraControl _getCamera2CameraControl(
+ CameraControl cameraControl) {
+ return Camera2CameraControl(cameraControl: cameraControl);
+ }
+
+ static CaptureRequestOptions _createAttachedCaptureRequestOptions(
+ List<(CaptureRequestKeySupportedType, Object?)> options) {
+ return CaptureRequestOptions(requestedOptions: options);
+ }
+
+ static MeteringPoint _createAttachedMeteringPoint(
+ double x, double y, double? size, CameraInfo cameraInfo) {
+ return MeteringPoint(x: x, y: y, size: size, cameraInfo: cameraInfo);
+ }
+
+ static FocusMeteringAction _createAttachedFocusMeteringAction(
+ List<(MeteringPoint, int?)> meteringPointInfos, bool? disableAutoCancel) {
+ return FocusMeteringAction(
+ meteringPointInfos: meteringPointInfos,
+ disableAutoCancel: disableAutoCancel);
+ }
+
+ static AspectRatioStrategy _createAttachedAspectRatioStrategy(
+ int preferredAspectRatio, int fallbackRule) {
+ return AspectRatioStrategy(
+ preferredAspectRatio: preferredAspectRatio, fallbackRule: fallbackRule);
+ }
+
+ static ResolutionFilter _createAttachedResolutionFilterWithOnePreferredSize(
+ Size preferredSize) {
+ return ResolutionFilter.onePreferredSize(
+ preferredResolution: preferredSize);
+ }
}
diff --git a/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart b/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart
index 6d9ebd97f015..81fa6f5abf14 100644
--- a/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart
+++ b/packages/camera/camera_android_camerax/lib/src/focus_metering_action.dart
@@ -19,15 +19,15 @@ class FocusMeteringAction extends JavaObject {
FocusMeteringAction({
BinaryMessenger? binaryMessenger,
InstanceManager? instanceManager,
- required List<(MeteringPoint meteringPoint, int? meteringMode)>
- meteringPointInfos,
+ required this.meteringPointInfos,
+ this.disableAutoCancel,
}) : super.detached(
binaryMessenger: binaryMessenger,
instanceManager: instanceManager,
) {
_api = _FocusMeteringActionHostApiImpl(
binaryMessenger: binaryMessenger, instanceManager: instanceManager);
- _api.createFromInstance(this, meteringPointInfos);
+ _api.createFromInstance(this, meteringPointInfos, disableAutoCancel);
}
/// Creates a [FocusMeteringAction] that is not automatically attached to a
@@ -35,6 +35,8 @@ class FocusMeteringAction extends JavaObject {
FocusMeteringAction.detached({
BinaryMessenger? binaryMessenger,
InstanceManager? instanceManager,
+ required this.meteringPointInfos,
+ this.disableAutoCancel,
}) : super.detached(
binaryMessenger: binaryMessenger,
instanceManager: instanceManager,
@@ -45,6 +47,17 @@ class FocusMeteringAction extends JavaObject {
late final _FocusMeteringActionHostApiImpl _api;
+ /// The requested [MeteringPoint]s and modes that are relevant to each of those
+ /// points.
+ final List<(MeteringPoint meteringPoint, int? meteringMode)>
+ meteringPointInfos;
+
+ /// Disables the auto-cancel.
+ ///
+ /// By default (and if set to false), auto-cancel is enabled with 5 seconds
+ /// duration.
+ final bool? disableAutoCancel;
+
/// Flag for metering mode that indicates the auto focus region is enabled.
///
/// An autofocus scan is also triggered when [flagAf] is assigned.
@@ -92,12 +105,14 @@ class _FocusMeteringActionHostApiImpl extends FocusMeteringActionHostApi {
/// [MeteringPoint]s and their modes in order of descending priority.
void createFromInstance(
FocusMeteringAction instance,
- List<(MeteringPoint meteringPoint, int? meteringMode)>
- meteringPointInfos) {
+ List<(MeteringPoint meteringPoint, int? meteringMode)> meteringPointInfos,
+ bool? disableAutoCancel) {
final int identifier = instanceManager.addDartCreatedInstance(instance,
onCopy: (FocusMeteringAction original) {
return FocusMeteringAction.detached(
- binaryMessenger: binaryMessenger, instanceManager: instanceManager);
+ binaryMessenger: binaryMessenger,
+ instanceManager: instanceManager,
+ meteringPointInfos: original.meteringPointInfos);
});
final List meteringPointInfosWithIds =
@@ -111,6 +126,6 @@ class _FocusMeteringActionHostApiImpl extends FocusMeteringActionHostApi {
meteringMode: meteringPointInfo.$2));
}
- create(identifier, meteringPointInfosWithIds);
+ create(identifier, meteringPointInfosWithIds, disableAutoCancel);
}
}
diff --git a/packages/camera/camera_android_camerax/lib/src/metering_point.dart b/packages/camera/camera_android_camerax/lib/src/metering_point.dart
index d699993a8933..c5ee6061de9c 100644
--- a/packages/camera/camera_android_camerax/lib/src/metering_point.dart
+++ b/packages/camera/camera_android_camerax/lib/src/metering_point.dart
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart' show BinaryMessenger;
import 'package:meta/meta.dart' show immutable;
import 'android_camera_camerax_flutter_api_impls.dart';
+import 'camera_info.dart';
import 'camerax_library.g.dart';
import 'instance_manager.dart';
import 'java_object.dart';
@@ -23,13 +24,14 @@ class MeteringPoint extends JavaObject {
required this.x,
required this.y,
this.size,
+ required this.cameraInfo,
}) : super.detached(
binaryMessenger: binaryMessenger,
instanceManager: instanceManager,
) {
_api = _MeteringPointHostApiImpl(
binaryMessenger: binaryMessenger, instanceManager: instanceManager);
- _api.createFromInstance(this, x, y, size);
+ _api.createFromInstance(this, x, y, size, cameraInfo);
AndroidCameraXCameraFlutterApis.instance.ensureSetUp();
}
@@ -41,6 +43,7 @@ class MeteringPoint extends JavaObject {
required this.x,
required this.y,
this.size,
+ required this.cameraInfo,
}) : super.detached(
binaryMessenger: binaryMessenger,
instanceManager: instanceManager,
@@ -63,6 +66,10 @@ class MeteringPoint extends JavaObject {
/// region width/height if crop region is set).
final double? size;
+ /// The [CameraInfo] used to construct the metering point with a display-
+ /// oriented metering point factory.
+ final CameraInfo cameraInfo;
+
/// The default size of the [MeteringPoint] width and height (ranging from 0
/// to 1) which is a (normalized) percentage of the sensor width/height (or
/// crop region width/height if crop region is set).
@@ -100,8 +107,8 @@ class _MeteringPointHostApiImpl extends MeteringPointHostApi {
/// Creates a [MeteringPoint] instance with the specified [x] and [y]
/// coordinates as well as [size] if non-null.
- Future createFromInstance(
- MeteringPoint instance, double x, double y, double? size) {
+ Future createFromInstance(MeteringPoint instance, double x, double y,
+ double? size, CameraInfo cameraInfo) {
int? identifier = instanceManager.getIdentifier(instance);
identifier ??= instanceManager.addDartCreatedInstance(instance,
onCopy: (MeteringPoint original) {
@@ -110,9 +117,11 @@ class _MeteringPointHostApiImpl extends MeteringPointHostApi {
instanceManager: instanceManager,
x: original.x,
y: original.y,
+ cameraInfo: original.cameraInfo,
size: original.size);
});
+ final int? camInfoId = instanceManager.getIdentifier(cameraInfo);
- return create(identifier, x, y, size);
+ return create(identifier, x, y, size, camInfoId!);
}
}
diff --git a/packages/camera/camera_android_camerax/lib/src/resolution_filter.dart b/packages/camera/camera_android_camerax/lib/src/resolution_filter.dart
new file mode 100644
index 000000000000..3a428a00e73a
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/resolution_filter.dart
@@ -0,0 +1,107 @@
+// 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.
+
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart' show immutable;
+
+import 'camerax_library.g.dart';
+import 'instance_manager.dart';
+import 'java_object.dart';
+
+/// Filterer for applications to specify preferred resolutions.
+///
+/// This is an indirect wrapping of the native Android `ResolutionFilter`,
+/// an interface that requires a synchronous response. Achieving such is not
+/// possible through pigeon. Thus, constructing a [ResolutionFilter] with a
+/// particular constructor will create a native `ResolutionFilter` with the
+/// characteristics described in the documentation for that constructor,
+/// respectively.
+///
+/// If the provided constructors do not meet your needs, feel free to add a new
+/// constructor; see CONTRIBUTING.MD for more information on how to do so.
+///
+/// See https://developer.android.com/reference/androidx/camera/core/ResolutionFilter/ResolutionFilter.
+@immutable
+class ResolutionFilter extends JavaObject {
+ /// Constructs a [ResolutionFilter].
+ ///
+ /// This will construct a native `ResolutionFilter` that will prioritize the
+ /// specified [preferredResolution] (if supported) over other supported
+ /// resolutions, whose priorities (as determined by CameraX) will remain the
+ /// same.
+ ResolutionFilter.onePreferredSize({
+ required this.preferredResolution,
+ super.binaryMessenger,
+ super.instanceManager,
+ }) : _api = _ResolutionFilterHostApiImpl(
+ instanceManager: instanceManager,
+ binaryMessenger: binaryMessenger,
+ ),
+ super.detached() {
+ _api.createWithOnePreferredSizeFromInstances(this, preferredResolution);
+ }
+
+ /// Instantiates a [ResolutionFilter.onePreferredSize] that is not
+ /// automatically attached to a native object.
+ ResolutionFilter.onePreferredSizeDetached({
+ required this.preferredResolution,
+ super.binaryMessenger,
+ super.instanceManager,
+ }) : _api = _ResolutionFilterHostApiImpl(
+ instanceManager: instanceManager,
+ binaryMessenger: binaryMessenger,
+ ),
+ super.detached();
+
+ final _ResolutionFilterHostApiImpl _api;
+
+ /// The resolution for a [ResolutionFilter.onePreferredSize] to prioritize.
+ final Size preferredResolution;
+}
+
+/// Host API implementation of [ResolutionFilter].
+class _ResolutionFilterHostApiImpl extends ResolutionFilterHostApi {
+ /// Constructs an [_ResolutionFilterHostApiImpl].
+ ///
+ /// If [binaryMessenger] is null, the default [BinaryMessenger] will be used,
+ /// which routes to the host platform.
+ ///
+ /// An [instanceManager] is typically passed when a copy of an instance
+ /// contained by an [InstanceManager] is being created. If left null, it
+ /// will default to the global instance defined in [JavaObject].
+ _ResolutionFilterHostApiImpl({
+ this.binaryMessenger,
+ InstanceManager? instanceManager,
+ }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
+ super(binaryMessenger: binaryMessenger);
+
+ /// Receives binary data across the Flutter platform barrier.
+ final BinaryMessenger? binaryMessenger;
+
+ /// Maintains instances stored to communicate with native language objects.
+ final InstanceManager instanceManager;
+
+ /// Creates a [ResolutionFilter] on the native side that will prioritize
+ /// the specified [preferredResolution].
+ Future createWithOnePreferredSizeFromInstances(
+ ResolutionFilter instance,
+ Size preferredResolution,
+ ) {
+ return createWithOnePreferredSize(
+ instanceManager.addDartCreatedInstance(
+ instance,
+ onCopy: (ResolutionFilter original) =>
+ ResolutionFilter.onePreferredSizeDetached(
+ preferredResolution: original.preferredResolution,
+ binaryMessenger: binaryMessenger,
+ instanceManager: instanceManager,
+ ),
+ ),
+ ResolutionInfo(
+ width: preferredResolution.width.toInt(),
+ height: preferredResolution.height.toInt(),
+ ),
+ );
+ }
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/resolution_selector.dart b/packages/camera/camera_android_camerax/lib/src/resolution_selector.dart
index 74191724e5ce..3017daab811a 100644
--- a/packages/camera/camera_android_camerax/lib/src/resolution_selector.dart
+++ b/packages/camera/camera_android_camerax/lib/src/resolution_selector.dart
@@ -9,6 +9,7 @@ import 'aspect_ratio_strategy.dart';
import 'camerax_library.g.dart';
import 'instance_manager.dart';
import 'java_object.dart';
+import 'resolution_filter.dart';
import 'resolution_strategy.dart';
/// A set of requirements and priorities used to select a resolution for a
@@ -20,6 +21,7 @@ class ResolutionSelector extends JavaObject {
/// Construct a [ResolutionSelector].
ResolutionSelector({
this.resolutionStrategy,
+ this.resolutionFilter,
this.aspectRatioStrategy,
super.binaryMessenger,
super.instanceManager,
@@ -28,7 +30,8 @@ class ResolutionSelector extends JavaObject {
binaryMessenger: binaryMessenger,
),
super.detached() {
- _api.createFromInstances(this, resolutionStrategy, aspectRatioStrategy);
+ _api.createFromInstances(
+ this, resolutionStrategy, resolutionFilter, aspectRatioStrategy);
}
/// Instantiates a [ResolutionSelector] without creating and attaching to an
@@ -38,6 +41,7 @@ class ResolutionSelector extends JavaObject {
/// library or to create a copy for an [InstanceManager].
ResolutionSelector.detached({
this.resolutionStrategy,
+ this.resolutionFilter,
this.aspectRatioStrategy,
super.binaryMessenger,
super.instanceManager,
@@ -53,6 +57,9 @@ class ResolutionSelector extends JavaObject {
/// image.
final ResolutionStrategy? resolutionStrategy;
+ /// Filter for CameraX to automatically select a desirable resolution.
+ final ResolutionFilter? resolutionFilter;
+
/// Determines how the UseCase will choose the aspect ratio of the captured
/// image.
final AspectRatioStrategy? aspectRatioStrategy;
@@ -81,10 +88,12 @@ class _ResolutionSelectorHostApiImpl extends ResolutionSelectorHostApi {
final InstanceManager instanceManager;
/// Creates a [ResolutionSelector] on the native side with the
- /// [ResolutionStrategy] and [AspectRatioStrategy] if specified.
+ /// [ResolutionStrategy], [ResolutionFilter], and [AspectRatioStrategy] if
+ /// specified.
Future createFromInstances(
ResolutionSelector instance,
ResolutionStrategy? resolutionStrategy,
+ ResolutionFilter? resolutionFilter,
AspectRatioStrategy? aspectRatioStrategy,
) {
return create(
@@ -100,6 +109,9 @@ class _ResolutionSelectorHostApiImpl extends ResolutionSelectorHostApi {
resolutionStrategy == null
? null
: instanceManager.getIdentifier(resolutionStrategy)!,
+ resolutionFilter == null
+ ? null
+ : instanceManager.getIdentifier(resolutionFilter)!,
aspectRatioStrategy == null
? null
: instanceManager.getIdentifier(aspectRatioStrategy)!,
diff --git a/packages/camera/camera_android_camerax/lib/src/zoom_state.dart b/packages/camera/camera_android_camerax/lib/src/zoom_state.dart
index 4c93d41e0249..6fe5321389b6 100644
--- a/packages/camera/camera_android_camerax/lib/src/zoom_state.dart
+++ b/packages/camera/camera_android_camerax/lib/src/zoom_state.dart
@@ -15,7 +15,7 @@ import 'java_object.dart';
/// See https://developer.android.com/reference/androidx/camera/core/ZoomState.
@immutable
class ZoomState extends JavaObject {
- /// Constructs a [CameraInfo] that is not automatically attached to a native object.
+ /// Constructs a [ZoomState] that is not automatically attached to a native object.
ZoomState.detached(
{super.binaryMessenger,
super.instanceManager,
diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
index db6164044d30..18741904a8d0 100644
--- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
+++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
@@ -66,7 +66,7 @@ class CameraStateTypeData {
/// If you need to add another type to support a type S to use a LiveData in
/// this plugin, ensure the following is done on the Dart side:
///
-/// * In `../lib/src/live_data.dart`, add new cases for S in
+/// * In `camera_android_camerax/lib/src/live_data.dart`, add new cases for S in
/// `_LiveDataHostApiImpl#getValueFromInstances` to get the current value of
/// type S from a LiveData instance and in `LiveDataFlutterApiImpl#create`
/// to create the expected type of LiveData when requested.
@@ -148,7 +148,7 @@ class MeteringPointInfo {
/// If you need to add another option to support, ensure the following is done
/// on the Dart side:
///
-/// * In `../lib/src/capture_request_options.dart`, add new cases for this
+/// * In `camera_android_camerax/lib/src/capture_request_options.dart`, add new cases for this
/// option in `_CaptureRequestOptionsHostApiImpl#createFromInstances`
/// to create the expected Map entry of option key index and value to send to
/// the native side.
@@ -366,6 +366,7 @@ abstract class ResolutionSelectorHostApi {
void create(
int identifier,
int? resolutionStrategyIdentifier,
+ int? resolutionSelectorIdentifier,
int? aspectRatioStrategyIdentifier,
);
}
@@ -485,13 +486,13 @@ abstract class CameraControlHostApi {
void setZoomRatio(int identifier, double ratio);
@async
- int startFocusAndMetering(int identifier, int focusMeteringActionId);
+ int? startFocusAndMetering(int identifier, int focusMeteringActionId);
@async
void cancelFocusAndMetering(int identifier);
@async
- int setExposureCompensationIndex(int identifier, int index);
+ int? setExposureCompensationIndex(int identifier, int index);
}
@FlutterApi()
@@ -501,7 +502,8 @@ abstract class CameraControlFlutterApi {
@HostApi(dartHostTestHandler: 'TestFocusMeteringActionHostApi')
abstract class FocusMeteringActionHostApi {
- void create(int identifier, List meteringPointInfos);
+ void create(int identifier, List meteringPointInfos,
+ bool? disableAutoCancel);
}
@HostApi(dartHostTestHandler: 'TestFocusMeteringResultHostApi')
@@ -516,7 +518,8 @@ abstract class FocusMeteringResultFlutterApi {
@HostApi(dartHostTestHandler: 'TestMeteringPointHostApi')
abstract class MeteringPointHostApi {
- void create(int identifier, double x, double y, double? size);
+ void create(
+ int identifier, double x, double y, double? size, int cameraInfoId);
double getDefaultPointSize();
}
@@ -534,3 +537,9 @@ abstract class Camera2CameraControlHostApi {
void addCaptureRequestOptions(
int identifier, int captureRequestOptionsIdentifier);
}
+
+@HostApi(dartHostTestHandler: 'TestResolutionFilterHostApi')
+abstract class ResolutionFilterHostApi {
+ void createWithOnePreferredSize(
+ int identifier, ResolutionInfo preferredResolution);
+}
diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml
index d6970d2e559e..cb53c9d6c636 100644
--- a/packages/camera/camera_android_camerax/pubspec.yaml
+++ b/packages/camera/camera_android_camerax/pubspec.yaml
@@ -2,7 +2,7 @@ name: camera_android_camerax
description: Android implementation of the camera plugin using the CameraX library.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.5.0+32
+version: 0.6.2
environment:
sdk: ^3.1.0
@@ -22,8 +22,6 @@ dependencies:
camera_platform_interface: ^2.3.2
flutter:
sdk: flutter
- integration_test:
- sdk: flutter
meta: ^1.7.0
stream_transform: ^2.1.0
@@ -31,6 +29,8 @@ dev_dependencies:
build_runner: ^2.2.0
flutter_test:
sdk: flutter
+ integration_test:
+ sdk: flutter
mockito: 5.4.4
pigeon: ^9.1.0
diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
index bc6a0d1c6a3b..f08a2e02ad49 100644
--- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
+++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
@@ -3,11 +3,14 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:math' show Point;
import 'package:async/async.dart';
import 'package:camera_android_camerax/camera_android_camerax.dart';
import 'package:camera_android_camerax/src/analyzer.dart';
+import 'package:camera_android_camerax/src/aspect_ratio_strategy.dart';
import 'package:camera_android_camerax/src/camera.dart';
+import 'package:camera_android_camerax/src/camera2_camera_control.dart';
import 'package:camera_android_camerax/src/camera_control.dart';
import 'package:camera_android_camerax/src/camera_info.dart';
import 'package:camera_android_camerax/src/camera_selector.dart';
@@ -15,13 +18,17 @@ import 'package:camera_android_camerax/src/camera_state.dart';
import 'package:camera_android_camerax/src/camera_state_error.dart';
import 'package:camera_android_camerax/src/camerax_library.g.dart';
import 'package:camera_android_camerax/src/camerax_proxy.dart';
+import 'package:camera_android_camerax/src/capture_request_options.dart';
import 'package:camera_android_camerax/src/device_orientation_manager.dart';
import 'package:camera_android_camerax/src/exposure_state.dart';
import 'package:camera_android_camerax/src/fallback_strategy.dart';
+import 'package:camera_android_camerax/src/focus_metering_action.dart';
+import 'package:camera_android_camerax/src/focus_metering_result.dart';
import 'package:camera_android_camerax/src/image_analysis.dart';
import 'package:camera_android_camerax/src/image_capture.dart';
import 'package:camera_android_camerax/src/image_proxy.dart';
import 'package:camera_android_camerax/src/live_data.dart';
+import 'package:camera_android_camerax/src/metering_point.dart';
import 'package:camera_android_camerax/src/observer.dart';
import 'package:camera_android_camerax/src/pending_recording.dart';
import 'package:camera_android_camerax/src/plane_proxy.dart';
@@ -30,6 +37,7 @@ import 'package:camera_android_camerax/src/process_camera_provider.dart';
import 'package:camera_android_camerax/src/quality_selector.dart';
import 'package:camera_android_camerax/src/recorder.dart';
import 'package:camera_android_camerax/src/recording.dart';
+import 'package:camera_android_camerax/src/resolution_filter.dart';
import 'package:camera_android_camerax/src/resolution_selector.dart';
import 'package:camera_android_camerax/src/resolution_strategy.dart';
import 'package:camera_android_camerax/src/surface.dart';
@@ -38,8 +46,9 @@ import 'package:camera_android_camerax/src/use_case.dart';
import 'package:camera_android_camerax/src/video_capture.dart';
import 'package:camera_android_camerax/src/zoom_state.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
-import 'package:flutter/services.dart' show DeviceOrientation, Uint8List;
-import 'package:flutter/widgets.dart';
+import 'package:flutter/services.dart'
+ show DeviceOrientation, PlatformException, Uint8List;
+import 'package:flutter/widgets.dart' show BuildContext, Size, Texture, Widget;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@@ -49,13 +58,16 @@ import 'test_camerax_library.g.dart';
@GenerateNiceMocks(>[
MockSpec(),
+ MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
+ MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
+ MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
@@ -66,6 +78,7 @@ import 'test_camerax_library.g.dart';
MockSpec(),
MockSpec(),
MockSpec(),
+ MockSpec(),
MockSpec(),
MockSpec(),
MockSpec(),
@@ -113,6 +126,115 @@ void main() {
return cameraClosingEventSent && cameraErrorSent;
}
+ /// CameraXProxy for testing functionality related to the camera resolution
+ /// preset (setting expected ResolutionSelectors, QualitySelectors, etc.).
+ CameraXProxy getProxyForTestingResolutionPreset(
+ MockProcessCameraProvider mockProcessCameraProvider) =>
+ CameraXProxy(
+ getProcessCameraProvider: () =>
+ Future.value(mockProcessCameraProvider),
+ createCameraSelector: (int cameraSelectorLensDirection) =>
+ MockCameraSelector(),
+ createPreview:
+ (ResolutionSelector? resolutionSelector, int? targetRotation) =>
+ Preview.detached(
+ initialTargetRotation: targetRotation,
+ resolutionSelector: resolutionSelector),
+ createImageCapture:
+ (ResolutionSelector? resolutionSelector, int? targetRotation) =>
+ ImageCapture.detached(
+ resolutionSelector: resolutionSelector,
+ initialTargetRotation: targetRotation),
+ createRecorder: (QualitySelector? qualitySelector) =>
+ Recorder.detached(qualitySelector: qualitySelector),
+ createVideoCapture: (_) =>
+ Future.value(MockVideoCapture()),
+ createImageAnalysis:
+ (ResolutionSelector? resolutionSelector, int? targetRotation) =>
+ ImageAnalysis.detached(
+ resolutionSelector: resolutionSelector,
+ initialTargetRotation: targetRotation),
+ createResolutionStrategy: (
+ {bool highestAvailable = false,
+ Size? boundSize,
+ int? fallbackRule}) {
+ if (highestAvailable) {
+ return ResolutionStrategy.detachedHighestAvailableStrategy();
+ }
+ return ResolutionStrategy.detached(
+ boundSize: boundSize, fallbackRule: fallbackRule);
+ },
+ createResolutionSelector: (ResolutionStrategy resolutionStrategy,
+ ResolutionFilter? resolutionFilter,
+ AspectRatioStrategy? aspectRatioStrategy) =>
+ ResolutionSelector.detached(
+ resolutionStrategy: resolutionStrategy,
+ resolutionFilter: resolutionFilter,
+ aspectRatioStrategy: aspectRatioStrategy),
+ createFallbackStrategy: (
+ {required VideoQuality quality,
+ required VideoResolutionFallbackRule fallbackRule}) =>
+ FallbackStrategy.detached(
+ quality: quality, fallbackRule: fallbackRule),
+ createQualitySelector: (
+ {required VideoQuality videoQuality,
+ required FallbackStrategy fallbackStrategy}) =>
+ QualitySelector.detached(qualityList: [
+ VideoQualityData(quality: videoQuality)
+ ], fallbackStrategy: fallbackStrategy),
+ createCameraStateObserver: (_) => MockObserver(),
+ requestCameraPermissions: (_) => Future.value(),
+ startListeningForDeviceOrientationChange: (_, __) {},
+ setPreviewSurfaceProvider: (_) => Future.value(
+ 3), // 3 is a random Flutter SurfaceTexture ID for testing,
+ createAspectRatioStrategy: (int aspectRatio, int fallbackRule) =>
+ AspectRatioStrategy.detached(
+ preferredAspectRatio: aspectRatio, fallbackRule: fallbackRule),
+ createResolutionFilterWithOnePreferredSize:
+ (Size preferredResolution) =>
+ ResolutionFilter.onePreferredSizeDetached(
+ preferredResolution: preferredResolution),
+ );
+
+ /// CameraXProxy for testing exposure and focus related controls.
+ ///
+ /// Modifies the creation of [MeteringPoint]s and [FocusMeteringAction]s to
+ /// return objects detached from a native object.
+ CameraXProxy getProxyForExposureAndFocus() => CameraXProxy(
+ createMeteringPoint:
+ (double x, double y, double? size, CameraInfo cameraInfo) =>
+ MeteringPoint.detached(
+ x: x, y: y, size: size, cameraInfo: cameraInfo),
+ createFocusMeteringAction:
+ (List<(MeteringPoint, int?)> meteringPointInfos,
+ bool? disableAutoCancel) =>
+ FocusMeteringAction.detached(
+ meteringPointInfos: meteringPointInfos,
+ disableAutoCancel: disableAutoCancel),
+ );
+
+ /// CameraXProxy for testing setting focus and exposure points.
+ ///
+ /// Modifies the retrieval of a [Camera2CameraControl] instance to depend on
+ /// interaction with expected [cameraControl] instance and modifies creation
+ /// of [CaptureRequestOptions] to return objects detached from a native object.
+ CameraXProxy getProxyForSettingFocusandExposurePoints(
+ CameraControl cameraControlForComparison,
+ Camera2CameraControl camera2cameraControl) {
+ final CameraXProxy proxy = getProxyForExposureAndFocus();
+
+ proxy.getCamera2CameraControl = (CameraControl cameraControl) =>
+ cameraControl == cameraControlForComparison
+ ? camera2cameraControl
+ : Camera2CameraControl.detached(cameraControl: cameraControl);
+
+ proxy.createCaptureRequestOptions =
+ (List<(CaptureRequestKeySupportedType, Object?)> options) =>
+ CaptureRequestOptions.detached(requestedOptions: options);
+
+ return proxy;
+ }
+
test('Should fetch CameraDescription instances for available cameras',
() async {
// Arrange
@@ -241,7 +363,7 @@ void main() {
Size? boundSize,
int? fallbackRule}) =>
MockResolutionStrategy(),
- createResolutionSelector: (_) => MockResolutionSelector(),
+ createResolutionSelector: (_, __, ___) => MockResolutionSelector(),
createFallbackStrategy: (
{required VideoQuality quality,
required VideoResolutionFallbackRule fallbackRule}) =>
@@ -259,6 +381,8 @@ void main() {
startListeningForDeviceOrientationChange: (_, __) {
startedListeningForDeviceOrientationChanges = true;
},
+ createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(),
+ createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(),
);
when(mockPreview.setSurfaceProvider())
@@ -331,6 +455,7 @@ void main() {
final MockVideoCapture mockVideoCapture = MockVideoCapture();
final MockCamera mockCamera = MockCamera();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final MockCameraControl mockCameraControl = MockCameraControl();
// Tell plugin to create mock/detached objects and stub method calls for the
// testing of createCamera.
@@ -356,7 +481,7 @@ void main() {
Size? boundSize,
int? fallbackRule}) =>
MockResolutionStrategy(),
- createResolutionSelector: (_) => MockResolutionSelector(),
+ createResolutionSelector: (_, __, ___) => MockResolutionSelector(),
createFallbackStrategy: (
{required VideoQuality quality,
required VideoResolutionFallbackRule fallbackRule}) =>
@@ -369,6 +494,8 @@ void main() {
Observer.detached(onChanged: onChanged),
requestCameraPermissions: (_) => Future.value(),
startListeningForDeviceOrientationChange: (_, __) {},
+ createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(),
+ createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(),
);
when(mockProcessCameraProvider.bindToLifecycle(mockBackCameraSelector,
@@ -377,6 +504,8 @@ void main() {
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
.thenAnswer((_) async => MockLiveCameraState());
+ when(mockCamera.getCameraControl())
+ .thenAnswer((_) async => mockCameraControl);
camera.processCameraProvider = mockProcessCameraProvider;
await camera.createCamera(testCameraDescription, testResolutionPreset,
@@ -389,13 +518,16 @@ void main() {
// Verify the camera's CameraInfo instance got updated.
expect(camera.cameraInfo, equals(mockCameraInfo));
+ // Verify camera's CameraControl instance got updated.
+ expect(camera.cameraControl, equals(mockCameraControl));
+
// Verify preview has been marked as bound to the camera lifecycle by
// createCamera.
expect(camera.previewInitiallyBound, isTrue);
});
test(
- 'createCamera properly sets preset resolution for non-video capture use cases',
+ 'createCamera properly sets preset resolution selection strategy for non-video capture use cases',
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const CameraLensDirection testLensDirection = CameraLensDirection.back;
@@ -412,69 +544,13 @@ void main() {
final MockProcessCameraProvider mockProcessCameraProvider =
MockProcessCameraProvider();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
- final MockCameraSelector mockBackCameraSelector = MockCameraSelector();
- final MockCameraSelector mockFrontCameraSelector = MockCameraSelector();
- final MockVideoCapture mockVideoCapture = MockVideoCapture();
- final MockRecorder mockRecorder = MockRecorder();
// Tell plugin to create mock/detached objects for testing createCamera
// as needed.
- camera.proxy = CameraXProxy(
- getProcessCameraProvider: () =>
- Future.value(mockProcessCameraProvider),
- createCameraSelector: (int cameraSelectorLensDirection) {
- switch (cameraSelectorLensDirection) {
- case CameraSelector.lensFacingFront:
- return mockFrontCameraSelector;
- case CameraSelector.lensFacingBack:
- default:
- return mockBackCameraSelector;
- }
- },
- createPreview:
- (ResolutionSelector? resolutionSelector, int? targetRotation) =>
- Preview.detached(
- initialTargetRotation: targetRotation,
- resolutionSelector: resolutionSelector),
- createImageCapture:
- (ResolutionSelector? resolutionSelector, int? targetRotation) =>
- ImageCapture.detached(
- resolutionSelector: resolutionSelector,
- initialTargetRotation: targetRotation),
- createRecorder: (_) => mockRecorder,
- createVideoCapture: (_) => Future.value(mockVideoCapture),
- createImageAnalysis:
- (ResolutionSelector? resolutionSelector, int? targetRotation) =>
- ImageAnalysis.detached(
- resolutionSelector: resolutionSelector,
- initialTargetRotation: targetRotation),
- createResolutionStrategy: (
- {bool highestAvailable = false, Size? boundSize, int? fallbackRule}) {
- if (highestAvailable) {
- return ResolutionStrategy.detachedHighestAvailableStrategy();
- }
-
- return ResolutionStrategy.detached(
- boundSize: boundSize, fallbackRule: fallbackRule);
- },
- createResolutionSelector: (ResolutionStrategy resolutionStrategy) =>
- ResolutionSelector.detached(resolutionStrategy: resolutionStrategy),
- createFallbackStrategy: (
- {required VideoQuality quality,
- required VideoResolutionFallbackRule fallbackRule}) =>
- MockFallbackStrategy(),
- createQualitySelector: (
- {required VideoQuality videoQuality,
- required FallbackStrategy fallbackStrategy}) =>
- MockQualitySelector(),
- createCameraStateObserver: (_) => MockObserver(),
- requestCameraPermissions: (_) => Future.value(),
- startListeningForDeviceOrientationChange: (_, __) {},
- setPreviewSurfaceProvider: (_) => Future.value(
- 3), // 3 is a random Flutter SurfaceTexture ID for testing},
- );
+ camera.proxy =
+ getProxyForTestingResolutionPreset(mockProcessCameraProvider);
- when(mockProcessCameraProvider.bindToLifecycle(mockBackCameraSelector, any))
+ when(mockProcessCameraProvider.bindToLifecycle(any, any))
.thenAnswer((_) async => mockCamera);
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
@@ -541,6 +617,189 @@ void main() {
expect(camera.imageAnalysis!.resolutionSelector, isNull);
});
+ test(
+ 'createCamera properly sets filter for resolution preset for non-video capture use cases',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const CameraLensDirection testLensDirection = CameraLensDirection.front;
+ const int testSensorOrientation = 180;
+ const CameraDescription testCameraDescription = CameraDescription(
+ name: 'cameraName',
+ lensDirection: testLensDirection,
+ sensorOrientation: testSensorOrientation);
+ const bool enableAudio = true;
+ final MockCamera mockCamera = MockCamera();
+
+ // Mock/Detached objects for (typically attached) objects created by
+ // createCamera.
+ final MockProcessCameraProvider mockProcessCameraProvider =
+ MockProcessCameraProvider();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Tell plugin to create mock/detached objects for testing createCamera
+ // as needed.
+ camera.proxy =
+ getProxyForTestingResolutionPreset(mockProcessCameraProvider);
+
+ when(mockProcessCameraProvider.bindToLifecycle(any, any))
+ .thenAnswer((_) async => mockCamera);
+ when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
+ when(mockCameraInfo.getCameraState())
+ .thenAnswer((_) async => MockLiveCameraState());
+ camera.processCameraProvider = mockProcessCameraProvider;
+
+ // Test non-null resolution presets.
+ for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) {
+ await camera.createCamera(testCameraDescription, resolutionPreset,
+ enableAudio: enableAudio);
+
+ Size? expectedPreferredResolution;
+ switch (resolutionPreset) {
+ case ResolutionPreset.low:
+ expectedPreferredResolution = const Size(320, 240);
+ case ResolutionPreset.medium:
+ expectedPreferredResolution = const Size(720, 480);
+ case ResolutionPreset.high:
+ expectedPreferredResolution = const Size(1280, 720);
+ case ResolutionPreset.veryHigh:
+ expectedPreferredResolution = const Size(1920, 1080);
+ case ResolutionPreset.ultraHigh:
+ expectedPreferredResolution = const Size(3840, 2160);
+ case ResolutionPreset.max:
+ expectedPreferredResolution = null;
+ }
+
+ if (expectedPreferredResolution == null) {
+ expect(camera.preview!.resolutionSelector!.resolutionFilter, isNull);
+ expect(
+ camera.imageCapture!.resolutionSelector!.resolutionFilter, isNull);
+ expect(
+ camera.imageAnalysis!.resolutionSelector!.resolutionFilter, isNull);
+ continue;
+ }
+
+ expect(
+ camera.preview!.resolutionSelector!.resolutionFilter!
+ .preferredResolution,
+ equals(expectedPreferredResolution));
+ expect(
+ camera
+ .imageCapture!.resolutionSelector!.resolutionStrategy!.boundSize,
+ equals(expectedPreferredResolution));
+ expect(
+ camera
+ .imageAnalysis!.resolutionSelector!.resolutionStrategy!.boundSize,
+ equals(expectedPreferredResolution));
+ }
+
+ // Test null case.
+ await camera.createCamera(testCameraDescription, null);
+ expect(camera.preview!.resolutionSelector, isNull);
+ expect(camera.imageCapture!.resolutionSelector, isNull);
+ expect(camera.imageAnalysis!.resolutionSelector, isNull);
+ });
+
+ test(
+ 'createCamera properly sets aspect ratio based on preset resolution for non-video capture use cases',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const CameraLensDirection testLensDirection = CameraLensDirection.back;
+ const int testSensorOrientation = 90;
+ const CameraDescription testCameraDescription = CameraDescription(
+ name: 'cameraName',
+ lensDirection: testLensDirection,
+ sensorOrientation: testSensorOrientation);
+ const bool enableAudio = true;
+ final MockCamera mockCamera = MockCamera();
+
+ // Mock/Detached objects for (typically attached) objects created by
+ // createCamera.
+ final MockProcessCameraProvider mockProcessCameraProvider =
+ MockProcessCameraProvider();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Tell plugin to create mock/detached objects for testing createCamera
+ // as needed.
+ camera.proxy =
+ getProxyForTestingResolutionPreset(mockProcessCameraProvider);
+ when(mockProcessCameraProvider.bindToLifecycle(any, any))
+ .thenAnswer((_) async => mockCamera);
+ when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
+ when(mockCameraInfo.getCameraState())
+ .thenAnswer((_) async => MockLiveCameraState());
+ camera.processCameraProvider = mockProcessCameraProvider;
+
+ // Test non-null resolution presets.
+ for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) {
+ await camera.createCamera(testCameraDescription, resolutionPreset,
+ enableAudio: enableAudio);
+
+ int? expectedAspectRatio;
+ AspectRatioStrategy? expectedAspectRatioStrategy;
+ switch (resolutionPreset) {
+ case ResolutionPreset.low:
+ expectedAspectRatio = AspectRatio.ratio4To3;
+ case ResolutionPreset.high:
+ case ResolutionPreset.veryHigh:
+ case ResolutionPreset.ultraHigh:
+ expectedAspectRatio = AspectRatio.ratio16To9;
+ case ResolutionPreset.medium:
+ // Medium resolution preset uses aspect ratio 3:2 which is unsupported
+ // by CameraX.
+ case ResolutionPreset.max:
+ expectedAspectRatioStrategy = null;
+ }
+
+ expectedAspectRatioStrategy = expectedAspectRatio == null
+ ? null
+ : AspectRatioStrategy.detached(
+ preferredAspectRatio: expectedAspectRatio,
+ fallbackRule: AspectRatioStrategy.fallbackRuleAuto);
+
+ if (expectedAspectRatio == null) {
+ expect(camera.preview!.resolutionSelector!.aspectRatioStrategy, isNull);
+ expect(camera.imageCapture!.resolutionSelector!.aspectRatioStrategy,
+ isNull);
+ expect(camera.imageAnalysis!.resolutionSelector!.aspectRatioStrategy,
+ isNull);
+ continue;
+ }
+
+ // Check aspect ratio.
+ expect(
+ camera.preview!.resolutionSelector!.aspectRatioStrategy!
+ .preferredAspectRatio,
+ equals(expectedAspectRatioStrategy!.preferredAspectRatio));
+ expect(
+ camera.imageCapture!.resolutionSelector!.aspectRatioStrategy!
+ .preferredAspectRatio,
+ equals(expectedAspectRatioStrategy.preferredAspectRatio));
+ expect(
+ camera.imageAnalysis!.resolutionSelector!.aspectRatioStrategy!
+ .preferredAspectRatio,
+ equals(expectedAspectRatioStrategy.preferredAspectRatio));
+
+ // Check fallback rule.
+ expect(
+ camera.preview!.resolutionSelector!.aspectRatioStrategy!.fallbackRule,
+ equals(expectedAspectRatioStrategy.fallbackRule));
+ expect(
+ camera.imageCapture!.resolutionSelector!.aspectRatioStrategy!
+ .fallbackRule,
+ equals(expectedAspectRatioStrategy.fallbackRule));
+ expect(
+ camera.imageAnalysis!.resolutionSelector!.aspectRatioStrategy!
+ .fallbackRule,
+ equals(expectedAspectRatioStrategy.fallbackRule));
+ }
+
+ // Test null case.
+ await camera.createCamera(testCameraDescription, null);
+ expect(camera.preview!.resolutionSelector, isNull);
+ expect(camera.imageCapture!.resolutionSelector, isNull);
+ expect(camera.imageAnalysis!.resolutionSelector, isNull);
+ });
+
test(
'createCamera properly sets preset resolution for video capture use case',
() async {
@@ -559,58 +818,13 @@ void main() {
final MockProcessCameraProvider mockProcessCameraProvider =
MockProcessCameraProvider();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
- final MockCameraSelector mockBackCameraSelector = MockCameraSelector();
- final MockCameraSelector mockFrontCameraSelector = MockCameraSelector();
- final MockPreview mockPreview = MockPreview();
- final MockImageCapture mockImageCapture = MockImageCapture();
- final MockVideoCapture mockVideoCapture = MockVideoCapture();
- final MockImageAnalysis mockImageAnalysis = MockImageAnalysis();
// Tell plugin to create mock/detached objects for testing createCamera
// as needed.
- camera.proxy = CameraXProxy(
- getProcessCameraProvider: () =>
- Future.value(mockProcessCameraProvider),
- createCameraSelector: (int cameraSelectorLensDirection) {
- switch (cameraSelectorLensDirection) {
- case CameraSelector.lensFacingFront:
- return mockFrontCameraSelector;
- case CameraSelector.lensFacingBack:
- default:
- return mockBackCameraSelector;
- }
- },
- createPreview: (_, __) => mockPreview,
- createImageCapture: (_, __) => mockImageCapture,
- createRecorder: (QualitySelector? qualitySelector) =>
- Recorder.detached(qualitySelector: qualitySelector),
- createVideoCapture: (_) => Future.value(mockVideoCapture),
- createImageAnalysis: (_, __) => mockImageAnalysis,
- createResolutionStrategy: (
- {bool highestAvailable = false,
- Size? boundSize,
- int? fallbackRule}) =>
- MockResolutionStrategy(),
- createResolutionSelector: (_) => MockResolutionSelector(),
- createFallbackStrategy: (
- {required VideoQuality quality,
- required VideoResolutionFallbackRule fallbackRule}) =>
- FallbackStrategy.detached(
- quality: quality, fallbackRule: fallbackRule),
- createQualitySelector: (
- {required VideoQuality videoQuality,
- required FallbackStrategy fallbackStrategy}) =>
- QualitySelector.detached(qualityList: [
- VideoQualityData(quality: videoQuality)
- ], fallbackStrategy: fallbackStrategy),
- createCameraStateObserver: (void Function(Object) onChanged) =>
- Observer.detached(onChanged: onChanged),
- requestCameraPermissions: (_) => Future.value(),
- startListeningForDeviceOrientationChange: (_, __) {},
- );
+ camera.proxy =
+ getProxyForTestingResolutionPreset(mockProcessCameraProvider);
- when(mockProcessCameraProvider.bindToLifecycle(mockBackCameraSelector,
- [mockPreview, mockImageCapture, mockImageAnalysis]))
+ when(mockProcessCameraProvider.bindToLifecycle(any, any))
.thenAnswer((_) async => mockCamera);
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
@@ -719,7 +933,7 @@ void main() {
Size? boundSize,
int? fallbackRule}) =>
MockResolutionStrategy(),
- createResolutionSelector: (_) => MockResolutionSelector(),
+ createResolutionSelector: (_, __, ___) => MockResolutionSelector(),
createFallbackStrategy: (
{required VideoQuality quality,
required VideoResolutionFallbackRule fallbackRule}) =>
@@ -732,21 +946,19 @@ void main() {
Observer.detached(onChanged: onChanged),
requestCameraPermissions: (_) => Future.value(),
startListeningForDeviceOrientationChange: (_, __) {},
+ createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(),
+ createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(),
);
- // TODO(camsim99): Modify this when camera configuration is supported and
- // default values no longer being used.
- // https://github.com/flutter/flutter/issues/120468
- // https://github.com/flutter/flutter/issues/120467
final CameraInitializedEvent testCameraInitializedEvent =
CameraInitializedEvent(
cameraId,
resolutionWidth.toDouble(),
resolutionHeight.toDouble(),
ExposureMode.auto,
- false,
+ true,
FocusMode.auto,
- false);
+ true);
// Call createCamera.
when(mockPreview.setSurfaceProvider()).thenAnswer((_) async => cameraId);
@@ -940,6 +1152,7 @@ void main() {
MockProcessCameraProvider();
final MockCamera mockCamera = MockCamera();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final MockCameraControl mockCameraControl = MockCameraControl();
final MockLiveCameraState mockLiveCameraState = MockLiveCameraState();
// Set directly for test versus calling createCamera.
@@ -961,6 +1174,8 @@ void main() {
when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
when(mockCameraInfo.getCameraState())
.thenAnswer((_) async => mockLiveCameraState);
+ when(mockCamera.getCameraControl())
+ .thenAnswer((_) async => mockCameraControl);
await camera.resumePreview(78);
@@ -974,6 +1189,7 @@ void main() {
as Observer),
isTrue);
expect(camera.cameraInfo, equals(mockCameraInfo));
+ expect(camera.cameraControl, equals(mockCameraControl));
});
test(
@@ -1017,6 +1233,7 @@ void main() {
final MockCamera mockCamera = MockCamera();
final MockCamera newMockCamera = MockCamera();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final MockCameraControl mockCameraControl = MockCameraControl();
final MockLiveCameraState mockLiveCameraState = MockLiveCameraState();
final MockLiveCameraState newMockLiveCameraState = MockLiveCameraState();
final TestSystemServicesHostApi mockSystemServicesApi =
@@ -1055,6 +1272,8 @@ void main() {
.thenAnswer((_) async => newMockCamera);
when(newMockCamera.getCameraInfo())
.thenAnswer((_) async => mockCameraInfo);
+ when(newMockCamera.getCameraControl())
+ .thenAnswer((_) async => mockCameraControl);
when(mockCameraInfo.getCameraState())
.thenAnswer((_) async => newMockLiveCameraState);
@@ -1066,6 +1285,7 @@ void main() {
camera.cameraSelector!, [camera.videoCapture!]));
expect(camera.camera, equals(newMockCamera));
expect(camera.cameraInfo, equals(mockCameraInfo));
+ expect(camera.cameraControl, equals(mockCameraControl));
verify(mockLiveCameraState.removeObservers());
expect(
await testCameraClosingObserver(
@@ -1453,18 +1673,14 @@ void main() {
() async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
const int cameraId = 77;
- final MockCameraControl mockCameraControl = MockCameraControl();
// Set directly for test versus calling createCamera.
camera.imageCapture = MockImageCapture();
- camera.camera = MockCamera();
+ camera.cameraControl = MockCameraControl();
// Ignore setting target rotation for this test; tested seprately.
camera.captureOrientationLocked = true;
- when(camera.camera!.getCameraControl())
- .thenAnswer((_) async => mockCameraControl);
-
await camera.setFlashMode(cameraId, FlashMode.torch);
await camera.takePicture(cameraId);
verify(camera.imageCapture!.setFlashMode(ImageCapture.flashModeOff));
@@ -1479,14 +1695,11 @@ void main() {
// Set directly for test versus calling createCamera.
camera.imageCapture = MockImageCapture();
- camera.camera = MockCamera();
+ camera.cameraControl = mockCameraControl;
// Ignore setting target rotation for this test; tested seprately.
camera.captureOrientationLocked = true;
- when(camera.camera!.getCameraControl())
- .thenAnswer((_) async => mockCameraControl);
-
for (final FlashMode flashMode in FlashMode.values) {
await camera.setFlashMode(cameraId, flashMode);
@@ -1520,10 +1733,7 @@ void main() {
final MockCameraControl mockCameraControl = MockCameraControl();
// Set directly for test versus calling createCamera.
- camera.camera = MockCamera();
-
- when(camera.camera!.getCameraControl())
- .thenAnswer((_) async => mockCameraControl);
+ camera.cameraControl = mockCameraControl;
await camera.setFlashMode(cameraId, FlashMode.torch);
@@ -1538,10 +1748,7 @@ void main() {
final MockCameraControl mockCameraControl = MockCameraControl();
// Set directly for test versus calling createCamera.
- camera.camera = MockCamera();
-
- when(camera.camera!.getCameraControl())
- .thenAnswer((_) async => mockCameraControl);
+ camera.cameraControl = mockCameraControl;
for (final FlashMode flashMode in FlashMode.values) {
camera.torchEnabled = true;
@@ -1614,6 +1821,25 @@ void main() {
expect(await camera.getExposureOffsetStepSize(55), 0.2);
});
+ test(
+ 'getExposureOffsetStepSize returns -1 when exposure compensation not supported on device',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final ExposureState exposureState = ExposureState.detached(
+ exposureCompensationRange:
+ ExposureCompensationRange(minCompensation: 0, maxCompensation: 0),
+ exposureCompensationStep: 0);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = mockCameraInfo;
+
+ when(mockCameraInfo.getExposureState())
+ .thenAnswer((_) async => exposureState);
+
+ expect(await camera.getExposureOffsetStepSize(55), -1);
+ });
+
test('getMaxZoomLevel returns expected exposure offset', () async {
final AndroidCameraCameraX camera = AndroidCameraCameraX();
final MockCameraInfo mockCameraInfo = MockCameraInfo();
@@ -1657,10 +1883,7 @@ void main() {
final MockCameraControl mockCameraControl = MockCameraControl();
// Set directly for test versus calling createCamera.
- camera.camera = MockCamera();
-
- when(camera.camera!.getCameraControl())
- .thenAnswer((_) async => mockCameraControl);
+ camera.cameraControl = mockCameraControl;
await camera.setZoomLevel(cameraId, zoomRatio);
@@ -1981,4 +2204,1272 @@ void main() {
await camera.unlockCaptureOrientation(cameraId);
expect(camera.captureOrientationLocked, isFalse);
});
+
+ test('setExposureMode sets expected controlAeLock value via Camera2 interop',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 78;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+
+ // Set directly for test versus calling createCamera.
+ camera.camera = MockCamera();
+ camera.cameraControl = mockCameraControl;
+
+ // Tell plugin to create detached Camera2CameraControl and
+ // CaptureRequestOptions instances for testing.
+ camera.proxy = CameraXProxy(
+ getCamera2CameraControl: (CameraControl cameraControl) =>
+ cameraControl == mockCameraControl
+ ? mockCamera2CameraControl
+ : Camera2CameraControl.detached(cameraControl: cameraControl),
+ createCaptureRequestOptions:
+ (List<(CaptureRequestKeySupportedType, Object?)> options) =>
+ CaptureRequestOptions.detached(requestedOptions: options),
+ );
+
+ // Test auto mode.
+ await camera.setExposureMode(cameraId, ExposureMode.auto);
+
+ VerificationResult verificationResult =
+ verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
+ CaptureRequestOptions capturedCaptureRequestOptions =
+ verificationResult.captured.single as CaptureRequestOptions;
+ List<(CaptureRequestKeySupportedType, Object?)> requestedOptions =
+ capturedCaptureRequestOptions.requestedOptions;
+ expect(requestedOptions.length, equals(1));
+ expect(requestedOptions.first.$1,
+ equals(CaptureRequestKeySupportedType.controlAeLock));
+ expect(requestedOptions.first.$2, equals(false));
+
+ // Test locked mode.
+ clearInteractions(mockCamera2CameraControl);
+ await camera.setExposureMode(cameraId, ExposureMode.locked);
+
+ verificationResult =
+ verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
+ capturedCaptureRequestOptions =
+ verificationResult.captured.single as CaptureRequestOptions;
+ requestedOptions = capturedCaptureRequestOptions.requestedOptions;
+ expect(requestedOptions.length, equals(1));
+ expect(requestedOptions.first.$1,
+ equals(CaptureRequestKeySupportedType.controlAeLock));
+ expect(requestedOptions.first.$2, equals(true));
+ });
+
+ test(
+ 'setExposurePoint clears current auto-exposure metering point as expected',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 93;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = mockCameraInfo;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ // Verify nothing happens if no current focus and metering action has been
+ // enabled.
+ await camera.setExposurePoint(cameraId, null);
+ verifyNever(mockCameraControl.startFocusAndMetering(any));
+ verifyNever(mockCameraControl.cancelFocusAndMetering());
+
+ // Verify current auto-exposure metering point is removed if previously set.
+ final (MeteringPoint, int?) autofocusMeteringPointInfo = (
+ MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAf
+ );
+ List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAe
+ ),
+ autofocusMeteringPointInfo
+ ];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setExposurePoint(cameraId, null);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(
+ capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo));
+
+ // Verify current focus and metering action is cleared if only previously
+ // set metering point was for auto-exposure.
+ meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAe
+ )
+ ];
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setExposurePoint(cameraId, null);
+
+ verify(mockCameraControl.cancelFocusAndMetering());
+ });
+
+ test('setExposurePoint throws CameraException if invalid point specified',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 23;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ const Point invalidExposurePoint = Point(3, -1);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ expect(() => camera.setExposurePoint(cameraId, invalidExposurePoint),
+ throwsA(isA()));
+ });
+
+ test(
+ 'setExposurePoint adds new exposure point to focus metering action to start as expected when previous metering points have been set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 9;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = mockCameraInfo;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ // Verify current auto-exposure metering point is removed if previously set.
+ double exposurePointX = 0.8;
+ double exposurePointY = 0.1;
+ Point exposurePoint = Point(exposurePointX, exposurePointY);
+ final (MeteringPoint, int?) autofocusMeteringPointInfo = (
+ MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAf
+ );
+ List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAe
+ ),
+ autofocusMeteringPointInfo
+ ];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setExposurePoint(cameraId, exposurePoint);
+
+ VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+ expect(
+ capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo));
+ expect(capturedMeteringPointInfos[1].$1.x, equals(exposurePointX));
+ expect(capturedMeteringPointInfos[1].$1.y, equals(exposurePointY));
+ expect(
+ capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAe));
+
+ // Verify exposure point is set when no auto-exposure metering point
+ // previously set, but an auto-focus point metering point has been.
+ exposurePointX = 0.2;
+ exposurePointY = 0.9;
+ exposurePoint = Point(exposurePointX, exposurePointY);
+ meteringPointInfos = <(MeteringPoint, int?)>[autofocusMeteringPointInfo];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setExposurePoint(cameraId, exposurePoint);
+
+ verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ capturedAction = verificationResult.captured.single as FocusMeteringAction;
+ capturedMeteringPointInfos = capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+ expect(
+ capturedMeteringPointInfos.first, equals(autofocusMeteringPointInfo));
+ expect(capturedMeteringPointInfos[1].$1.x, equals(exposurePointX));
+ expect(capturedMeteringPointInfos[1].$1.y, equals(exposurePointY));
+ expect(
+ capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAe));
+ });
+
+ test(
+ 'setExposurePoint adds new exposure point to focus metering action to start as expected when no previous metering points have been set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 19;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ const double exposurePointX = 0.8;
+ const double exposurePointY = 0.1;
+ const Point exposurePoint =
+ Point(exposurePointX, exposurePointY);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+ camera.currentFocusMeteringAction = null;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ await camera.setExposurePoint(cameraId, exposurePoint);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(exposurePointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(exposurePointY));
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAe));
+ });
+
+ test(
+ 'setExposurePoint disables auto-cancel for focus and metering as expected',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 2;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final FocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ const Point exposurePoint = Point(0.1, 0.2);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, MockCamera2CameraControl());
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Test not disabling auto cancel.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+ clearInteractions(mockCameraControl);
+ await camera.setExposurePoint(cameraId, exposurePoint);
+ VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isFalse);
+
+ clearInteractions(mockCameraControl);
+
+ // Test disabling auto cancel.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ clearInteractions(mockCameraControl);
+ await camera.setExposurePoint(cameraId, exposurePoint);
+ verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ capturedAction = verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+ });
+
+ test(
+ 'setExposureOffset throws exception if exposure compensation not supported',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 6;
+ const double offset = 2;
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final ExposureState exposureState = ExposureState.detached(
+ exposureCompensationRange:
+ ExposureCompensationRange(minCompensation: 3, maxCompensation: 4),
+ exposureCompensationStep: 0);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = mockCameraInfo;
+
+ when(mockCameraInfo.getExposureState())
+ .thenAnswer((_) async => exposureState);
+
+ expect(() => camera.setExposureOffset(cameraId, offset),
+ throwsA(isA()));
+ });
+
+ test(
+ 'setExposureOffset throws exception if exposure compensation could not be set for unknown reason',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 11;
+ const double offset = 3;
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final CameraControl mockCameraControl = MockCameraControl();
+ final ExposureState exposureState = ExposureState.detached(
+ exposureCompensationRange:
+ ExposureCompensationRange(minCompensation: 3, maxCompensation: 4),
+ exposureCompensationStep: 0.2);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = mockCameraInfo;
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCameraInfo.getExposureState())
+ .thenAnswer((_) async => exposureState);
+ when(mockCameraControl.setExposureCompensationIndex(15)).thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message:
+ 'This is a test error message indicating exposure offset could not be set.'));
+
+ expect(() => camera.setExposureOffset(cameraId, offset),
+ throwsA(isA()));
+ });
+
+ test(
+ 'setExposureOffset throws exception if exposure compensation could not be set due to camera being closed or newer value being set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 21;
+ const double offset = 5;
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final CameraControl mockCameraControl = MockCameraControl();
+ final ExposureState exposureState = ExposureState.detached(
+ exposureCompensationRange:
+ ExposureCompensationRange(minCompensation: 3, maxCompensation: 4),
+ exposureCompensationStep: 0.1);
+ final int expectedExposureCompensationIndex =
+ (offset / exposureState.exposureCompensationStep).round();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = mockCameraInfo;
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCameraInfo.getExposureState())
+ .thenAnswer((_) async => exposureState);
+ when(mockCameraControl
+ .setExposureCompensationIndex(expectedExposureCompensationIndex))
+ .thenAnswer((_) async => Future.value());
+
+ expect(() => camera.setExposureOffset(cameraId, offset),
+ throwsA(isA()));
+ });
+
+ test(
+ 'setExposureOffset behaves as expected to successful attempt to set exposure compensation index',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 11;
+ const double offset = 3;
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+ final CameraControl mockCameraControl = MockCameraControl();
+ final ExposureState exposureState = ExposureState.detached(
+ exposureCompensationRange:
+ ExposureCompensationRange(minCompensation: 3, maxCompensation: 4),
+ exposureCompensationStep: 0.2);
+ final int expectedExposureCompensationIndex =
+ (offset / exposureState.exposureCompensationStep).round();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = mockCameraInfo;
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCameraInfo.getExposureState())
+ .thenAnswer((_) async => exposureState);
+ when(mockCameraControl
+ .setExposureCompensationIndex(expectedExposureCompensationIndex))
+ .thenAnswer((_) async => Future.value(
+ (expectedExposureCompensationIndex *
+ exposureState.exposureCompensationStep)
+ .round()));
+
+ // Exposure index * exposure offset step size = exposure offset, i.e.
+ // 15 * 0.2 = 3.
+ expect(await camera.setExposureOffset(cameraId, offset), equals(3));
+ });
+
+ test('setFocusPoint clears current auto-exposure metering point as expected',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 93;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = mockCameraInfo;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ // Verify nothing happens if no current focus and metering action has been
+ // enabled.
+ await camera.setFocusPoint(cameraId, null);
+ verifyNever(mockCameraControl.startFocusAndMetering(any));
+ verifyNever(mockCameraControl.cancelFocusAndMetering());
+
+ // Verify current auto-exposure metering point is removed if previously set.
+ final (MeteringPoint, int?) autoexposureMeteringPointInfo = (
+ MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAe
+ );
+ List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAf
+ ),
+ autoexposureMeteringPointInfo
+ ];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setFocusPoint(cameraId, null);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first,
+ equals(autoexposureMeteringPointInfo));
+
+ // Verify current focus and metering action is cleared if only previously
+ // set metering point was for auto-exposure.
+ meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAf
+ )
+ ];
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setFocusPoint(cameraId, null);
+
+ verify(mockCameraControl.cancelFocusAndMetering());
+ });
+
+ test('setFocusPoint throws CameraException if invalid point specified',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 23;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ const Point invalidFocusPoint = Point(-3, 1);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ expect(() => camera.setFocusPoint(cameraId, invalidFocusPoint),
+ throwsA(isA()));
+ });
+
+ test(
+ 'setFocusPoint adds new exposure point to focus metering action to start as expected when previous metering points have been set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 9;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCameraInfo mockCameraInfo = MockCameraInfo();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = mockCameraInfo;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ // Verify current auto-exposure metering point is removed if previously set.
+ double focusPointX = 0.8;
+ double focusPointY = 0.1;
+ Point exposurePoint = Point(focusPointX, focusPointY);
+ final (MeteringPoint, int?) autoExposureMeteringPointInfo = (
+ MeteringPoint.detached(x: 0.3, y: 0.7, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAe
+ );
+ List<(MeteringPoint, int?)> meteringPointInfos = <(MeteringPoint, int?)>[
+ (
+ MeteringPoint.detached(x: 0.2, y: 0.5, cameraInfo: mockCameraInfo),
+ FocusMeteringAction.flagAf
+ ),
+ autoExposureMeteringPointInfo
+ ];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setFocusPoint(cameraId, exposurePoint);
+
+ VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+ expect(capturedMeteringPointInfos.first,
+ equals(autoExposureMeteringPointInfo));
+ expect(capturedMeteringPointInfos[1].$1.x, equals(focusPointX));
+ expect(capturedMeteringPointInfos[1].$1.y, equals(focusPointY));
+ expect(
+ capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAf));
+
+ // Verify exposure point is set when no auto-exposure metering point
+ // previously set, but an auto-focus point metering point has been.
+ focusPointX = 0.2;
+ focusPointY = 0.9;
+ exposurePoint = Point(focusPointX, focusPointY);
+ meteringPointInfos = <(MeteringPoint, int?)>[autoExposureMeteringPointInfo];
+
+ camera.currentFocusMeteringAction =
+ FocusMeteringAction.detached(meteringPointInfos: meteringPointInfos);
+
+ await camera.setFocusPoint(cameraId, exposurePoint);
+
+ verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ capturedAction = verificationResult.captured.single as FocusMeteringAction;
+ capturedMeteringPointInfos = capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+ expect(capturedMeteringPointInfos.first,
+ equals(autoExposureMeteringPointInfo));
+ expect(capturedMeteringPointInfos[1].$1.x, equals(focusPointX));
+ expect(capturedMeteringPointInfos[1].$1.y, equals(focusPointY));
+ expect(
+ capturedMeteringPointInfos[1].$2, equals(FocusMeteringAction.flagAf));
+ });
+
+ test(
+ 'setFocusPoint adds new exposure point to focus metering action to start as expected when no previous metering points have been set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 19;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ const double focusPointX = 0.8;
+ const double focusPointY = 0.1;
+ const Point exposurePoint = Point(focusPointX, focusPointY);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+ camera.currentFocusMeteringAction = null;
+
+ camera.proxy = getProxyForExposureAndFocus();
+
+ await camera.setFocusPoint(cameraId, exposurePoint);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(focusPointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(focusPointY));
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAf));
+ });
+
+ test('setFocusPoint disables auto-cancel for focus and metering as expected',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 2;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockFocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ const Point exposurePoint = Point(0.1, 0.2);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, MockCamera2CameraControl());
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Test not disabling auto cancel.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+ clearInteractions(mockCameraControl);
+
+ await camera.setFocusPoint(cameraId, exposurePoint);
+ VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isFalse);
+
+ clearInteractions(mockCameraControl);
+
+ // Test disabling auto cancel.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ clearInteractions(mockCameraControl);
+
+ await camera.setFocusPoint(cameraId, exposurePoint);
+ verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ capturedAction = verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+ });
+
+ test(
+ 'setFocusMode does nothing if setting auto-focus mode and is already using auto-focus mode',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 4;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockFocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, MockCamera2CameraControl());
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Set locked focus mode and then try to re-set it.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ clearInteractions(mockCameraControl);
+
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ verifyNoMoreInteractions(mockCameraControl);
+ });
+
+ test(
+ 'setFocusMode does nothing if setting locked focus mode and is already using locked focus mode',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 4;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+
+ // Camera uses auto-focus by default, so try setting auto mode again.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+
+ verifyNoMoreInteractions(mockCameraControl);
+ });
+
+ test(
+ 'setFocusMode removes default auto-focus point if previously set and setting auto-focus mode',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 5;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockFocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double exposurePointX = 0.2;
+ const double exposurePointY = 0.7;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Set exposure points.
+ await camera.setExposurePoint(
+ cameraId, const Point(exposurePointX, exposurePointY));
+
+ // Lock focus default focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ clearInteractions(mockCameraControl);
+
+ // Test removal of default focus point.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isFalse);
+
+ // We expect only the previously set exposure point to be re-set.
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(exposurePointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(exposurePointY));
+ expect(capturedMeteringPointInfos.first.$1.size, isNull);
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAe));
+ });
+
+ test(
+ 'setFocusMode cancels focus and metering if only focus point previously set is a focus point',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 5;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final FocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Lock focus default focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ // Test removal of default focus point.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+
+ verify(mockCameraControl.cancelFocusAndMetering());
+ });
+
+ test(
+ 'setFocusMode re-focuses on previously set auto-focus point with auto-canceled enabled if setting auto-focus mode',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 6;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final FocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double focusPointX = 0.1;
+ const double focusPointY = 0.2;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Lock a focus point.
+ await camera.setFocusPoint(
+ cameraId, const Point(focusPointX, focusPointY));
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ clearInteractions(mockCameraControl);
+
+ // Test re-focusing on previously set auto-focus point with auto-cancel enabled.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isFalse);
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(focusPointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(focusPointY));
+ expect(capturedMeteringPointInfos.first.$1.size, isNull);
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAf));
+ });
+
+ test(
+ 'setFocusMode starts expected focus and metering action with previously set auto-focus point if setting locked focus mode and current focus and metering action has auto-focus point',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 7;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double focusPointX = 0.88;
+ const double focusPointY = 0.33;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Set a focus point.
+ await camera.setFocusPoint(
+ cameraId, const Point(focusPointX, focusPointY));
+ clearInteractions(mockCameraControl);
+
+ // Lock focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+
+ // We expect the set focus point to be locked.
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(focusPointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(focusPointY));
+ expect(capturedMeteringPointInfos.first.$1.size, isNull);
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAf));
+ });
+
+ test(
+ 'setFocusMode starts expected focus and metering action with previously set auto-focus point if setting locked focus mode and current focus and metering action has auto-focus point amongst others',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 8;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double focusPointX = 0.38;
+ const double focusPointY = 0.38;
+ const double exposurePointX = 0.54;
+ const double exposurePointY = 0.45;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Set focus and exposure points.
+ await camera.setFocusPoint(
+ cameraId, const Point(focusPointX, focusPointY));
+ await camera.setExposurePoint(
+ cameraId, const Point(exposurePointX, exposurePointY));
+ clearInteractions(mockCameraControl);
+
+ // Lock focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+
+ // We expect two MeteringPoints, the set focus point and the set exposure
+ // point.
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+
+ final List<(MeteringPoint, int?)> focusPoints = capturedMeteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAf)
+ .toList();
+ expect(focusPoints.length, equals(1));
+ expect(focusPoints.first.$1.x, equals(focusPointX));
+ expect(focusPoints.first.$1.y, equals(focusPointY));
+ expect(focusPoints.first.$1.size, isNull);
+
+ final List<(MeteringPoint, int?)> exposurePoints =
+ capturedMeteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAe)
+ .toList();
+ expect(exposurePoints.length, equals(1));
+ expect(exposurePoints.first.$1.x, equals(exposurePointX));
+ expect(exposurePoints.first.$1.y, equals(exposurePointY));
+ expect(exposurePoints.first.$1.size, isNull);
+ });
+
+ test(
+ 'setFocusMode starts expected focus and metering action if setting locked focus mode and current focus and metering action does not contain an auto-focus point',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 9;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double exposurePointX = 0.8;
+ const double exposurePointY = 0.3;
+ const double defaultFocusPointX = 0.5;
+ const double defaultFocusPointY = 0.5;
+ const double defaultFocusPointSize = 1;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Set an exposure point (creates a current focus and metering action
+ // without a focus point).
+ await camera.setExposurePoint(
+ cameraId, const Point(exposurePointX, exposurePointY));
+ clearInteractions(mockCameraControl);
+
+ // Lock focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+
+ // We expect two MeteringPoints, the default focus point and the set
+ //exposure point.
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(2));
+
+ final List<(MeteringPoint, int?)> focusPoints = capturedMeteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAf)
+ .toList();
+ expect(focusPoints.length, equals(1));
+ expect(focusPoints.first.$1.x, equals(defaultFocusPointX));
+ expect(focusPoints.first.$1.y, equals(defaultFocusPointY));
+ expect(focusPoints.first.$1.size, equals(defaultFocusPointSize));
+
+ final List<(MeteringPoint, int?)> exposurePoints =
+ capturedMeteringPointInfos
+ .where(((MeteringPoint, int?) meteringPointInfo) =>
+ meteringPointInfo.$2 == FocusMeteringAction.flagAe)
+ .toList();
+ expect(exposurePoints.length, equals(1));
+ expect(exposurePoints.first.$1.x, equals(exposurePointX));
+ expect(exposurePoints.first.$1.y, equals(exposurePointY));
+ expect(exposurePoints.first.$1.size, isNull);
+ });
+
+ test(
+ 'setFocusMode starts expected focus and metering action if there is no current focus and metering action',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 10;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+ const double defaultFocusPointX = 0.5;
+ const double defaultFocusPointY = 0.5;
+ const double defaultFocusPointSize = 1;
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Lock focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+
+ // We expect only the default focus point to be set.
+ final List<(MeteringPoint, int?)> capturedMeteringPointInfos =
+ capturedAction.meteringPointInfos;
+ expect(capturedMeteringPointInfos.length, equals(1));
+ expect(capturedMeteringPointInfos.first.$1.x, equals(defaultFocusPointX));
+ expect(capturedMeteringPointInfos.first.$1.y, equals(defaultFocusPointY));
+ expect(capturedMeteringPointInfos.first.$1.size,
+ equals(defaultFocusPointSize));
+ expect(capturedMeteringPointInfos.first.$2,
+ equals(FocusMeteringAction.flagAf));
+ });
+
+ test(
+ 'setFocusMode re-sets exposure mode if setting locked focus mode while using auto exposure mode',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 11;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final FocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ final MockCamera2CameraControl mockCamera2CameraControl =
+ MockCamera2CameraControl();
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraInfo = MockCameraInfo();
+ camera.cameraControl = mockCameraControl;
+
+ when(mockCamera2CameraControl.addCaptureRequestOptions(any))
+ .thenAnswer((_) async => Future.value());
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, mockCamera2CameraControl);
+
+ // Make setting focus and metering action successful for test.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Set auto exposure mode.
+ await camera.setExposureMode(cameraId, ExposureMode.auto);
+ clearInteractions(mockCamera2CameraControl);
+
+ // Lock focus point.
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+
+ final VerificationResult verificationResult =
+ verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny));
+ final CaptureRequestOptions capturedCaptureRequestOptions =
+ verificationResult.captured.single as CaptureRequestOptions;
+ final List<(CaptureRequestKeySupportedType, Object?)> requestedOptions =
+ capturedCaptureRequestOptions.requestedOptions;
+ expect(requestedOptions.length, equals(1));
+ expect(requestedOptions.first.$1,
+ equals(CaptureRequestKeySupportedType.controlAeLock));
+ expect(requestedOptions.first.$2, equals(false));
+ });
+
+ test(
+ 'setFocusPoint disables auto-cancel if auto focus mode fails to be set after locked focus mode is set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 22;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockFocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ const Point focusPoint = Point(0.21, 0.21);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, MockCamera2CameraControl());
+
+ // Make setting focus and metering action successful to set locked focus
+ // mode.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Set exposure point to later mock failed call to set an exposure point (
+ // otherwise, focus and metering will be canceled altogether, which is
+ //considered a successful call).
+ await camera.setExposurePoint(cameraId, const Point(0.3, 0.4));
+
+ // Set locked focus mode so we can set auto mode (cannot set auto mode
+ // directly since it is the default).
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ clearInteractions(mockCameraControl);
+
+ // Make setting focus and metering action fail to test that auto-cancel is
+ // still disabled.
+ reset(mockFocusMeteringResult);
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(false));
+
+ // Test disabling auto cancel.
+ await camera.setFocusMode(cameraId, FocusMode.auto);
+ clearInteractions(mockCameraControl);
+
+ await camera.setFocusPoint(cameraId, focusPoint);
+ final VerificationResult verificationResult =
+ verify(mockCameraControl.startFocusAndMetering(captureAny));
+ final FocusMeteringAction capturedAction =
+ verificationResult.captured.single as FocusMeteringAction;
+ expect(capturedAction.disableAutoCancel, isTrue);
+ });
+
+ test(
+ 'setExposurePoint disables auto-cancel if auto focus mode fails to be set after locked focus mode is set',
+ () async {
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+ const int cameraId = 342;
+ final MockCameraControl mockCameraControl = MockCameraControl();
+ final MockFocusMeteringResult mockFocusMeteringResult =
+ MockFocusMeteringResult();
+ const Point exposurePoint = Point(0.23, 0.32);
+
+ // Set directly for test versus calling createCamera.
+ camera.cameraControl = mockCameraControl;
+ camera.cameraInfo = MockCameraInfo();
+
+ camera.proxy = getProxyForSettingFocusandExposurePoints(
+ mockCameraControl, MockCamera2CameraControl());
+
+ // Make setting focus and metering action successful to set locked focus
+ // mode.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future.value(true));
+ when(mockCameraControl.startFocusAndMetering(any)).thenAnswer((_) async =>
+ Future.value(mockFocusMeteringResult));
+
+ // Set exposure point to later mock failed call to set an exposure point (
+ // otherwise, focus and metering will be canceled altogether, which is
+ //considered a successful call).
+ await camera.setExposurePoint(cameraId, const Point(0.4, 0.3));
+
+ // Set locked focus mode so we can set auto mode (cannot set auto mode
+ // directly since it is the default).
+ await camera.setFocusMode(cameraId, FocusMode.locked);
+ clearInteractions(mockCameraControl);
+
+ // Make setting focus and metering action fail to test that auto-cancel is
+ // still disabled.
+ when(mockFocusMeteringResult.isFocusSuccessful())
+ .thenAnswer((_) async => Future