Merge flutter/plugins (#3233)
Merges everything from the packages/ directory of flutter/plugins (which
is essentially the entire remaining repository) into this repository,
completing the core of the repository merge.
Part of https://github.com/flutter/flutter/issues/113764
diff --git a/.cirrus.yml b/.cirrus.yml
index 4f6cf53..ad467b7 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -236,6 +236,9 @@
- name: dart_unit_tests
env:
matrix:
+ PACKAGE_SHARDING: "--shardIndex 0 --shardCount 2"
+ PACKAGE_SHARDING: "--shardIndex 1 --shardCount 2"
+ matrix:
CHANNEL: "master"
CHANNEL: "stable"
unit_test_script:
@@ -315,7 +318,13 @@
build_script:
- ./script/tool_runner.sh build-examples --web
drive_script:
- - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml
+ # TODO(stuartmorgan): Figure out why url_launcher_web is failing on stable and re-enable it:
+ # https://github.com/flutter/flutter/issues/121161
+ - if [[ "$CHANNEL" == "master" ]]; then
+ - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml
+ - else
+ - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml,url_launcher_web
+ - fi
- name: web_benchmarks_test
env:
matrix:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 396a3c0..a365609 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -33,6 +33,78 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/camera/camera_android/android"
+ commit-message:
+ prefix: "[camera]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/camera/camera_android/example/android/app"
+ commit-message:
+ prefix: "[camera]"
+ 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/camera/camera_android_camerax/android"
+ commit-message:
+ prefix: "[camera]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/camera/camera_android_camerax/example/android/app"
+ commit-message:
+ prefix: "[camera]"
+ 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/camera/camera/example/android/app"
+ commit-message:
+ prefix: "[camera]"
+ 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/dynamic_layouts/example/android/app"
commit-message:
@@ -43,6 +115,37 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/espresso/android"
+ commit-message:
+ prefix: "[espresso]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/espresso/example/android/app"
+ commit-message:
+ prefix: "[espresso]"
+ 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/extension_google_sign_in_as_googleapis_auth/example/android/app"
commit-message:
@@ -53,6 +156,7 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/flutter_adaptive_scaffold/example/android/app"
commit-message:
@@ -63,6 +167,7 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/flutter_markdown/example/android/app"
commit-message:
@@ -73,6 +178,37 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/flutter_plugin_android_lifecycle/android"
+ commit-message:
+ prefix: "[lifecycle]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/flutter_plugin_android_lifecycle/example/android/app"
+ commit-message:
+ prefix: "[lifecycle]"
+ 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/go_router/example/android/app"
commit-message:
@@ -83,6 +219,212 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/google_maps_flutter/google_maps_flutter/example/android/app"
+ commit-message:
+ prefix: "[google_maps]"
+ 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/google_maps_flutter/google_maps_flutter_android/android"
+ commit-message:
+ prefix: "[google_maps]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/google_maps_flutter/google_maps_flutter_android/example/android/app"
+ commit-message:
+ prefix: "[google_maps]"
+ 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/google_sign_in/google_sign_in/example/android/app"
+ commit-message:
+ prefix: "[sign_in]"
+ 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/google_sign_in/google_sign_in_android/android"
+ commit-message:
+ prefix: "[sign_in]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/google_sign_in/google_sign_in_android/example/android/app"
+ commit-message:
+ prefix: "[sign_in]"
+ 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/in_app_purchase/in_app_purchase_android/android"
+ commit-message:
+ prefix: "[in_app_pur]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/in_app_purchase/in_app_purchase_android/example/android/app"
+ commit-message:
+ prefix: "[in_app_pur]"
+ 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/in_app_purchase/in_app_purchase/example/android/app"
+ commit-message:
+ prefix: "[in_app_pur]"
+ 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:
+ prefix: "[image_picker]"
+ 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_android/android"
+ commit-message:
+ prefix: "[image_picker]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/image_picker/image_picker_android/example/android/app"
+ commit-message:
+ prefix: "[image_picker]"
+ 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/local_auth/local_auth_android/android"
+ commit-message:
+ prefix: "[local_auth]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/local_auth/local_auth_android/example/android/app"
+ commit-message:
+ prefix: "[local_auth]"
+ 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/local_auth/local_auth/example/android/app"
+ commit-message:
+ prefix: "[local_auth]"
+ 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/palette_generator/example/android/app"
commit-message:
@@ -93,6 +435,48 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/path_provider/path_provider/example/android/app"
+ commit-message:
+ prefix: "[path_provider]"
+ 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/path_provider/path_provider_android/android"
+ commit-message:
+ prefix: "[path_provider]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/path_provider/path_provider_android/example/android/app"
+ commit-message:
+ prefix: "[path_provider]"
+ 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/pigeon/platform_tests/test_plugin/android"
commit-message:
@@ -111,6 +495,7 @@
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
- dependency-name: "org.robolectric:*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/pigeon/platform_tests/test_plugin/example/android/app"
commit-message:
@@ -121,6 +506,7 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/pigeon/platform_tests/alternate_language_test_plugin/android"
commit-message:
@@ -139,6 +525,7 @@
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
- dependency-name: "org.robolectric:*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/app"
commit-message:
@@ -149,6 +536,48 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/quick_actions/quick_actions_android/android"
+ commit-message:
+ prefix: "[quick_actions]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/quick_actions/quick_actions_android/example/android/app"
+ commit-message:
+ prefix: "[quick_actions]"
+ 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/quick_actions/quick_actions/example/android/app"
+ commit-message:
+ prefix: "[quick_actions]"
+ 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/rfw/example/hello/android/app"
commit-message:
@@ -159,6 +588,7 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/rfw/example/local/android/app"
commit-message:
@@ -169,6 +599,7 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
- package-ecosystem: "gradle"
directory: "/packages/rfw/example/remote/android/app"
commit-message:
@@ -179,3 +610,167 @@
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/shared_preferences/shared_preferences/example/android/app"
+ commit-message:
+ prefix: "[shared_pref]"
+ 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/shared_preferences/shared_preferences_android/android"
+ commit-message:
+ prefix: "[shared_pref]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/shared_preferences/shared_preferences_android/example/android/app"
+ commit-message:
+ prefix: "[shared_pref]"
+ 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/url_launcher/url_launcher_android/android"
+ commit-message:
+ prefix: "[url_launcher]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/url_launcher/url_launcher_android/example/android/app"
+ commit-message:
+ prefix: "[url_launcher]"
+ 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/url_launcher/url_launcher/example/android/app"
+ commit-message:
+ prefix: "[url_launcher]"
+ 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/video_player/video_player/example/android/app"
+ commit-message:
+ prefix: "[video_player]"
+ 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/video_player/video_player_android/android"
+ commit-message:
+ prefix: "[video_player]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/video_player/video_player_android/example/android/app"
+ commit-message:
+ prefix: "[video_player]"
+ 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/webview_flutter/webview_flutter/example/android/app"
+ commit-message:
+ prefix: "[webview]"
+ 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/webview_flutter/webview_flutter_android/android"
+ commit-message:
+ prefix: "[webview]"
+ 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"]
+ - dependency-name: "org.robolectric:*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
+
+ - package-ecosystem: "gradle"
+ directory: "/packages/webview_flutter/webview_flutter_android/example/android/app"
+ commit-message:
+ prefix: "[webview]"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "*"
+ update-types: ["version-update:semver-minor", "version-update:semver-patch"]
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 2104027..7bbdbd1 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,18 +1,30 @@
'p: animations':
- packages/animations/**/*
+'p: camera':
+ - packages/camera/**/*
+
'p: cross_file':
- packages/cross_file/**/*
'p: css_colors':
- packages/css_colors/**/*
+'p: cupertino_icons':
+ - third_party/packages/cupertino_icons/**/*
+
'p: dynamic_layouts':
- packages/dynamic_layouts/**/*
+'p: espresso':
+ - packages/espresso/**/*
+
'p: extension_google_sign_in_as_googleapis_auth':
- packages/extension_google_sign_in_as_googleapis_auth/**/*
+'p: file_selector':
+ - packages/file_selector/**/*
+
'p: flutter_adaptive_scaffold':
- packages/flutter_adaptive_scaffold/**/*
@@ -28,6 +40,9 @@
'p: flutter_migrate':
- packages/flutter_migrate/**/*
+'p: flutter_plugin_android_lifecycle':
+ - packages/flutter_plugin_android_lifecycle/**/*
+
'p: flutter_template_images':
- packages/flutter_template_images/**/*
@@ -40,6 +55,24 @@
'p: google_identity_services':
- packages/google_indentity_services_web/**
+'p: google_maps_flutter':
+ - packages/google_maps_flutter/**/*
+
+'p: google_sign_in':
+ - packages/google_sign_in/**/*
+
+'p: image_picker':
+ - packages/image_picker/**/*
+
+'p: in_app_purchase':
+ - packages/in_app_purchase/**/*
+
+'p: ios_platform_images':
+ - packages/ios_platform_images/**/*
+
+'p: local_auth':
+ - packages/local_auth/**/*
+
'p: metrics_center':
- packages/metrics_center/**/*
@@ -49,23 +82,68 @@
'p: palette_generator':
- packages/palette_generator/**/*
+'p: path_provider':
+ - packages/path_provider/**/*
+
'p: pigeon':
- packages/pigeon/**/*
+'p: plugin_platform_interface':
+ - packages/plugin_platform_interface/**/*
+
'p: pointer_interceptor':
- packages/pointer_interceptor/**/*
+'p: quick_actions':
+ - packages/quick_actions/**/*
+
'p: rfw':
- packages/rfw/**/*
+'p: shared_preferences':
+ - packages/shared_preferences/**/*
+
'p: standard_message_codec':
- packages/standard_message_codec/**/*
+'p: url_launcher':
+ - packages/url_launcher/**/*
+
+'p: video_player':
+ - packages/video_player/**/*
+
'p: web_benchmarks':
- packages/web_benchmarks/**/*
+'p: webview_flutter':
+ - packages/webview_flutter/**/*
+
'p: xdg_directories':
- packages/xdg_directories/**/*
-'p: cupertino_icons':
- - third_party/packages/cupertino_icons/**/*
+'platform-android':
+ - packages/*/*_android/**/*
+ - packages/**/android/**/*
+
+'platform-ios':
+ - packages/*/*_ios/**/*
+ - packages/*/*_storekit/**/*
+ - packages/*/*_wkwebview/**/*
+ - packages/**/ios/**/*
+
+'platform-linux':
+ - packages/*/*_linux/**/*
+ - packages/**/linux/**/*
+
+'platform-macos':
+ - packages/*/*_macos/**/*
+ - packages/**/macos/**/*
+
+'platform-web':
+ - packages/*/*_web/**/*
+ - packages/**/web/**/*
+
+'platform-windows':
+ - packages/*/*_windows/**/*
+ - packages/**/windows/**/*
+
diff --git a/AUTHORS b/AUTHORS
index 7b9785e..23670fb 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -7,3 +7,67 @@
The Chromium Authors
Britannio Jarrett <britanniojarrett@gmail.com>
Sarbagya Dhaubanjar <mail@sarbagyastha.com.np>
+German Saprykin <saprykin.h@gmail.com>
+Benjamin Sauer <sauer.benjamin@gmail.com>
+larsenthomasj@gmail.com
+Ali Bitek <alibitek@protonmail.ch>
+Pol Batlló <pol.batllo@gmail.com>
+Anatoly Pulyaevskiy
+Hayden Flinner <haydenflinner@gmail.com>
+Stefano Rodriguez <hlsroddy@gmail.com>
+Salvatore Giordano <salvatoregiordanoo@gmail.com>
+Brian Armstrong <brian@flutter.institute>
+Paul DeMarco <paulmdemarco@gmail.com>
+Fabricio Nogueira <feufeu@gmail.com>
+Simon Lightfoot <simon@devangels.london>
+Ashton Thomas <ashton@acrinta.com>
+Thomas Danner <thmsdnnr@gmail.com>
+Diego Velásquez <diego.velasquez.lopez@gmail.com>
+Hajime Nakamura <nkmrhj@gmail.com>
+Tuyển Vũ Xuân <netsoft1985@gmail.com>
+Miguel Ruivo <miguel@miguelruivo.com>
+Sarthak Verma <sarthak@artiosys.com>
+Mike Diarmid <mike@invertase.io>
+Invertase <oss@invertase.io>
+Elliot Hesp <elliot@invertase.io>
+Vince Varga <vince.varga@smaho.com>
+Aawaz Gyawali <awazgyawali@gmail.com>
+EUI Limited <ian.evans3@admiralgroup.co.uk>
+Katarina Sheremet <katarina@sheremet.ch>
+Thomas Stockx <thomas@stockxit.com>
+Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
+Ozkan Eksi <ozeksi@gmail.com>
+Rishab Nayak <rishab@bu.edu>
+ko2ic <ko2ic.dev@gmail.com>
+Jonathan Younger <jonathan@daikini.com>
+Jose Sanchez <josesm82@gmail.com>
+Debkanchan Samadder <debu.samadder@gmail.com>
+Audrius Karosevicius <audrius.karosevicius@gmail.com>
+Lukasz Piliszczuk <lukasz@intheloup.io>
+SoundReply Solutions GmbH <ch@soundreply.com>
+Rafal Wachol <rwachol@gmail.com>
+Pau Picas <pau.picas@gmail.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Alexandru Tuca <salexandru.tuca@outlook.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Rhodes Davis Jr. <rody.davis.jr@gmail.com>
+Luigi Agosti <luigi@tengio.com>
+Quentin Le Guennec <quentin@tengio.com>
+Koushik Ravikumar <koushik@tengio.com>
+Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
+Ryo Miyake <ryo@miyake.id>
+Théo Champion <contact.theochampion@gmail.com>
+Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
+Eitan Schwartz <eshvartz@gmail.com>
+Chris Rutkowski <chrisrutkowski89@gmail.com>
+Juan Alvarez <juan.alvarez@resideo.com>
+Aleksandr Yurkovskiy <sanekyy@gmail.com>
+Anton Borries <mail@antonborri.es>
+Alex Li <google@alexv525.com>
+Rahul Raj <64.rahulraj@gmail.com>
+Daniel Roek <daniel.roek@gmail.com>
+TheOneWithTheBraid <the-one@with-the-braid.cf>
+Rulong Chen(陈汝龙) <rulong.crl@alibaba-inc.com>
+Hwanseok Kang <tttkhs96@gmail.com>
+Twin Sun, LLC <google-contrib@twinsunsolutions.com>
diff --git a/CODEOWNERS b/CODEOWNERS
index d160af2..538c826 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -4,27 +4,98 @@
# These names are just suggestions. It is fine to have your changes
# reviewed by someone else.
-packages/animations/** @goderbauer
-packages/cross_file/** @ditman
-packages/css_colors/** @stuartmorgan
-packages/dynamic_layouts/** @Piinks
+packages/animations/** @goderbauer
+packages/camera/** @bparrishMines
+packages/cross_file/** @ditman
+packages/css_colors/** @stuartmorgan
+packages/dynamic_layouts/** @Piinks
packages/extension_google_sign_in_as_googleapis_auth/** @ditman
-packages/flutter_adaptive_scaffold/** @gspencergoog
-packages/flutter_image/** @stuartmorgan
-packages/flutter_lints/** @goderbauer
-packages/flutter_markdown/** @domesticmouse
-packages/flutter_migrate/** @GaryQian
-packages/flutter_template_images/** @stuartmorgan
-packages/go_router/** @chunhtai
-packages/go_router_builder/** @chunhtai
-packages/google_identity_services_web/** @ditman
-packages/metrics_center/** @keyonghan
-packages/multicast_dns/** @dnfield
-packages/palette_generator/** @gspencergoog
-packages/pigeon/** @tarrinneal
-packages/pointer_interceptor/** @ditman
-packages/rfw/** @Hixie
-packages/standard_message_codec/** @jonahwilliams
-packages/web_benchmarks/** @yjbanov
-packages/xdg_directories/** @stuartmorgan
-third_party/packages/cupertino_icons/** @jmagman
+packages/file_selector/** @stuartmorgan
+packages/flutter_adaptive_scaffold/** @gspencergoog
+packages/flutter_image/** @stuartmorgan
+packages/flutter_lints/** @goderbauer
+packages/flutter_markdown/** @domesticmouse
+packages/flutter_migrate/** @GaryQian
+packages/flutter_template_images/** @stuartmorgan
+packages/go_router/** @chunhtai
+packages/go_router_builder/** @chunhtai
+packages/google_identity_services_web/** @ditman
+packages/google_maps_flutter/** @stuartmorgan
+packages/google_sign_in/** @stuartmorgan
+packages/image_picker/** @tarrinneal
+packages/in_app_purchase/** @bparrishMines
+packages/local_auth/** @stuartmorgan
+packages/metrics_center/** @keyonghan
+packages/multicast_dns/** @dnfield
+packages/palette_generator/** @gspencergoog
+packages/path_provider/** @stuartmorgan
+packages/pigeon/** @tarrinneal
+packages/plugin_platform_interface/** @stuartmorgan
+packages/pointer_interceptor/** @ditman
+packages/quick_actions/** @bparrishMines
+packages/rfw/** @Hixie
+packages/shared_preferences/** @tarrinneal
+packages/standard_message_codec/** @jonahwilliams
+packages/url_launcher/** @stuartmorgan
+packages/video_player/** @tarrinneal
+packages/web_benchmarks/** @yjbanov
+packages/webview_flutter/** @bparrishMines
+packages/xdg_directories/** @stuartmorgan
+third_party/packages/cupertino_icons/** @jmagman
+
+# Plugin platform implementation rules. These should stay last, since the last
+# matching entry takes precedence.
+
+# - Web
+packages/**/*_web/** @ditman
+
+# - Android
+packages/camera/camera_android/** @camsim99
+packages/camera/camera_android_camerax/** @camsim99
+packages/espresso/** @reidbaker
+packages/flutter_plugin_android_lifecycle/** @reidbaker
+packages/google_maps_flutter/google_maps_flutter_android/** @reidbaker
+packages/google_sign_in/google_sign_in_android/** @camsim99
+packages/image_picker/image_picker_android/** @gmackall
+packages/in_app_purchase/in_app_purchase_android/** @gmackall
+packages/local_auth/local_auth_android/** @camsim99
+packages/path_provider/path_provider_android/** @camsim99
+packages/quick_actions/quick_actions_android/** @camsim99
+packages/shared_preferences/shared_preferences_android/** @reidbaker
+packages/url_launcher/url_launcher_android/** @gmackall
+packages/video_player/video_player_android/** @camsim99
+
+# - iOS
+packages/camera/camera_avfoundation/** @hellohuanlin
+packages/file_selector/file_selector_ios/** @jmagman
+packages/google_maps_flutter/google_maps_flutter_ios/** @cyanglaz
+packages/google_sign_in/google_sign_in_ios/** @vashworth
+packages/image_picker/image_picker_ios/** @vashworth
+packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz
+packages/ios_platform_images/ios/** @jmagman
+packages/local_auth/local_auth_ios/** @louisehsu
+packages/path_provider/path_provider_foundation/** @jmagman
+packages/quick_actions/quick_actions_ios/** @hellohuanlin
+packages/shared_preferences/shared_preferences_foundation/** @cyanglaz
+packages/url_launcher/url_launcher_ios/** @jmagman
+packages/video_player/video_player_avfoundation/** @hellohuanlin
+packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz
+
+# - Linux
+packages/file_selector/file_selector_linux/** @cbracken
+packages/path_provider/path_provider_linux/** @cbracken
+packages/shared_preferences/shared_preferences_linux/** @cbracken
+packages/url_launcher/url_launcher_linux/** @cbracken
+
+# - macOS
+packages/file_selector/file_selector_macos/** @cbracken
+packages/url_launcher/url_launcher_macos/** @cbracken
+
+# - Windows
+packages/camera/camera_windows/** @cbracken
+packages/file_selector/file_selector_windows/** @cbracken
+packages/image_picker/image_picker_windows/** @cbracken
+packages/local_auth/local_auth_windows/** @cbracken
+packages/path_provider/path_provider_windows/** @cbracken
+packages/shared_preferences/shared_preferences_windows/** @cbracken
+packages/url_launcher/url_launcher_windows/** @cbracken
diff --git a/packages/camera/camera/AUTHORS b/packages/camera/camera/AUTHORS
new file mode 100644
index 0000000..493a0b4
--- /dev/null
+++ b/packages/camera/camera/AUTHORS
@@ -0,0 +1,66 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
+The Chromium Authors
+German Saprykin <saprykin.h@gmail.com>
+Benjamin Sauer <sauer.benjamin@gmail.com>
+larsenthomasj@gmail.com
+Ali Bitek <alibitek@protonmail.ch>
+Pol Batlló <pol.batllo@gmail.com>
+Anatoly Pulyaevskiy
+Hayden Flinner <haydenflinner@gmail.com>
+Stefano Rodriguez <hlsroddy@gmail.com>
+Salvatore Giordano <salvatoregiordanoo@gmail.com>
+Brian Armstrong <brian@flutter.institute>
+Paul DeMarco <paulmdemarco@gmail.com>
+Fabricio Nogueira <feufeu@gmail.com>
+Simon Lightfoot <simon@devangels.london>
+Ashton Thomas <ashton@acrinta.com>
+Thomas Danner <thmsdnnr@gmail.com>
+Diego Velásquez <diego.velasquez.lopez@gmail.com>
+Hajime Nakamura <nkmrhj@gmail.com>
+Tuyển Vũ Xuân <netsoft1985@gmail.com>
+Miguel Ruivo <miguel@miguelruivo.com>
+Sarthak Verma <sarthak@artiosys.com>
+Mike Diarmid <mike@invertase.io>
+Invertase <oss@invertase.io>
+Elliot Hesp <elliot@invertase.io>
+Vince Varga <vince.varga@smaho.com>
+Aawaz Gyawali <awazgyawali@gmail.com>
+EUI Limited <ian.evans3@admiralgroup.co.uk>
+Katarina Sheremet <katarina@sheremet.ch>
+Thomas Stockx <thomas@stockxit.com>
+Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
+Ozkan Eksi <ozeksi@gmail.com>
+Rishab Nayak <rishab@bu.edu>
+ko2ic <ko2ic.dev@gmail.com>
+Jonathan Younger <jonathan@daikini.com>
+Jose Sanchez <josesm82@gmail.com>
+Debkanchan Samadder <debu.samadder@gmail.com>
+Audrius Karosevicius <audrius.karosevicius@gmail.com>
+Lukasz Piliszczuk <lukasz@intheloup.io>
+SoundReply Solutions GmbH <ch@soundreply.com>
+Rafal Wachol <rwachol@gmail.com>
+Pau Picas <pau.picas@gmail.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Alexandru Tuca <salexandru.tuca@outlook.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Rhodes Davis Jr. <rody.davis.jr@gmail.com>
+Luigi Agosti <luigi@tengio.com>
+Quentin Le Guennec <quentin@tengio.com>
+Koushik Ravikumar <koushik@tengio.com>
+Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
+Ryo Miyake <ryo@miyake.id>
+Théo Champion <contact.theochampion@gmail.com>
+Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
+Eitan Schwartz <eshvartz@gmail.com>
+Chris Rutkowski <chrisrutkowski89@gmail.com>
+Juan Alvarez <juan.alvarez@resideo.com>
+Aleksandr Yurkovskiy <sanekyy@gmail.com>
+Anton Borries <mail@antonborri.es>
+Alex Li <google@alexv525.com>
+Rahul Raj <64.rahulraj@gmail.com>
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
new file mode 100644
index 0000000..13c0040
--- /dev/null
+++ b/packages/camera/camera/CHANGELOG.md
@@ -0,0 +1,719 @@
+## 0.10.3
+
+* Adds back use of Optional type.
+
+## 0.10.2+1
+
+* Updates code for stricter lint checks.
+
+## 0.10.2
+
+* Implements option to also stream when recording a video.
+
+## 0.10.1
+
+* Remove usage of deprecated quiver Optional type.
+
+## 0.10.0+5
+
+* Updates code for stricter lint checks.
+
+## 0.10.0+4
+
+* Removes usage of `_ambiguate` method in example.
+* Updates minimum Flutter version to 3.0.
+
+## 0.10.0+3
+
+* Updates code for `no_leading_underscores_for_local_identifiers` lint.
+
+## 0.10.0+2
+
+* Updates imports for `prefer_relative_imports`.
+* Updates minimum Flutter version to 2.10.
+
+## 0.10.0+1
+
+* Fixes avoid_redundant_argument_values lint warnings and minor typos.
+
+## 0.10.0
+
+* **Breaking Change** Bumps default camera_web package version, which updates permission exception code from `cameraPermission` to `CameraAccessDenied`.
+* **Breaking Change** Bumps default camera_android package version, which updates permission exception code from `cameraPermission` to
+ `CameraAccessDenied` and `AudioAccessDenied`.
+* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316).
+
+## 0.9.8+1
+
+* Ignores deprecation warnings for upcoming styleFrom button API changes.
+
+## 0.9.8
+
+* Moves Android and iOS implementations to federated packages.
+* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231).
+
+## 0.9.7+1
+
+* Moves streaming implementation to the platform interface package.
+
+## 0.9.7
+
+* Returns all the available cameras on iOS.
+
+## 0.9.6
+
+* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result.
+
+## 0.9.5+1
+
+* Suppresses warnings for pre-iOS-11 codepaths.
+
+## 0.9.5
+
+* Adds camera access permission handling logic on iOS to fix a related crash when using the camera for the first time.
+
+## 0.9.4+24
+
+* Fixes preview orientation when pausing preview with locked orientation.
+
+## 0.9.4+23
+
+* Minor fixes for new analysis options.
+
+## 0.9.4+22
+
+* Removes unnecessary imports.
+* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors
+ lint warnings.
+
+## 0.9.4+21
+
+* Fixes README code samples.
+
+## 0.9.4+20
+
+* Fixes an issue with the orientation of videos recorded in landscape on Android.
+
+## 0.9.4+19
+
+* Migrate deprecated Scaffold SnackBar methods to ScaffoldMessenger.
+
+## 0.9.4+18
+
+* Fixes a crash in iOS when streaming on low-performance devices.
+
+## 0.9.4+17
+
+* Removes obsolete information from README, and adds OS support table.
+
+## 0.9.4+16
+
+* Fixes a bug resulting in a `CameraAccessException` that prevents image
+ capture on some Android devices.
+
+## 0.9.4+15
+
+* Uses dispatch queue for pixel buffer synchronization on iOS.
+* Minor iOS internal code cleanup related to queue helper functions.
+
+## 0.9.4+14
+
+* Restores compatibility with Flutter 2.5 and 2.8.
+
+## 0.9.4+13
+
+* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup.
+
+## 0.9.4+12
+
+* Skips unnecessary AppDelegate setup for unit tests on iOS.
+* Internal code cleanup for stricter analysis options.
+
+## 0.9.4+11
+
+* Manages iOS camera's orientation-related states on a background queue to prevent potential race conditions.
+
+## 0.9.4+10
+
+* iOS performance improvement by moving file writing from the main queue to a background IO queue.
+
+## 0.9.4+9
+
+* iOS performance improvement by moving sample buffer handling from the main queue to a background session queue.
+* Minor iOS internal code cleanup related to camera class and its delegate.
+* Minor iOS internal code cleanup related to resolution preset, video format, focus mode, exposure mode and device orientation.
+* Minor iOS internal code cleanup related to flash mode.
+
+## 0.9.4+8
+
+* Fixes a bug where ImageFormatGroup was ignored in `startImageStream` on iOS.
+
+## 0.9.4+7
+
+* Fixes a crash in iOS when passing null queue pointer into AVFoundation API due to race condition.
+* Minor iOS internal code cleanup related to dispatch queue.
+
+## 0.9.4+6
+
+* Fixes a crash in iOS when using image stream due to calling Flutter engine API on non-main thread.
+
+## 0.9.4+5
+
+* Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception.
+* Fixes integration tests.
+
+## 0.9.4+4
+
+* Change Android compileSdkVersion to 31.
+* Remove usages of deprecated Android API `CamcorderProfile`.
+* Update gradle version to 7.0.2 on Android.
+
+## 0.9.4+3
+
+* Fix registerTexture and result being called on background thread on iOS.
+
+## 0.9.4+2
+
+* Updated package description;
+* Refactor unit test on iOS to make it compatible with new restrictions in Xcode 13 which only supports the use of the `XCUIDevice` in Xcode UI tests.
+
+## 0.9.4+1
+
+* Fixed Android implementation throwing IllegalStateException when switching to a different activity.
+
+## 0.9.4
+
+* Add web support by endorsing `package:camera_web`.
+
+## 0.9.3+1
+
+* Remove iOS 9 availability check around ultra high capture sessions.
+
+## 0.9.3
+
+* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0.
+
+## 0.9.2+2
+
+* Ensure that setting the exposure offset returns the new offset value on Android.
+
+## 0.9.2+1
+
+* Fixed camera controller throwing an exception when being replaced in the preview widget.
+
+## 0.9.2
+
+* Added functions to pause and resume the camera preview.
+
+## 0.9.1+1
+
+* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md)
+
+## 0.9.1
+
+* Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto.
+
+## 0.9.0
+
+* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues.
+* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ.
+* Android Flash mode works with full precapture sequence.
+* Updated Android lint settings.
+
+## 0.8.1+7
+
+* Fix device orientation sometimes not affecting the camera preview orientation.
+
+## 0.8.1+6
+
+* Remove references to the Android V1 embedding.
+
+## 0.8.1+5
+
+* Make sure the `setFocusPoint` and `setExposurePoint` coordinates work correctly in all orientations on iOS (instead of only in portrait mode).
+
+## 0.8.1+4
+
+* Silenced warnings that may occur during build when using a very
+ recent version of Flutter relating to null safety.
+
+## 0.8.1+3
+
+* Do not change camera orientation when iOS device is flat.
+
+## 0.8.1+2
+
+* Fix iOS crash when selecting an unsupported FocusMode.
+
+## 0.8.1+1
+
+* Migrate maven repository from jcenter to mavenCentral.
+
+## 0.8.1
+
+* Solved a rotation issue on iOS which caused the default preview to be displayed as landscape right instead of portrait.
+
+## 0.8.0
+
+* Stable null safety release.
+* Solved delay when using the zoom feature on iOS.
+* Added a timeout to the pre-capture sequence on Android to prevent crashes when the camera cannot get a focus.
+* Updates the example code listed in the [README.md](README.md), so it runs without errors when you simply copy/ paste it into a Flutter App.
+
+## 0.7.0+4
+
+* Fix crash when taking picture with orientation lock
+
+## 0.7.0+3
+
+* Clockwise rotation of focus point in android
+
+## 0.7.0+2
+
+* Fix example reference in README.
+* Revert compileSdkVersion back to 29 (from 30) as this is causing problems with add-to-app configurations.
+
+## 0.7.0+1
+
+* Ensure communication from JAVA to Dart is done on the main UI thread.
+
+## 0.7.0
+
+* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`. [(commit)](https://github.com/flutter/plugins/commit/100c7470d4066b1d0f8f7e4ec6d7c943e736f970)
+ * Added support for capture orientation locking on Android and iOS.
+ * Fixed camera preview not rotating correctly on Android and iOS.
+ * Fixed camera preview sometimes appearing stretched on Android and iOS.
+ * Fixed videos & photos saving with the incorrect rotation on iOS.
+* New Features:
+ * Adds auto focus support for Android and iOS implementations. [(commmit)](https://github.com/flutter/plugins/commit/71a831790220f898bf8120c8a23840ac6e742db5)
+ * Adds ImageFormat selection for ImageStream and Video(iOS only). [(commit)](https://github.com/flutter/plugins/commit/da1b4638b750a5ff832d7be86a42831c42c6d6c0)
+* Bug Fixes:
+ * Fixes crash when taking a picture on iOS devices without flash. [(commit)](https://github.com/flutter/plugins/commit/831344490984b1feec007afc9c8595d80b6c13f4)
+ * Make sure the configured zoom scale is copied over to the final capture builder on Android. Fixes the issue where the preview is zoomed but the final picture is not. [(commit)](https://github.com/flutter/plugins/commit/5916f55664e1772a4c3f0c02c5c71fc11e491b76)
+ * Fixes crash with using inner camera on some Android devices. [(commit)](https://github.com/flutter/plugins/commit/980b674cb4020c1927917426211a87e275346d5e)
+ * Improved error feedback by differentiating between uninitialized and disposed camera controllers. [(commit)](https://github.com/flutter/plugins/commit/d0b7109f6b00a0eda03506fed2c74cc123ffc6f3)
+ * Fixes picture captures causing a crash on some Huawei devices. [(commit)](https://github.com/flutter/plugins/commit/6d18db83f00f4861ffe485aba2d1f8aa08845ce6)
+
+## 0.6.4+5
+
+* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.
+
+## 0.6.4+4
+
+* Set camera auto focus enabled by default.
+
+## 0.6.4+3
+
+* Detect if selected camera supports auto focus and act accordingly on Android. This solves a problem where front facing cameras are not capturing the picture because auto focus is not supported.
+
+## 0.6.4+2
+
+* Set ImageStreamReader listener to null to prevent stale images when streaming images.
+
+## 0.6.4+1
+
+* Added closeCaptureSession() to stopVideoRecording in Camera.java to fix an Android 6 crash.
+
+## 0.6.4
+
+* Adds auto exposure support for Android and iOS implementations.
+
+## 0.6.3+4
+
+* Revert previous dependency update: Changed dependency on camera_platform_interface to >=1.04 <1.1.0.
+
+## 0.6.3+3
+
+* Updated dependency on camera_platform_interface to ^1.2.0.
+
+## 0.6.3+2
+
+* Fixes crash on Android which occurs after video recording has stopped just before taking a picture.
+
+## 0.6.3+1
+
+* Fixes flash & torch modes not working on some Android devices.
+
+## 0.6.3
+
+* Adds torch mode as a flash mode for Android and iOS implementations.
+
+## 0.6.2+1
+
+* Fix the API documentation for the `CameraController.takePicture` method.
+
+## 0.6.2
+
+* Add zoom support for Android and iOS implementations.
+
+## 0.6.1+1
+
+* Added implementation of the `didFinishProcessingPhoto` on iOS which allows saving image metadata (EXIF) on iOS 11 and up.
+
+## 0.6.1
+
+* Add flash support for Android and iOS implementations.
+
+## 0.6.0+2
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 0.6.0+1
+
+Updated README to inform users that iOS 10.0+ is needed for use
+
+## 0.6.0
+
+As part of implementing federated architecture and making the interface compatible with the web this version contains the following **breaking changes**:
+
+Method changes in `CameraController`:
+- The `takePicture` method no longer accepts the `path` parameter, but instead returns the captured image as an instance of the `XFile` class;
+- The `startVideoRecording` method no longer accepts the `filePath`. Instead the recorded video is now returned as a `XFile` instance when the `stopVideoRecording` method completes;
+- The `stopVideoRecording` method now returns the captured video when it completes;
+- Added the `buildPreview` method which is now used to implement the CameraPreview widget.
+
+## 0.5.8+19
+
+* Update Flutter SDK constraint.
+
+## 0.5.8+18
+
+* Suppress unchecked warning in Android tests which prevented the tests to compile.
+
+## 0.5.8+17
+
+* Added Android 30 support.
+
+## 0.5.8+16
+
+* Moved package to camera/camera subdir, to allow for federated implementations.
+
+## 0.5.8+15
+
+* Added the `debugCheckIsDisposed` method which can be used in debug mode to validate if the `CameraController` class has been disposed.
+
+## 0.5.8+14
+
+* Changed the order of the setters for `mediaRecorder` in `MediaRecorderBuilder.java` to make it more readable.
+
+## 0.5.8+13
+
+* Added Dartdocs for all public APIs.
+
+## 0.5.8+12
+
+* Added information of video not working correctly on Android emulators to `README.md`.
+
+## 0.5.8+11
+
+* Fix rare nullptr exception on Android.
+* Updated README.md with information about handling App lifecycle changes.
+
+## 0.5.8+10
+
+* Suppress the `deprecated_member_use` warning in the example app for `ScaffoldMessenger.showSnackBar`.
+
+## 0.5.8+9
+
+* Update android compileSdkVersion to 29.
+
+## 0.5.8+8
+
+* Fixed garbled audio (in video) by setting audio encoding bitrate.
+
+## 0.5.8+7
+
+* Keep handling deprecated Android v1 classes for backward compatibility.
+
+## 0.5.8+6
+
+* Avoiding uses or overrides a deprecated API in CameraPlugin.java.
+
+## 0.5.8+5
+
+* Fix compilation/availability issues on iOS.
+
+## 0.5.8+4
+
+* Fixed bug caused by casting a `CameraAccessException` on Android.
+
+## 0.5.8+3
+
+* Fix bug in usage example in README.md
+
+## 0.5.8+2
+
+* Post-v2 embedding cleanups.
+
+## 0.5.8+1
+
+* Update lower bound of dart dependency to 2.1.0.
+
+## 0.5.8
+
+* Remove Android dependencies fallback.
+* Require Flutter SDK 1.12.13+hotfix.5 or greater.
+
+## 0.5.7+5
+
+* Replace deprecated `getFlutterEngine` call on Android.
+
+## 0.5.7+4
+
+* Add `pedantic` to dev_dependency.
+
+## 0.5.7+3
+
+* Fix an Android crash when permissions are requested multiple times.
+
+## 0.5.7+2
+
+* Remove the deprecated `author:` field from pubspec.yaml
+* Migrate the plugin to the pubspec platforms manifest.
+* Require Flutter SDK 1.10.0 or greater.
+
+## 0.5.7+1
+
+* Fix example null exception.
+
+## 0.5.7
+
+* Fix unawaited futures.
+
+## 0.5.6+4
+
+* Android: Use CameraDevice.TEMPLATE_RECORD to improve image streaming.
+
+## 0.5.6+3
+
+* Remove AndroidX warning.
+
+## 0.5.6+2
+
+* Include lifecycle dependency as a compileOnly one on Android to resolve
+ potential version conflicts with other transitive libraries.
+
+## 0.5.6+1
+
+* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX.
+
+## 0.5.6
+
+* Add support for the v2 Android embedding. This shouldn't affect existing
+ functionality.
+
+## 0.5.5+1
+
+* Fix event type check
+
+## 0.5.5
+
+* Define clang modules for iOS.
+
+## 0.5.4+3
+
+* Update and migrate iOS example project.
+
+## 0.5.4+2
+
+* Fix Android NullPointerException on devices with only front-facing camera.
+
+## 0.5.4+1
+
+* Fix Android pause and resume video crash when executing in APIs below 24.
+
+## 0.5.4
+
+* Add feature to pause and resume video recording.
+
+## 0.5.3+1
+
+* Fix too large request code for FragmentActivity users.
+
+## 0.5.3
+
+* Added new quality presets.
+* Now all quality presets can be used to control image capture quality.
+
+## 0.5.2+2
+
+* Fix memory leak related to not unregistering stream handler in FlutterEventChannel when disposing camera.
+
+## 0.5.2+1
+
+* Fix bug that prevented video recording with audio.
+
+## 0.5.2
+
+* Added capability to disable audio for the `CameraController`. (e.g. `CameraController(_, _,
+ enableAudio: false);`)
+
+## 0.5.1
+
+* Can now be compiled with earlier Android sdks below 21 when
+`<uses-sdk tools:overrideLibrary="io.flutter.plugins.camera"/>` has been added to the project
+`AndroidManifest.xml`. For sdks below 21, the plugin won't be registered and calls to it will throw
+a `MissingPluginException.`
+
+## 0.5.0
+
+* **Breaking Change** This plugin no longer handles closing and opening the camera on Android
+ lifecycle changes. Please use `WidgetsBindingObserver` to control camera resources on lifecycle
+ changes. See example project for example using `WidgetsBindingObserver`.
+
+## 0.4.3+2
+
+* Bump the minimum Flutter version to 1.2.0.
+* Add template type parameter to `invokeMethod` calls.
+
+## 0.4.3+1
+
+* Catch additional `Exception`s from Android and throw as `CameraException`s.
+
+## 0.4.3
+
+* Add capability to prepare the capture session for video recording on iOS.
+
+## 0.4.2
+
+* Add sensor orientation value to `CameraDescription`.
+
+## 0.4.1
+
+* Camera methods are ran in a background thread on iOS.
+
+## 0.4.0+3
+
+* Fixed a crash when the plugin is registered by a background FlutterView.
+
+## 0.4.0+2
+
+* Fix orientation of captured photos when camera is used for the first time on Android.
+
+## 0.4.0+1
+
+* Remove categories.
+
+## 0.4.0
+
+* **Breaking Change** Change iOS image stream format to `ImageFormatGroup.bgra8888` from
+ `ImageFormatGroup.yuv420`.
+
+## 0.3.0+4
+
+* Fixed bug causing black screen on some Android devices.
+
+## 0.3.0+3
+
+* Log a more detailed warning at build time about the previous AndroidX
+ migration.
+
+## 0.3.0+2
+
+* Fix issue with calculating iOS image orientation in certain edge cases.
+
+## 0.3.0+1
+
+* Remove initial method call invocation from static camera method.
+
+## 0.3.0
+
+* **Breaking change**. Migrate from the deprecated original Android Support
+ Library to AndroidX. This shouldn't result in any functional changes, but it
+ requires any Android apps using this plugin to [also
+ migrate](https://developer.android.com/jetpack/androidx/migrate) if they're
+ using the original support library.
+
+## 0.2.9+1
+
+* Fix a crash when failing to start preview.
+
+## 0.2.9
+
+* Save photo orientation data on iOS.
+
+## 0.2.8
+
+* Add access to the image stream from Dart.
+* Use `cameraController.startImageStream(listener)` to process the images.
+
+## 0.2.7
+
+* Fix issue with crash when the physical device's orientation is unknown.
+
+## 0.2.6
+
+* Update the camera to use the physical device's orientation instead of the UI
+ orientation on Android.
+
+## 0.2.5
+
+* Fix preview and video size with satisfying conditions of multiple outputs.
+
+## 0.2.4
+
+* Unregister the activity lifecycle callbacks when disposing the camera.
+
+## 0.2.3
+
+* Added path_provider and video_player as dev dependencies because the example uses them.
+* Updated example path_provider version to get Dart 2 support.
+
+## 0.2.2
+
+* iOS image capture is done in high quality (full camera size)
+
+## 0.2.1
+
+* Updated Gradle tooling to match Android Studio 3.1.2.
+
+## 0.2.0
+
+* Added support for video recording.
+* Changed the example app to add video recording.
+
+A lot of **breaking changes** in this version:
+
+Getter changes:
+ - Removed `isStarted`
+ - Renamed `initialized` to `isInitialized`
+ - Added `isRecordingVideo`
+
+Method changes:
+ - Renamed `capture` to `takePicture`
+ - Removed `start` (the preview starts automatically when `initialize` is called)
+ - Added `startVideoRecording(String filePath)`
+ - Removed `stop` (the preview stops automatically when `dispose` is called)
+ - Added `stopVideoRecording`
+
+## 0.1.2
+
+* Fix Dart 2 runtime errors.
+
+## 0.1.1
+
+* Fix Dart 2 runtime error.
+
+## 0.1.0
+
+* **Breaking change**. Set SDK constraints to match the Flutter beta release.
+
+## 0.0.4
+
+* Revert regression of `CameraController.capture()` introduced in v. 0.0.3.
+
+## 0.0.3
+
+* Improved resource cleanup on Android. Avoids crash on Activity restart.
+* Made the Future returned by `CameraController.dispose()` and `CameraController.capture()` actually complete on
+ Android.
+
+## 0.0.2
+
+* Simplified and upgraded Android project template to Android SDK 27.
+* Moved Android package to io.flutter.plugins.
+* Fixed warnings from the Dart 2.0 analyzer.
+
+## 0.0.1
+
+* Initial release
diff --git a/packages/camera/camera/LICENSE b/packages/camera/camera/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/camera/camera/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md
new file mode 100644
index 0000000..86b0355
--- /dev/null
+++ b/packages/camera/camera/README.md
@@ -0,0 +1,174 @@
+# Camera Plugin
+
+<?code-excerpt path-base="excerpts/packages/camera_example"?>
+
+[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera)
+
+A Flutter plugin for iOS, Android and Web allowing access to the device cameras.
+
+| | Android | iOS | Web |
+|----------------|---------|----------|------------------------|
+| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
+
+## Features
+
+* Display live camera preview in a widget.
+* Snapshots can be captured and saved to a file.
+* Record video.
+* Add access to the image stream from Dart.
+
+## Installation
+
+First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/).
+
+### iOS
+
+\* The camera plugin compiles for any version of iOS, but its functionality
+requires iOS 10 or higher. If compiling for iOS 9, make sure to programmatically
+check the version of iOS running on the device before using any camera plugin features.
+The [device_info_plus](https://pub.dev/packages/device_info_plus) plugin, for example, can be used to check the iOS version.
+
+Add two rows to the `ios/Runner/Info.plist`:
+
+* one with the key `Privacy - Camera Usage Description` and a usage description.
+* and one with the key `Privacy - Microphone Usage Description` and a usage description.
+
+If editing `Info.plist` as text, add:
+
+```xml
+<key>NSCameraUsageDescription</key>
+<string>your usage description here</string>
+<key>NSMicrophoneUsageDescription</key>
+<string>your usage description here</string>
+```
+
+### Android
+
+Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file.
+
+```groovy
+minSdkVersion 21
+```
+
+It's important to note that the `MediaRecorder` class is not working properly on emulators, as stated in the documentation: https://developer.android.com/reference/android/media/MediaRecorder. Specifically, when recording a video with sound enabled and trying to play it back, the duration won't be correct and you will only see the first frame.
+
+### Web integration
+
+For web integration details, see the
+[`camera_web` package](https://pub.dev/packages/camera_web).
+
+### Handling Lifecycle states
+
+As of version [0.5.0](https://github.com/flutter/plugins/blob/main/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so:
+
+<?code-excerpt "main.dart (AppLifecycle)"?>
+```dart
+@override
+void didChangeAppLifecycleState(AppLifecycleState state) {
+ final CameraController? cameraController = controller;
+
+ // App state changed before we got the chance to initialize.
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ return;
+ }
+
+ if (state == AppLifecycleState.inactive) {
+ cameraController.dispose();
+ } else if (state == AppLifecycleState.resumed) {
+ onNewCameraSelected(cameraController.description);
+ }
+}
+```
+
+### Handling camera access permissions
+
+Permission errors may be thrown when initializing the camera controller, and you are expected to handle them properly.
+
+Here is a list of all permission error codes that can be thrown:
+
+- `CameraAccessDenied`: Thrown when user denies the camera access permission.
+
+- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Camera in order to enable camera access.
+
+- `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control).
+
+- `AudioAccessDenied`: Thrown when user denies the audio access permission.
+
+- `AudioAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Microphone in order to enable audio access.
+
+- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control).
+
+### Example
+
+Here is a small example flutter app displaying a full screen camera preview.
+
+<?code-excerpt "readme_full_example.dart (FullAppExample)"?>
+```dart
+import 'package:camera/camera.dart';
+import 'package:flutter/material.dart';
+
+late List<CameraDescription> _cameras;
+
+Future<void> main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ _cameras = await availableCameras();
+ runApp(const CameraApp());
+}
+
+/// CameraApp is the Main Application.
+class CameraApp extends StatefulWidget {
+ /// Default Constructor
+ const CameraApp({Key? key}) : super(key: key);
+
+ @override
+ State<CameraApp> createState() => _CameraAppState();
+}
+
+class _CameraAppState extends State<CameraApp> {
+ late CameraController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = CameraController(_cameras[0], ResolutionPreset.max);
+ controller.initialize().then((_) {
+ if (!mounted) {
+ return;
+ }
+ setState(() {});
+ }).catchError((Object e) {
+ if (e is CameraException) {
+ switch (e.code) {
+ case 'CameraAccessDenied':
+ // Handle access errors here.
+ break;
+ default:
+ // Handle other errors here.
+ break;
+ }
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (!controller.value.isInitialized) {
+ return Container();
+ }
+ return MaterialApp(
+ home: CameraPreview(controller),
+ );
+ }
+}
+```
+
+For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/main/packages/camera/camera/example).
+
+[1]: https://pub.dev/packages/camera_web#limitations-on-the-web-platform
diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle
new file mode 100644
index 0000000..5d6af58
--- /dev/null
+++ b/packages/camera/camera/example/android/app/build.gradle
@@ -0,0 +1,64 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 31
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "io.flutter.plugins.cameraexample"
+ minSdkVersion 21
+ targetSdkVersion 28
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ profile {
+ matchingFallbacks = ['debug', 'release']
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test:rules:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..29e4134
--- /dev/null
+++ b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java
new file mode 100644
index 0000000..0f4298d
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java
@@ -0,0 +1,14 @@
+// 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface DartIntegrationTest {}
diff --git a/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java
new file mode 100644
index 0000000..39cae48
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java
@@ -0,0 +1,19 @@
+// 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.cameraexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.plugins.DartIntegrationTest;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@DartIntegrationTest
+@RunWith(FlutterTestRunner.class)
+public class FlutterActivityTest {
+ @Rule
+ public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}
diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cef2316
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.plugins.cameraexample">
+
+ <application
+ android:icon="@mipmap/ic_launcher"
+ android:label="camera_example">
+ <activity
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:launchMode="singleTop"
+ android:name="io.flutter.embedding.android.FlutterActivity"
+ android:theme="@style/LaunchTheme"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <meta-data android:name="flutterEmbedding" android:value="2"/>
+ </application>
+
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="true"/>
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.FLASHLIGHT"/>
+</manifest>
diff --git a/packages/camera/camera/example/android/app/src/main/res/drawable/launch_background.xml b/packages/camera/camera/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@android:color/white" />
+
+ <!-- You can insert your own image assets here -->
+ <!-- <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@mipmap/launch_image" />
+ </item> -->
+</layer-list>
diff --git a/packages/camera/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/camera/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/camera/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/camera/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/camera/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/camera/camera/example/android/app/src/main/res/values/styles.xml b/packages/camera/camera/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/camera/camera/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+ <!-- Show a splash screen on the activity. Automatically removed when
+ Flutter draws its first frame -->
+ <item name="android:windowBackground">@drawable/launch_background</item>
+ </style>
+</resources>
diff --git a/packages/camera/camera/example/android/build.gradle b/packages/camera/camera/example/android/build.gradle
new file mode 100644
index 0000000..c21bff8
--- /dev/null
+++ b/packages/camera/camera/example/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.0.1'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties
new file mode 100644
index 0000000..d0448f1
--- /dev/null
+++ b/packages/camera/camera/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx4G
+android.useAndroidX=true
+android.enableJetifier=false
+android.enableR8=true
diff --git a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..297f2fe
--- /dev/null
+++ b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
diff --git a/packages/camera/camera/example/android/settings.gradle b/packages/camera/camera/example/android/settings.gradle
new file mode 100644
index 0000000..115da6c
--- /dev/null
+++ b/packages/camera/camera/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/camera/camera/example/build.excerpt.yaml b/packages/camera/camera/example/build.excerpt.yaml
new file mode 100644
index 0000000..e317efa
--- /dev/null
+++ b/packages/camera/camera/example/build.excerpt.yaml
@@ -0,0 +1,15 @@
+targets:
+ $default:
+ sources:
+ include:
+ - lib/**
+ # Some default includes that aren't really used here but will prevent
+ # false-negative warnings:
+ - $package$
+ - lib/$lib$
+ exclude:
+ - '**/.*/**'
+ - '**/build/**'
+ builders:
+ code_excerpter|code_excerpter:
+ enabled: true
diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart
new file mode 100644
index 0000000..f0cc67f
--- /dev/null
+++ b/packages/camera/camera/example/integration_test/camera_test.dart
@@ -0,0 +1,293 @@
+// 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 'dart:async';
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:camera/camera.dart';
+import 'package:flutter/painting.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:video_player/video_player.dart';
+
+void main() {
+ late Directory testDir;
+
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ setUpAll(() async {
+ final Directory extDir = await getTemporaryDirectory();
+ testDir = await Directory('${extDir.path}/test').create(recursive: true);
+ });
+
+ tearDownAll(() async {
+ await testDir.delete(recursive: true);
+ });
+
+ final Map<ResolutionPreset, Size> presetExpectedSizes =
+ <ResolutionPreset, Size>{
+ ResolutionPreset.low:
+ Platform.isAndroid ? const Size(240, 320) : const Size(288, 352),
+ ResolutionPreset.medium:
+ Platform.isAndroid ? const Size(480, 720) : const Size(480, 640),
+ ResolutionPreset.high: const Size(720, 1280),
+ ResolutionPreset.veryHigh: const Size(1080, 1920),
+ ResolutionPreset.ultraHigh: const Size(2160, 3840),
+ // Don't bother checking for max here since it could be anything.
+ };
+
+ /// Verify that [actual] has dimensions that are at least as large as
+ /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns
+ /// whether the dimensions exactly match.
+ bool assertExpectedDimensions(Size expectedSize, Size actual) {
+ expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide));
+ expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide));
+ return actual.shortestSide == expectedSize.shortestSide &&
+ actual.longestSide == expectedSize.longestSide;
+ }
+
+ // This tests that the capture is no bigger than the preset, since we have
+ // automatic code to fall back to smaller sizes when we need to. Returns
+ // whether the image is exactly the desired resolution.
+ Future<bool> testCaptureImageResolution(
+ CameraController controller, ResolutionPreset preset) async {
+ final Size expectedSize = presetExpectedSizes[preset]!;
+
+ // Take Picture
+ final XFile file = await controller.takePicture();
+
+ // Load picture
+ final File fileImage = File(file.path);
+ final Image image = await decodeImageFromList(fileImage.readAsBytesSync());
+
+ // Verify image dimensions are as expected
+ expect(image, isNotNull);
+ return assertExpectedDimensions(
+ expectedSize, Size(image.height.toDouble(), image.width.toDouble()));
+ }
+
+ testWidgets(
+ 'Capture specific image resolutions',
+ (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+ for (final CameraDescription cameraDescription in cameras) {
+ bool previousPresetExactlySupported = true;
+ for (final MapEntry<ResolutionPreset, Size> preset
+ in presetExpectedSizes.entries) {
+ final CameraController controller =
+ CameraController(cameraDescription, preset.key);
+ await controller.initialize();
+ final bool presetExactlySupported =
+ await testCaptureImageResolution(controller, preset.key);
+ assert(!(!previousPresetExactlySupported && presetExactlySupported),
+ 'The camera took higher resolution pictures at a lower resolution.');
+ previousPresetExactlySupported = presetExactlySupported;
+ await controller.dispose();
+ }
+ }
+ },
+ // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686.
+ skip: true,
+ );
+
+ // This tests that the capture is no bigger than the preset, since we have
+ // automatic code to fall back to smaller sizes when we need to. Returns
+ // whether the image is exactly the desired resolution.
+ Future<bool> testCaptureVideoResolution(
+ CameraController controller, ResolutionPreset preset) async {
+ final Size expectedSize = presetExpectedSizes[preset]!;
+
+ // Take Video
+ await controller.startVideoRecording();
+ sleep(const Duration(milliseconds: 300));
+ final XFile file = await controller.stopVideoRecording();
+
+ // Load video metadata
+ final File videoFile = File(file.path);
+ final VideoPlayerController videoController =
+ VideoPlayerController.file(videoFile);
+ await videoController.initialize();
+ final Size video = videoController.value.size;
+
+ // Verify image dimensions are as expected
+ expect(video, isNotNull);
+ return assertExpectedDimensions(
+ expectedSize, Size(video.height, video.width));
+ }
+
+ testWidgets(
+ 'Capture specific video resolutions',
+ (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+ for (final CameraDescription cameraDescription in cameras) {
+ bool previousPresetExactlySupported = true;
+ for (final MapEntry<ResolutionPreset, Size> preset
+ in presetExpectedSizes.entries) {
+ final CameraController controller =
+ CameraController(cameraDescription, preset.key);
+ await controller.initialize();
+ await controller.prepareForVideoRecording();
+ final bool presetExactlySupported =
+ await testCaptureVideoResolution(controller, preset.key);
+ assert(!(!previousPresetExactlySupported && presetExactlySupported),
+ 'The camera took higher resolution pictures at a lower resolution.');
+ previousPresetExactlySupported = presetExactlySupported;
+ await controller.dispose();
+ }
+ }
+ },
+ // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686.
+ skip: true,
+ );
+
+ testWidgets('Pause and resume video recording', (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+
+ final CameraController controller = CameraController(
+ cameras[0],
+ ResolutionPreset.low,
+ enableAudio: false,
+ );
+
+ await controller.initialize();
+ await controller.prepareForVideoRecording();
+
+ int startPause;
+ int timePaused = 0;
+
+ await controller.startVideoRecording();
+ final int recordingStart = DateTime.now().millisecondsSinceEpoch;
+ sleep(const Duration(milliseconds: 500));
+
+ await controller.pauseVideoRecording();
+ startPause = DateTime.now().millisecondsSinceEpoch;
+ sleep(const Duration(milliseconds: 500));
+ await controller.resumeVideoRecording();
+ timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
+
+ sleep(const Duration(milliseconds: 500));
+
+ await controller.pauseVideoRecording();
+ startPause = DateTime.now().millisecondsSinceEpoch;
+ sleep(const Duration(milliseconds: 500));
+ await controller.resumeVideoRecording();
+ timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
+
+ sleep(const Duration(milliseconds: 500));
+
+ final XFile file = await controller.stopVideoRecording();
+ final int recordingTime =
+ DateTime.now().millisecondsSinceEpoch - recordingStart;
+
+ final File videoFile = File(file.path);
+ final VideoPlayerController videoController = VideoPlayerController.file(
+ videoFile,
+ );
+ await videoController.initialize();
+ final int duration = videoController.value.duration.inMilliseconds;
+ await videoController.dispose();
+
+ expect(duration, lessThan(recordingTime - timePaused));
+ }, skip: !Platform.isAndroid);
+
+ testWidgets(
+ 'Android image streaming',
+ (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+
+ final CameraController controller = CameraController(
+ cameras[0],
+ ResolutionPreset.low,
+ enableAudio: false,
+ );
+
+ await controller.initialize();
+ bool isDetecting = false;
+
+ await controller.startImageStream((CameraImage image) {
+ if (isDetecting) {
+ return;
+ }
+
+ isDetecting = true;
+
+ expectLater(image, isNotNull).whenComplete(() => isDetecting = false);
+ });
+
+ expect(controller.value.isStreamingImages, true);
+
+ sleep(const Duration(milliseconds: 500));
+
+ await controller.stopImageStream();
+ await controller.dispose();
+ },
+ skip: !Platform.isAndroid,
+ );
+
+ /// Start streaming with specifying the ImageFormatGroup.
+ Future<CameraImage> startStreaming(List<CameraDescription> cameras,
+ ImageFormatGroup? imageFormatGroup) async {
+ final CameraController controller = CameraController(
+ cameras.first,
+ ResolutionPreset.low,
+ enableAudio: false,
+ imageFormatGroup: imageFormatGroup,
+ );
+
+ await controller.initialize();
+ final Completer<CameraImage> completer = Completer<CameraImage>();
+
+ await controller.startImageStream((CameraImage image) {
+ if (!completer.isCompleted) {
+ Future<void>(() async {
+ await controller.stopImageStream();
+ await controller.dispose();
+ }).then((Object? value) {
+ completer.complete(image);
+ });
+ }
+ });
+ return completer.future;
+ }
+
+ testWidgets(
+ 'iOS image streaming with imageFormatGroup',
+ (WidgetTester tester) async {
+ final List<CameraDescription> cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+
+ CameraImage image = await startStreaming(cameras, null);
+ expect(image, isNotNull);
+ expect(image.format.group, ImageFormatGroup.bgra8888);
+ expect(image.planes.length, 1);
+
+ image = await startStreaming(cameras, ImageFormatGroup.yuv420);
+ expect(image, isNotNull);
+ expect(image.format.group, ImageFormatGroup.yuv420);
+ expect(image.planes.length, 2);
+
+ image = await startStreaming(cameras, ImageFormatGroup.bgra8888);
+ expect(image, isNotNull);
+ expect(image.format.group, ImageFormatGroup.bgra8888);
+ expect(image.planes.length, 1);
+ },
+ skip: !Platform.isIOS,
+ );
+}
diff --git a/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..3a9c234
--- /dev/null
+++ b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>App</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.flutter.flutter.app</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>App</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>MinimumOSVersion</key>
+ <string>9.0</string>
+</dict>
+</plist>
diff --git a/packages/camera/camera/example/ios/Flutter/Debug.xcconfig b/packages/camera/camera/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..b2f5fae
--- /dev/null
+++ b/packages/camera/camera/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,3 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/camera/camera/example/ios/Flutter/Release.xcconfig b/packages/camera/camera/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..88c2914
--- /dev/null
+++ b/packages/camera/camera/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,3 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/camera/camera/example/ios/Podfile b/packages/camera/camera/example/ios/Podfile
new file mode 100644
index 0000000..f7d6a5e
--- /dev/null
+++ b/packages/camera/camera/example/ios/Podfile
@@ -0,0 +1,38 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..99433b0
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,472 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
+ 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+ 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+ 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+ 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 89D82918721FABF772705DB0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
+ A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 3242FD2B467C15C62200632F /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 89D82918721FABF772705DB0 /* libPods-Runner.a */,
+ 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "<group>";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ FD386F00E98D73419C929072 /* Pods */,
+ 3242FD2B467C15C62200632F /* Frameworks */,
+ );
+ sourceTree = "<group>";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ );
+ path = Runner;
+ sourceTree = "<group>";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146F21CF9000F007C117D /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ FD386F00E98D73419C929072 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */,
+ 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */,
+ 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */,
+ A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "The Flutter Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+ 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "<group>";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..f4b3c10
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "1300"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "03BB76672665316900CE5A93"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ <AdditionalOption
+ key = "NSZombieEnabled"
+ value = "YES"
+ isEnabled = "YES">
+ </AdditionalOption>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+ BuildableName = "Runner.app"
+ BlueprintName = "Runner"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "group:Runner.xcodeproj">
+ </FileRef>
+ <FileRef
+ location = "group:Pods/Pods.xcodeproj">
+ </FileRef>
+</Workspace>
diff --git a/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IDEDidComputeMac32BitWarning</key>
+ <true/>
+</dict>
+</plist>
diff --git a/packages/camera/camera/example/ios/Runner/AppDelegate.h b/packages/camera/camera/example/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..0681d28
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/AppDelegate.h
@@ -0,0 +1,10 @@
+// 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 <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/camera/camera/example/ios/Runner/AppDelegate.m b/packages/camera/camera/example/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..30b8796
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/AppDelegate.m
@@ -0,0 +1,17 @@
+// 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.
+
+#include "AppDelegate.h"
+#include "GeneratedPluginRegistrant.h"
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ [GeneratedPluginRegistrant registerWithRegistry:self];
+ // Override point for customization after application launch.
+ return [super application:application didFinishLaunchingWithOptions:launchOptions];
+}
+
+@end
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d225b3c
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,121 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "size" : "1024x1024",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/packages/camera/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/camera/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <subviews>
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
+ </imageView>
+ </subviews>
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <constraints>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
+ </constraints>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+ <resources>
+ <image name="LaunchImage" width="168" height="185"/>
+ </resources>
+</document>
diff --git a/packages/camera/camera/example/ios/Runner/Base.lproj/Main.storyboard b/packages/camera/camera/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--Flutter View Controller-->
+ <scene sceneID="tne-QT-ifu">
+ <objects>
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..ff2e341
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/Info.plist
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>camera_example</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSApplicationCategoryType</key>
+ <string></string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>NSCameraUsageDescription</key>
+ <string>Can I use the camera please? Only for demo purpose of the app</string>
+ <key>NSMicrophoneUsageDescription</key>
+ <string>Only for demo purpose of the app</string>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/packages/camera/camera/example/ios/Runner/main.m b/packages/camera/camera/example/ios/Runner/main.m
new file mode 100644
index 0000000..d1224fe
--- /dev/null
+++ b/packages/camera/camera/example/ios/Runner/main.m
@@ -0,0 +1,19 @@
+// 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 <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char *argv[]) {
+ @autoreleasepool {
+ // The setup logic in `AppDelegate::didFinishLaunchingWithOptions:` eventually sends camera
+ // operations on the background queue, which would run concurrently with the test cases during
+ // unit tests, making the debugging process confusing. This setup is actually not necessary for
+ // the unit tests, so it is better to skip the AppDelegate when running unit tests.
+ BOOL isTesting = NSClassFromString(@"XCTestCase") != nil;
+ return UIApplicationMain(argc, argv, nil,
+ isTesting ? nil : NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
new file mode 100644
index 0000000..b343b6d
--- /dev/null
+++ b/packages/camera/camera/example/lib/main.dart
@@ -0,0 +1,1080 @@
+// 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 'dart:async';
+import 'dart:io';
+
+import 'package:camera/camera.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:video_player/video_player.dart';
+
+/// Camera example home widget.
+class CameraExampleHome extends StatefulWidget {
+ /// Default Constructor
+ const CameraExampleHome({Key? key}) : super(key: key);
+
+ @override
+ State<CameraExampleHome> createState() {
+ return _CameraExampleHomeState();
+ }
+}
+
+/// Returns a suitable camera icon for [direction].
+IconData getCameraLensIcon(CameraLensDirection direction) {
+ switch (direction) {
+ case CameraLensDirection.back:
+ return Icons.camera_rear;
+ case CameraLensDirection.front:
+ return Icons.camera_front;
+ case CameraLensDirection.external:
+ return Icons.camera;
+ }
+ // This enum is from a different package, so a new value could be added at
+ // any time. The example should keep working if that happens.
+ // ignore: dead_code
+ return Icons.camera;
+}
+
+void _logError(String code, String? message) {
+ // ignore: avoid_print
+ print('Error: $code${message == null ? '' : '\nError Message: $message'}');
+}
+
+class _CameraExampleHomeState extends State<CameraExampleHome>
+ with WidgetsBindingObserver, TickerProviderStateMixin {
+ CameraController? controller;
+ XFile? imageFile;
+ XFile? videoFile;
+ VideoPlayerController? videoController;
+ VoidCallback? videoPlayerListener;
+ bool enableAudio = true;
+ double _minAvailableExposureOffset = 0.0;
+ double _maxAvailableExposureOffset = 0.0;
+ double _currentExposureOffset = 0.0;
+ late AnimationController _flashModeControlRowAnimationController;
+ late Animation<double> _flashModeControlRowAnimation;
+ late AnimationController _exposureModeControlRowAnimationController;
+ late Animation<double> _exposureModeControlRowAnimation;
+ late AnimationController _focusModeControlRowAnimationController;
+ late Animation<double> _focusModeControlRowAnimation;
+ double _minAvailableZoom = 1.0;
+ double _maxAvailableZoom = 1.0;
+ double _currentScale = 1.0;
+ double _baseScale = 1.0;
+
+ // Counting pointers (number of user fingers on screen)
+ int _pointers = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+
+ _flashModeControlRowAnimationController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+ _flashModeControlRowAnimation = CurvedAnimation(
+ parent: _flashModeControlRowAnimationController,
+ curve: Curves.easeInCubic,
+ );
+ _exposureModeControlRowAnimationController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+ _exposureModeControlRowAnimation = CurvedAnimation(
+ parent: _exposureModeControlRowAnimationController,
+ curve: Curves.easeInCubic,
+ );
+ _focusModeControlRowAnimationController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+ _focusModeControlRowAnimation = CurvedAnimation(
+ parent: _focusModeControlRowAnimationController,
+ curve: Curves.easeInCubic,
+ );
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ _flashModeControlRowAnimationController.dispose();
+ _exposureModeControlRowAnimationController.dispose();
+ super.dispose();
+ }
+
+ // #docregion AppLifecycle
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ final CameraController? cameraController = controller;
+
+ // App state changed before we got the chance to initialize.
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ return;
+ }
+
+ if (state == AppLifecycleState.inactive) {
+ cameraController.dispose();
+ } else if (state == AppLifecycleState.resumed) {
+ onNewCameraSelected(cameraController.description);
+ }
+ }
+ // #enddocregion AppLifecycle
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Camera example'),
+ ),
+ body: Column(
+ children: <Widget>[
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.black,
+ border: Border.all(
+ color:
+ controller != null && controller!.value.isRecordingVideo
+ ? Colors.redAccent
+ : Colors.grey,
+ width: 3.0,
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(1.0),
+ child: Center(
+ child: _cameraPreviewWidget(),
+ ),
+ ),
+ ),
+ ),
+ _captureControlRowWidget(),
+ _modeControlRowWidget(),
+ Padding(
+ padding: const EdgeInsets.all(5.0),
+ child: Row(
+ children: <Widget>[
+ _cameraTogglesRowWidget(),
+ _thumbnailWidget(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ /// Display the preview from the camera (or a message if the preview is not available).
+ Widget _cameraPreviewWidget() {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ return const Text(
+ 'Tap a camera',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 24.0,
+ fontWeight: FontWeight.w900,
+ ),
+ );
+ } else {
+ return Listener(
+ onPointerDown: (_) => _pointers++,
+ onPointerUp: (_) => _pointers--,
+ child: CameraPreview(
+ controller!,
+ child: LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onScaleStart: _handleScaleStart,
+ onScaleUpdate: _handleScaleUpdate,
+ onTapDown: (TapDownDetails details) =>
+ onViewFinderTap(details, constraints),
+ );
+ }),
+ ),
+ );
+ }
+ }
+
+ void _handleScaleStart(ScaleStartDetails details) {
+ _baseScale = _currentScale;
+ }
+
+ Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
+ // When there are not exactly two fingers on screen don't scale
+ if (controller == null || _pointers != 2) {
+ return;
+ }
+
+ _currentScale = (_baseScale * details.scale)
+ .clamp(_minAvailableZoom, _maxAvailableZoom);
+
+ await controller!.setZoomLevel(_currentScale);
+ }
+
+ /// Display the thumbnail of the captured image or video.
+ Widget _thumbnailWidget() {
+ final VideoPlayerController? localVideoController = videoController;
+
+ return Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ if (localVideoController == null && imageFile == null)
+ Container()
+ else
+ SizedBox(
+ width: 64.0,
+ height: 64.0,
+ child: (localVideoController == null)
+ ? (
+ // The captured image on the web contains a network-accessible URL
+ // pointing to a location within the browser. It may be displayed
+ // either with Image.network or Image.memory after loading the image
+ // bytes to memory.
+ kIsWeb
+ ? Image.network(imageFile!.path)
+ : Image.file(File(imageFile!.path)))
+ : Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.pink)),
+ child: Center(
+ child: AspectRatio(
+ aspectRatio:
+ localVideoController.value.size != null
+ ? localVideoController.value.aspectRatio
+ : 1.0,
+ child: VideoPlayer(localVideoController)),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Display a bar with buttons to change the flash and exposure modes
+ Widget _modeControlRowWidget() {
+ return Column(
+ children: <Widget>[
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ IconButton(
+ icon: const Icon(Icons.flash_on),
+ color: Colors.blue,
+ onPressed: controller != null ? onFlashModeButtonPressed : null,
+ ),
+ // The exposure and focus mode are currently not supported on the web.
+ ...!kIsWeb
+ ? <Widget>[
+ IconButton(
+ icon: const Icon(Icons.exposure),
+ color: Colors.blue,
+ onPressed: controller != null
+ ? onExposureModeButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.filter_center_focus),
+ color: Colors.blue,
+ onPressed:
+ controller != null ? onFocusModeButtonPressed : null,
+ )
+ ]
+ : <Widget>[],
+ IconButton(
+ icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute),
+ color: Colors.blue,
+ onPressed: controller != null ? onAudioModeButtonPressed : null,
+ ),
+ IconButton(
+ icon: Icon(controller?.value.isCaptureOrientationLocked ?? false
+ ? Icons.screen_lock_rotation
+ : Icons.screen_rotation),
+ color: Colors.blue,
+ onPressed: controller != null
+ ? onCaptureOrientationLockButtonPressed
+ : null,
+ ),
+ ],
+ ),
+ _flashModeControlRowWidget(),
+ _exposureModeControlRowWidget(),
+ _focusModeControlRowWidget(),
+ ],
+ );
+ }
+
+ Widget _flashModeControlRowWidget() {
+ return SizeTransition(
+ sizeFactor: _flashModeControlRowAnimation,
+ child: ClipRect(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ IconButton(
+ icon: const Icon(Icons.flash_off),
+ color: controller?.value.flashMode == FlashMode.off
+ ? Colors.orange
+ : Colors.blue,
+ onPressed: controller != null
+ ? () => onSetFlashModeButtonPressed(FlashMode.off)
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.flash_auto),
+ color: controller?.value.flashMode == FlashMode.auto
+ ? Colors.orange
+ : Colors.blue,
+ onPressed: controller != null
+ ? () => onSetFlashModeButtonPressed(FlashMode.auto)
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.flash_on),
+ color: controller?.value.flashMode == FlashMode.always
+ ? Colors.orange
+ : Colors.blue,
+ onPressed: controller != null
+ ? () => onSetFlashModeButtonPressed(FlashMode.always)
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.highlight),
+ color: controller?.value.flashMode == FlashMode.torch
+ ? Colors.orange
+ : Colors.blue,
+ onPressed: controller != null
+ ? () => onSetFlashModeButtonPressed(FlashMode.torch)
+ : null,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _exposureModeControlRowWidget() {
+ final ButtonStyle styleAuto = TextButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: controller?.value.exposureMode == ExposureMode.auto
+ ? Colors.orange
+ : Colors.blue,
+ );
+ final ButtonStyle styleLocked = TextButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: controller?.value.exposureMode == ExposureMode.locked
+ ? Colors.orange
+ : Colors.blue,
+ );
+
+ return SizeTransition(
+ sizeFactor: _exposureModeControlRowAnimation,
+ child: ClipRect(
+ child: Container(
+ color: Colors.grey.shade50,
+ child: Column(
+ children: <Widget>[
+ const Center(
+ child: Text('Exposure Mode'),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ TextButton(
+ style: styleAuto,
+ onPressed: controller != null
+ ? () =>
+ onSetExposureModeButtonPressed(ExposureMode.auto)
+ : null,
+ onLongPress: () {
+ if (controller != null) {
+ controller!.setExposurePoint(null);
+ showInSnackBar('Resetting exposure point');
+ }
+ },
+ child: const Text('AUTO'),
+ ),
+ TextButton(
+ style: styleLocked,
+ onPressed: controller != null
+ ? () =>
+ onSetExposureModeButtonPressed(ExposureMode.locked)
+ : null,
+ child: const Text('LOCKED'),
+ ),
+ TextButton(
+ style: styleLocked,
+ onPressed: controller != null
+ ? () => controller!.setExposureOffset(0.0)
+ : null,
+ child: const Text('RESET OFFSET'),
+ ),
+ ],
+ ),
+ const Center(
+ child: Text('Exposure Offset'),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ Text(_minAvailableExposureOffset.toString()),
+ Slider(
+ value: _currentExposureOffset,
+ min: _minAvailableExposureOffset,
+ max: _maxAvailableExposureOffset,
+ label: _currentExposureOffset.toString(),
+ onChanged: _minAvailableExposureOffset ==
+ _maxAvailableExposureOffset
+ ? null
+ : setExposureOffset,
+ ),
+ Text(_maxAvailableExposureOffset.toString()),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _focusModeControlRowWidget() {
+ final ButtonStyle styleAuto = TextButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: controller?.value.focusMode == FocusMode.auto
+ ? Colors.orange
+ : Colors.blue,
+ );
+ final ButtonStyle styleLocked = TextButton.styleFrom(
+ // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+ // ignore: deprecated_member_use
+ primary: controller?.value.focusMode == FocusMode.locked
+ ? Colors.orange
+ : Colors.blue,
+ );
+
+ return SizeTransition(
+ sizeFactor: _focusModeControlRowAnimation,
+ child: ClipRect(
+ child: Container(
+ color: Colors.grey.shade50,
+ child: Column(
+ children: <Widget>[
+ const Center(
+ child: Text('Focus Mode'),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ TextButton(
+ style: styleAuto,
+ onPressed: controller != null
+ ? () => onSetFocusModeButtonPressed(FocusMode.auto)
+ : null,
+ onLongPress: () {
+ if (controller != null) {
+ controller!.setFocusPoint(null);
+ }
+ showInSnackBar('Resetting focus point');
+ },
+ child: const Text('AUTO'),
+ ),
+ TextButton(
+ style: styleLocked,
+ onPressed: controller != null
+ ? () => onSetFocusModeButtonPressed(FocusMode.locked)
+ : null,
+ child: const Text('LOCKED'),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Display the control bar with buttons to take pictures and record videos.
+ Widget _captureControlRowWidget() {
+ final CameraController? cameraController = controller;
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ IconButton(
+ icon: const Icon(Icons.camera_alt),
+ color: Colors.blue,
+ onPressed: cameraController != null &&
+ cameraController.value.isInitialized &&
+ !cameraController.value.isRecordingVideo
+ ? onTakePictureButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.videocam),
+ color: Colors.blue,
+ onPressed: cameraController != null &&
+ cameraController.value.isInitialized &&
+ !cameraController.value.isRecordingVideo
+ ? onVideoRecordButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: cameraController != null &&
+ cameraController.value.isRecordingPaused
+ ? const Icon(Icons.play_arrow)
+ : const Icon(Icons.pause),
+ color: Colors.blue,
+ onPressed: cameraController != null &&
+ cameraController.value.isInitialized &&
+ cameraController.value.isRecordingVideo
+ ? (cameraController.value.isRecordingPaused)
+ ? onResumeButtonPressed
+ : onPauseButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.stop),
+ color: Colors.red,
+ onPressed: cameraController != null &&
+ cameraController.value.isInitialized &&
+ cameraController.value.isRecordingVideo
+ ? onStopButtonPressed
+ : null,
+ ),
+ IconButton(
+ icon: const Icon(Icons.pause_presentation),
+ color:
+ cameraController != null && cameraController.value.isPreviewPaused
+ ? Colors.red
+ : Colors.blue,
+ onPressed:
+ cameraController == null ? null : onPausePreviewButtonPressed,
+ ),
+ ],
+ );
+ }
+
+ /// Display a row of toggle to select the camera (or a message if no camera is available).
+ Widget _cameraTogglesRowWidget() {
+ final List<Widget> toggles = <Widget>[];
+
+ void onChanged(CameraDescription? description) {
+ if (description == null) {
+ return;
+ }
+
+ onNewCameraSelected(description);
+ }
+
+ if (_cameras.isEmpty) {
+ SchedulerBinding.instance.addPostFrameCallback((_) async {
+ showInSnackBar('No camera found.');
+ });
+ return const Text('None');
+ } else {
+ for (final CameraDescription cameraDescription in _cameras) {
+ toggles.add(
+ SizedBox(
+ width: 90.0,
+ child: RadioListTile<CameraDescription>(
+ title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
+ groupValue: controller?.description,
+ value: cameraDescription,
+ onChanged:
+ controller != null && controller!.value.isRecordingVideo
+ ? null
+ : onChanged,
+ ),
+ ),
+ );
+ }
+ }
+
+ return Row(children: toggles);
+ }
+
+ String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
+
+ void showInSnackBar(String message) {
+ ScaffoldMessenger.of(context)
+ .showSnackBar(SnackBar(content: Text(message)));
+ }
+
+ void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
+ if (controller == null) {
+ return;
+ }
+
+ final CameraController cameraController = controller!;
+
+ final Offset offset = Offset(
+ details.localPosition.dx / constraints.maxWidth,
+ details.localPosition.dy / constraints.maxHeight,
+ );
+ cameraController.setExposurePoint(offset);
+ cameraController.setFocusPoint(offset);
+ }
+
+ Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
+ final CameraController? oldController = controller;
+ if (oldController != null) {
+ // `controller` needs to be set to null before getting disposed,
+ // to avoid a race condition when we use the controller that is being
+ // disposed. This happens when camera permission dialog shows up,
+ // which triggers `didChangeAppLifecycleState`, which disposes and
+ // re-creates the controller.
+ controller = null;
+ await oldController.dispose();
+ }
+
+ final CameraController cameraController = CameraController(
+ cameraDescription,
+ kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
+ enableAudio: enableAudio,
+ imageFormatGroup: ImageFormatGroup.jpeg,
+ );
+
+ controller = cameraController;
+
+ // If the controller is updated then update the UI.
+ cameraController.addListener(() {
+ if (mounted) {
+ setState(() {});
+ }
+ if (cameraController.value.hasError) {
+ showInSnackBar(
+ 'Camera error ${cameraController.value.errorDescription}');
+ }
+ });
+
+ try {
+ await cameraController.initialize();
+ await Future.wait(<Future<Object?>>[
+ // The exposure mode is currently not supported on the web.
+ ...!kIsWeb
+ ? <Future<Object?>>[
+ cameraController.getMinExposureOffset().then(
+ (double value) => _minAvailableExposureOffset = value),
+ cameraController
+ .getMaxExposureOffset()
+ .then((double value) => _maxAvailableExposureOffset = value)
+ ]
+ : <Future<Object?>>[],
+ cameraController
+ .getMaxZoomLevel()
+ .then((double value) => _maxAvailableZoom = value),
+ cameraController
+ .getMinZoomLevel()
+ .then((double value) => _minAvailableZoom = value),
+ ]);
+ } on CameraException catch (e) {
+ switch (e.code) {
+ case 'CameraAccessDenied':
+ showInSnackBar('You have denied camera access.');
+ break;
+ case 'CameraAccessDeniedWithoutPrompt':
+ // iOS only
+ showInSnackBar('Please go to Settings app to enable camera access.');
+ break;
+ case 'CameraAccessRestricted':
+ // iOS only
+ showInSnackBar('Camera access is restricted.');
+ break;
+ case 'AudioAccessDenied':
+ showInSnackBar('You have denied audio access.');
+ break;
+ case 'AudioAccessDeniedWithoutPrompt':
+ // iOS only
+ showInSnackBar('Please go to Settings app to enable audio access.');
+ break;
+ case 'AudioAccessRestricted':
+ // iOS only
+ showInSnackBar('Audio access is restricted.');
+ break;
+ default:
+ _showCameraException(e);
+ break;
+ }
+ }
+
+ if (mounted) {
+ setState(() {});
+ }
+ }
+
+ void onTakePictureButtonPressed() {
+ takePicture().then((XFile? file) {
+ if (mounted) {
+ setState(() {
+ imageFile = file;
+ videoController?.dispose();
+ videoController = null;
+ });
+ if (file != null) {
+ showInSnackBar('Picture saved to ${file.path}');
+ }
+ }
+ });
+ }
+
+ void onFlashModeButtonPressed() {
+ if (_flashModeControlRowAnimationController.value == 1) {
+ _flashModeControlRowAnimationController.reverse();
+ } else {
+ _flashModeControlRowAnimationController.forward();
+ _exposureModeControlRowAnimationController.reverse();
+ _focusModeControlRowAnimationController.reverse();
+ }
+ }
+
+ void onExposureModeButtonPressed() {
+ if (_exposureModeControlRowAnimationController.value == 1) {
+ _exposureModeControlRowAnimationController.reverse();
+ } else {
+ _exposureModeControlRowAnimationController.forward();
+ _flashModeControlRowAnimationController.reverse();
+ _focusModeControlRowAnimationController.reverse();
+ }
+ }
+
+ void onFocusModeButtonPressed() {
+ if (_focusModeControlRowAnimationController.value == 1) {
+ _focusModeControlRowAnimationController.reverse();
+ } else {
+ _focusModeControlRowAnimationController.forward();
+ _flashModeControlRowAnimationController.reverse();
+ _exposureModeControlRowAnimationController.reverse();
+ }
+ }
+
+ void onAudioModeButtonPressed() {
+ enableAudio = !enableAudio;
+ if (controller != null) {
+ onNewCameraSelected(controller!.description);
+ }
+ }
+
+ Future<void> onCaptureOrientationLockButtonPressed() async {
+ try {
+ if (controller != null) {
+ final CameraController cameraController = controller!;
+ if (cameraController.value.isCaptureOrientationLocked) {
+ await cameraController.unlockCaptureOrientation();
+ showInSnackBar('Capture orientation unlocked');
+ } else {
+ await cameraController.lockCaptureOrientation();
+ showInSnackBar(
+ 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
+ }
+ }
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ }
+ }
+
+ void onSetFlashModeButtonPressed(FlashMode mode) {
+ setFlashMode(mode).then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ showInSnackBar('Flash mode set to ${mode.toString().split('.').last}');
+ });
+ }
+
+ void onSetExposureModeButtonPressed(ExposureMode mode) {
+ setExposureMode(mode).then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}');
+ });
+ }
+
+ void onSetFocusModeButtonPressed(FocusMode mode) {
+ setFocusMode(mode).then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ showInSnackBar('Focus mode set to ${mode.toString().split('.').last}');
+ });
+ }
+
+ void onVideoRecordButtonPressed() {
+ startVideoRecording().then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ });
+ }
+
+ void onStopButtonPressed() {
+ stopVideoRecording().then((XFile? file) {
+ if (mounted) {
+ setState(() {});
+ }
+ if (file != null) {
+ showInSnackBar('Video recorded to ${file.path}');
+ videoFile = file;
+ _startVideoPlayer();
+ }
+ });
+ }
+
+ Future<void> onPausePreviewButtonPressed() async {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ showInSnackBar('Error: select a camera first.');
+ return;
+ }
+
+ if (cameraController.value.isPreviewPaused) {
+ await cameraController.resumePreview();
+ } else {
+ await cameraController.pausePreview();
+ }
+
+ if (mounted) {
+ setState(() {});
+ }
+ }
+
+ void onPauseButtonPressed() {
+ pauseVideoRecording().then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ showInSnackBar('Video recording paused');
+ });
+ }
+
+ void onResumeButtonPressed() {
+ resumeVideoRecording().then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ showInSnackBar('Video recording resumed');
+ });
+ }
+
+ Future<void> startVideoRecording() async {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ showInSnackBar('Error: select a camera first.');
+ return;
+ }
+
+ if (cameraController.value.isRecordingVideo) {
+ // A recording is already started, do nothing.
+ return;
+ }
+
+ try {
+ await cameraController.startVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return;
+ }
+ }
+
+ Future<XFile?> stopVideoRecording() async {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isRecordingVideo) {
+ return null;
+ }
+
+ try {
+ return cameraController.stopVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return null;
+ }
+ }
+
+ Future<void> pauseVideoRecording() async {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isRecordingVideo) {
+ return;
+ }
+
+ try {
+ await cameraController.pauseVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> resumeVideoRecording() async {
+ final CameraController? cameraController = controller;
+
+ if (cameraController == null || !cameraController.value.isRecordingVideo) {
+ return;
+ }
+
+ try {
+ await cameraController.resumeVideoRecording();
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> setFlashMode(FlashMode mode) async {
+ if (controller == null) {
+ return;
+ }
+
+ try {
+ await controller!.setFlashMode(mode);
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> setExposureMode(ExposureMode mode) async {
+ if (controller == null) {
+ return;
+ }
+
+ try {
+ await controller!.setExposureMode(mode);
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> setExposureOffset(double offset) async {
+ if (controller == null) {
+ return;
+ }
+
+ setState(() {
+ _currentExposureOffset = offset;
+ });
+ try {
+ offset = await controller!.setExposureOffset(offset);
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> setFocusMode(FocusMode mode) async {
+ if (controller == null) {
+ return;
+ }
+
+ try {
+ await controller!.setFocusMode(mode);
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ rethrow;
+ }
+ }
+
+ Future<void> _startVideoPlayer() async {
+ if (videoFile == null) {
+ return;
+ }
+
+ final VideoPlayerController vController = kIsWeb
+ ? VideoPlayerController.network(videoFile!.path)
+ : VideoPlayerController.file(File(videoFile!.path));
+
+ videoPlayerListener = () {
+ if (videoController != null && videoController!.value.size != null) {
+ // Refreshing the state to update video player with the correct ratio.
+ if (mounted) {
+ setState(() {});
+ }
+ videoController!.removeListener(videoPlayerListener!);
+ }
+ };
+ vController.addListener(videoPlayerListener!);
+ await vController.setLooping(true);
+ await vController.initialize();
+ await videoController?.dispose();
+ if (mounted) {
+ setState(() {
+ imageFile = null;
+ videoController = vController;
+ });
+ }
+ await vController.play();
+ }
+
+ Future<XFile?> takePicture() async {
+ final CameraController? cameraController = controller;
+ if (cameraController == null || !cameraController.value.isInitialized) {
+ showInSnackBar('Error: select a camera first.');
+ return null;
+ }
+
+ if (cameraController.value.isTakingPicture) {
+ // A capture is already pending, do nothing.
+ return null;
+ }
+
+ try {
+ final XFile file = await cameraController.takePicture();
+ return file;
+ } on CameraException catch (e) {
+ _showCameraException(e);
+ return null;
+ }
+ }
+
+ void _showCameraException(CameraException e) {
+ _logError(e.code, e.description);
+ showInSnackBar('Error: ${e.code}\n${e.description}');
+ }
+}
+
+/// CameraApp is the Main Application.
+class CameraApp extends StatelessWidget {
+ /// Default Constructor
+ const CameraApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return const MaterialApp(
+ home: CameraExampleHome(),
+ );
+ }
+}
+
+List<CameraDescription> _cameras = <CameraDescription>[];
+
+Future<void> main() async {
+ // Fetch the available cameras before initializing the app.
+ try {
+ WidgetsFlutterBinding.ensureInitialized();
+ _cameras = await availableCameras();
+ } on CameraException catch (e) {
+ _logError(e.code, e.description);
+ }
+ runApp(const CameraApp());
+}
diff --git a/packages/camera/camera/example/lib/readme_full_example.dart b/packages/camera/camera/example/lib/readme_full_example.dart
new file mode 100644
index 0000000..20bfe78
--- /dev/null
+++ b/packages/camera/camera/example/lib/readme_full_example.dart
@@ -0,0 +1,69 @@
+// 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.
+
+// #docregion FullAppExample
+import 'package:camera/camera.dart';
+import 'package:flutter/material.dart';
+
+late List<CameraDescription> _cameras;
+
+Future<void> main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ _cameras = await availableCameras();
+ runApp(const CameraApp());
+}
+
+/// CameraApp is the Main Application.
+class CameraApp extends StatefulWidget {
+ /// Default Constructor
+ const CameraApp({Key? key}) : super(key: key);
+
+ @override
+ State<CameraApp> createState() => _CameraAppState();
+}
+
+class _CameraAppState extends State<CameraApp> {
+ late CameraController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = CameraController(_cameras[0], ResolutionPreset.max);
+ controller.initialize().then((_) {
+ if (!mounted) {
+ return;
+ }
+ setState(() {});
+ }).catchError((Object e) {
+ if (e is CameraException) {
+ switch (e.code) {
+ case 'CameraAccessDenied':
+ // Handle access errors here.
+ break;
+ default:
+ // Handle other errors here.
+ break;
+ }
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (!controller.value.isInitialized) {
+ return Container();
+ }
+ return MaterialApp(
+ home: CameraPreview(controller),
+ );
+ }
+}
+// #enddocregion FullAppExample
diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml
new file mode 100644
index 0000000..e630240
--- /dev/null
+++ b/packages/camera/camera/example/pubspec.yaml
@@ -0,0 +1,32 @@
+name: camera_example
+description: Demonstrates how to use the camera plugin.
+publish_to: none
+
+environment:
+ sdk: ">=2.14.0 <3.0.0"
+ flutter: ">=3.0.0"
+
+dependencies:
+ camera:
+ # When depending on this package from a real application you should use:
+ # camera: ^x.y.z
+ # See https://dart.dev/tools/pub/dependencies#version-constraints
+ # The example app is bundled with the plugin so we use a path dependency on
+ # the parent directory to use the current plugin's version.
+ path: ../
+ flutter:
+ sdk: flutter
+ path_provider: ^2.0.0
+ video_player: ^2.1.4
+
+dev_dependencies:
+ build_runner: ^2.1.10
+ flutter_driver:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+ integration_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/packages/camera/camera/example/test/main_test.dart b/packages/camera/camera/example/test/main_test.dart
new file mode 100644
index 0000000..6e909ef
--- /dev/null
+++ b/packages/camera/camera/example/test/main_test.dart
@@ -0,0 +1,16 @@
+// 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:camera_example/main.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Test snackbar', (WidgetTester tester) async {
+ WidgetsFlutterBinding.ensureInitialized();
+ await tester.pumpWidget(const CameraApp());
+ await tester.pumpAndSettle();
+ expect(find.byType(SnackBar), findsOneWidget);
+ });
+}
diff --git a/packages/camera/camera/example/test_driver/integration_test.dart b/packages/camera/camera/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..aa57599
--- /dev/null
+++ b/packages/camera/camera/example/test_driver/integration_test.dart
@@ -0,0 +1,66 @@
+// 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.
+
+// ignore_for_file: avoid_print
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_driver/flutter_driver.dart';
+
+const String _examplePackage = 'io.flutter.plugins.cameraexample';
+
+Future<void> main() async {
+ if (!(Platform.isLinux || Platform.isMacOS)) {
+ print('This test must be run on a POSIX host. Skipping...');
+ exit(0);
+ }
+ final bool adbExists =
+ Process.runSync('which', <String>['adb']).exitCode == 0;
+ if (!adbExists) {
+ print(r'This test needs ADB to exist on the $PATH. Skipping...');
+ exit(0);
+ }
+ print('Granting camera permissions...');
+ Process.runSync('adb', <String>[
+ 'shell',
+ 'pm',
+ 'grant',
+ _examplePackage,
+ 'android.permission.CAMERA'
+ ]);
+ Process.runSync('adb', <String>[
+ 'shell',
+ 'pm',
+ 'grant',
+ _examplePackage,
+ 'android.permission.RECORD_AUDIO'
+ ]);
+ print('Starting test.');
+ final FlutterDriver driver = await FlutterDriver.connect();
+ final String data = await driver.requestData(
+ null,
+ timeout: const Duration(minutes: 1),
+ );
+ await driver.close();
+ print('Test finished. Revoking camera permissions...');
+ Process.runSync('adb', <String>[
+ 'shell',
+ 'pm',
+ 'revoke',
+ _examplePackage,
+ 'android.permission.CAMERA'
+ ]);
+ Process.runSync('adb', <String>[
+ 'shell',
+ 'pm',
+ 'revoke',
+ _examplePackage,
+ 'android.permission.RECORD_AUDIO'
+ ]);
+
+ final Map<String, dynamic> result = jsonDecode(data) as Map<String, dynamic>;
+ exit(result['result'] == 'true' ? 0 : 1);
+}
diff --git a/packages/camera/camera/example/web/favicon.png b/packages/camera/camera/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/camera/camera/example/web/favicon.png
Binary files differ
diff --git a/packages/camera/camera/example/web/icons/Icon-192.png b/packages/camera/camera/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/camera/camera/example/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/camera/camera/example/web/icons/Icon-512.png b/packages/camera/camera/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/camera/camera/example/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/camera/camera/example/web/index.html b/packages/camera/camera/example/web/index.html
new file mode 100644
index 0000000..2a3117d
--- /dev/null
+++ b/packages/camera/camera/example/web/index.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+ <meta name="description" content="An example of the camera on the web.">
+
+ <!-- iOS meta tags & icons -->
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
+ <meta name="apple-mobile-web-app-title" content="example">
+ <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+ <!-- Favicon -->
+ <link rel="shortcut icon" type="image/png" href="favicon.png" />
+
+ <title>Camera Web Example</title>
+ <link rel="manifest" href="manifest.json">
+</head>
+
+<body>
+ <!-- This script installs service_worker.js to provide PWA functionality to
+ application. For more information, see:
+ https://developers.google.com/web/fundamentals/primers/service-workers -->
+ <script>
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', function () {
+ navigator.serviceWorker.register('flutter_service_worker.js');
+ });
+ }
+ </script>
+ <script src="main.dart.js" type="application/javascript"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/packages/camera/camera/example/web/manifest.json b/packages/camera/camera/example/web/manifest.json
new file mode 100644
index 0000000..5fe0e04
--- /dev/null
+++ b/packages/camera/camera/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "camera example",
+ "short_name": "camera",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "An example of the camera on the web.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart
new file mode 100644
index 0000000..900c263
--- /dev/null
+++ b/packages/camera/camera/lib/camera.dart
@@ -0,0 +1,19 @@
+// 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.
+
+export 'package:camera_platform_interface/camera_platform_interface.dart'
+ show
+ CameraDescription,
+ CameraException,
+ CameraLensDirection,
+ FlashMode,
+ ExposureMode,
+ FocusMode,
+ ResolutionPreset,
+ XFile,
+ ImageFormatGroup;
+
+export 'src/camera_controller.dart';
+export 'src/camera_image.dart';
+export 'src/camera_preview.dart';
diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart
new file mode 100644
index 0000000..7a396c1
--- /dev/null
+++ b/packages/camera/camera/lib/src/camera_controller.dart
@@ -0,0 +1,957 @@
+// 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 'dart:async';
+import 'dart:collection';
+import 'dart:math';
+
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import '../camera.dart';
+
+/// Signature for a callback receiving the a camera image.
+///
+/// This is used by [CameraController.startImageStream].
+// TODO(stuartmorgan): Fix this naming the next time there's a breaking change
+// to this package.
+// ignore: camel_case_types
+typedef onLatestImageAvailable = Function(CameraImage image);
+
+/// Completes with a list of available cameras.
+///
+/// May throw a [CameraException].
+Future<List<CameraDescription>> availableCameras() async {
+ return CameraPlatform.instance.availableCameras();
+}
+
+// TODO(stuartmorgan): Remove this once the package requires 2.10, where the
+// dart:async `unawaited` accepts a nullable future.
+void _unawaited(Future<void>? future) {}
+
+/// The state of a [CameraController].
+class CameraValue {
+ /// Creates a new camera controller state.
+ const CameraValue({
+ required this.isInitialized,
+ this.errorDescription,
+ this.previewSize,
+ required this.isRecordingVideo,
+ required this.isTakingPicture,
+ required this.isStreamingImages,
+ required bool isRecordingPaused,
+ required this.flashMode,
+ required this.exposureMode,
+ required this.focusMode,
+ required this.exposurePointSupported,
+ required this.focusPointSupported,
+ required this.deviceOrientation,
+ this.lockedCaptureOrientation,
+ this.recordingOrientation,
+ this.isPreviewPaused = false,
+ this.previewPauseOrientation,
+ }) : _isRecordingPaused = isRecordingPaused;
+
+ /// Creates a new camera controller state for an uninitialized controller.
+ const CameraValue.uninitialized()
+ : this(
+ isInitialized: false,
+ isRecordingVideo: false,
+ isTakingPicture: false,
+ isStreamingImages: false,
+ isRecordingPaused: false,
+ flashMode: FlashMode.auto,
+ exposureMode: ExposureMode.auto,
+ exposurePointSupported: false,
+ focusMode: FocusMode.auto,
+ focusPointSupported: false,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ isPreviewPaused: false,
+ );
+
+ /// True after [CameraController.initialize] has completed successfully.
+ final bool isInitialized;
+
+ /// True when a picture capture request has been sent but as not yet returned.
+ final bool isTakingPicture;
+
+ /// True when the camera is recording (not the same as previewing).
+ final bool isRecordingVideo;
+
+ /// True when images from the camera are being streamed.
+ final bool isStreamingImages;
+
+ final bool _isRecordingPaused;
+
+ /// True when the preview widget has been paused manually.
+ final bool isPreviewPaused;
+
+ /// Set to the orientation the preview was paused in, if it is currently paused.
+ final DeviceOrientation? previewPauseOrientation;
+
+ /// True when camera [isRecordingVideo] and recording is paused.
+ bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused;
+
+ /// Description of an error state.
+ ///
+ /// This is null while the controller is not in an error state.
+ /// When [hasError] is true this contains the error description.
+ final String? errorDescription;
+
+ /// The size of the preview in pixels.
+ ///
+ /// Is `null` until [isInitialized] is `true`.
+ final Size? previewSize;
+
+ /// Convenience getter for `previewSize.width / previewSize.height`.
+ ///
+ /// Can only be called when [initialize] is done.
+ double get aspectRatio => previewSize!.width / previewSize!.height;
+
+ /// Whether the controller is in an error state.
+ ///
+ /// When true [errorDescription] describes the error.
+ bool get hasError => errorDescription != null;
+
+ /// The flash mode the camera is currently set to.
+ final FlashMode flashMode;
+
+ /// The exposure mode the camera is currently set to.
+ final ExposureMode exposureMode;
+
+ /// The focus mode the camera is currently set to.
+ final FocusMode focusMode;
+
+ /// Whether setting the exposure point is supported.
+ final bool exposurePointSupported;
+
+ /// Whether setting the focus point is supported.
+ final bool focusPointSupported;
+
+ /// The current device UI orientation.
+ final DeviceOrientation deviceOrientation;
+
+ /// The currently locked capture orientation.
+ final DeviceOrientation? lockedCaptureOrientation;
+
+ /// Whether the capture orientation is currently locked.
+ bool get isCaptureOrientationLocked => lockedCaptureOrientation != null;
+
+ /// The orientation of the currently running video recording.
+ final DeviceOrientation? recordingOrientation;
+
+ /// Creates a modified copy of the object.
+ ///
+ /// Explicitly specified fields get the specified value, all other fields get
+ /// the same value of the current object.
+ CameraValue copyWith({
+ bool? isInitialized,
+ bool? isRecordingVideo,
+ bool? isTakingPicture,
+ bool? isStreamingImages,
+ String? errorDescription,
+ Size? previewSize,
+ bool? isRecordingPaused,
+ FlashMode? flashMode,
+ ExposureMode? exposureMode,
+ FocusMode? focusMode,
+ bool? exposurePointSupported,
+ bool? focusPointSupported,
+ DeviceOrientation? deviceOrientation,
+ Optional<DeviceOrientation>? lockedCaptureOrientation,
+ Optional<DeviceOrientation>? recordingOrientation,
+ bool? isPreviewPaused,
+ Optional<DeviceOrientation>? previewPauseOrientation,
+ }) {
+ return CameraValue(
+ isInitialized: isInitialized ?? this.isInitialized,
+ errorDescription: errorDescription,
+ previewSize: previewSize ?? this.previewSize,
+ isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo,
+ isTakingPicture: isTakingPicture ?? this.isTakingPicture,
+ isStreamingImages: isStreamingImages ?? this.isStreamingImages,
+ isRecordingPaused: isRecordingPaused ?? _isRecordingPaused,
+ flashMode: flashMode ?? this.flashMode,
+ exposureMode: exposureMode ?? this.exposureMode,
+ focusMode: focusMode ?? this.focusMode,
+ exposurePointSupported:
+ exposurePointSupported ?? this.exposurePointSupported,
+ focusPointSupported: focusPointSupported ?? this.focusPointSupported,
+ deviceOrientation: deviceOrientation ?? this.deviceOrientation,
+ lockedCaptureOrientation: lockedCaptureOrientation == null
+ ? this.lockedCaptureOrientation
+ : lockedCaptureOrientation.orNull,
+ recordingOrientation: recordingOrientation == null
+ ? this.recordingOrientation
+ : recordingOrientation.orNull,
+ isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
+ previewPauseOrientation: previewPauseOrientation == null
+ ? this.previewPauseOrientation
+ : previewPauseOrientation.orNull,
+ );
+ }
+
+ @override
+ String toString() {
+ return '${objectRuntimeType(this, 'CameraValue')}('
+ 'isRecordingVideo: $isRecordingVideo, '
+ 'isInitialized: $isInitialized, '
+ 'errorDescription: $errorDescription, '
+ 'previewSize: $previewSize, '
+ 'isStreamingImages: $isStreamingImages, '
+ 'flashMode: $flashMode, '
+ 'exposureMode: $exposureMode, '
+ 'focusMode: $focusMode, '
+ 'exposurePointSupported: $exposurePointSupported, '
+ 'focusPointSupported: $focusPointSupported, '
+ 'deviceOrientation: $deviceOrientation, '
+ 'lockedCaptureOrientation: $lockedCaptureOrientation, '
+ 'recordingOrientation: $recordingOrientation, '
+ 'isPreviewPaused: $isPreviewPaused, '
+ 'previewPausedOrientation: $previewPauseOrientation)';
+ }
+}
+
+/// Controls a device camera.
+///
+/// Use [availableCameras] to get a list of available cameras.
+///
+/// Before using a [CameraController] a call to [initialize] must complete.
+///
+/// To show the camera preview on the screen use a [CameraPreview] widget.
+class CameraController extends ValueNotifier<CameraValue> {
+ /// Creates a new camera controller in an uninitialized state.
+ CameraController(
+ this.description,
+ this.resolutionPreset, {
+ this.enableAudio = true,
+ this.imageFormatGroup,
+ }) : super(const CameraValue.uninitialized());
+
+ /// The properties of the camera device controlled by this controller.
+ final CameraDescription description;
+
+ /// The resolution this controller is targeting.
+ ///
+ /// This resolution preset is not guaranteed to be available on the device,
+ /// if unavailable a lower resolution will be used.
+ ///
+ /// See also: [ResolutionPreset].
+ final ResolutionPreset resolutionPreset;
+
+ /// Whether to include audio when recording a video.
+ final bool enableAudio;
+
+ /// The [ImageFormatGroup] describes the output of the raw image format.
+ ///
+ /// When null the imageFormat will fallback to the platforms default.
+ final ImageFormatGroup? imageFormatGroup;
+
+ /// The id of a camera that hasn't been initialized.
+ @visibleForTesting
+ static const int kUninitializedCameraId = -1;
+ int _cameraId = kUninitializedCameraId;
+
+ bool _isDisposed = false;
+ StreamSubscription<CameraImageData>? _imageStreamSubscription;
+ FutureOr<bool>? _initCalled;
+ StreamSubscription<DeviceOrientationChangedEvent>?
+ _deviceOrientationSubscription;
+
+ /// Checks whether [CameraController.dispose] has completed successfully.
+ ///
+ /// This is a no-op when asserts are disabled.
+ void debugCheckIsDisposed() {
+ assert(_isDisposed);
+ }
+
+ /// The camera identifier with which the controller is associated.
+ int get cameraId => _cameraId;
+
+ /// Initializes the camera on the device.
+ ///
+ /// Throws a [CameraException] if the initialization fails.
+ Future<void> initialize() async {
+ if (_isDisposed) {
+ throw CameraException(
+ 'Disposed CameraController',
+ 'initialize was called on a disposed CameraController',
+ );
+ }
+ try {
+ final Completer<CameraInitializedEvent> initializeCompleter =
+ Completer<CameraInitializedEvent>();
+
+ _deviceOrientationSubscription = CameraPlatform.instance
+ .onDeviceOrientationChanged()
+ .listen((DeviceOrientationChangedEvent event) {
+ value = value.copyWith(
+ deviceOrientation: event.orientation,
+ );
+ });
+
+ _cameraId = await CameraPlatform.instance.createCamera(
+ description,
+ resolutionPreset,
+ enableAudio: enableAudio,
+ );
+
+ _unawaited(CameraPlatform.instance
+ .onCameraInitialized(_cameraId)
+ .first
+ .then((CameraInitializedEvent event) {
+ initializeCompleter.complete(event);
+ }));
+
+ await CameraPlatform.instance.initializeCamera(
+ _cameraId,
+ imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown,
+ );
+
+ value = value.copyWith(
+ isInitialized: true,
+ previewSize: await initializeCompleter.future
+ .then((CameraInitializedEvent event) => Size(
+ event.previewWidth,
+ event.previewHeight,
+ )),
+ exposureMode: await initializeCompleter.future
+ .then((CameraInitializedEvent event) => event.exposureMode),
+ focusMode: await initializeCompleter.future
+ .then((CameraInitializedEvent event) => event.focusMode),
+ exposurePointSupported: await initializeCompleter.future.then(
+ (CameraInitializedEvent event) => event.exposurePointSupported),
+ focusPointSupported: await initializeCompleter.future
+ .then((CameraInitializedEvent event) => event.focusPointSupported),
+ );
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+
+ _initCalled = true;
+ }
+
+ /// Prepare the capture session for video recording.
+ ///
+ /// Use of this method is optional, but it may be called for performance
+ /// reasons on iOS.
+ ///
+ /// Preparing audio can cause a minor delay in the CameraPreview view on iOS.
+ /// If video recording is intended, calling this early eliminates this delay
+ /// that would otherwise be experienced when video recording is started.
+ /// This operation is a no-op on Android and Web.
+ ///
+ /// Throws a [CameraException] if the prepare fails.
+ Future<void> prepareForVideoRecording() async {
+ await CameraPlatform.instance.prepareForVideoRecording();
+ }
+
+ /// Pauses the current camera preview
+ Future<void> pausePreview() async {
+ if (value.isPreviewPaused) {
+ return;
+ }
+ try {
+ await CameraPlatform.instance.pausePreview(_cameraId);
+ value = value.copyWith(
+ isPreviewPaused: true,
+ previewPauseOrientation: Optional<DeviceOrientation>.of(
+ value.lockedCaptureOrientation ?? value.deviceOrientation));
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Resumes the current camera preview
+ Future<void> resumePreview() async {
+ if (!value.isPreviewPaused) {
+ return;
+ }
+ try {
+ await CameraPlatform.instance.resumePreview(_cameraId);
+ value = value.copyWith(
+ isPreviewPaused: false,
+ previewPauseOrientation: const Optional<DeviceOrientation>.absent());
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Captures an image and returns the file where it was saved.
+ ///
+ /// Throws a [CameraException] if the capture fails.
+ Future<XFile> takePicture() async {
+ _throwIfNotInitialized('takePicture');
+ if (value.isTakingPicture) {
+ throw CameraException(
+ 'Previous capture has not returned yet.',
+ 'takePicture was called before the previous capture returned.',
+ );
+ }
+ try {
+ value = value.copyWith(isTakingPicture: true);
+ final XFile file = await CameraPlatform.instance.takePicture(_cameraId);
+ value = value.copyWith(isTakingPicture: false);
+ return file;
+ } on PlatformException catch (e) {
+ value = value.copyWith(isTakingPicture: false);
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Start streaming images from platform camera.
+ ///
+ /// Settings for capturing images on iOS and Android is set to always use the
+ /// latest image available from the camera and will drop all other images.
+ ///
+ /// When running continuously with [CameraPreview] widget, this function runs
+ /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can
+ /// have significant frame rate drops for [CameraPreview] on lower end
+ /// devices.
+ ///
+ /// Throws a [CameraException] if image streaming or video recording has
+ /// already started.
+ ///
+ /// The `startImageStream` method is only available on Android and iOS (other
+ /// platforms won't be supported in current setup).
+ ///
+ // TODO(bmparr): Add settings for resolution and fps.
+ Future<void> startImageStream(onLatestImageAvailable onAvailable) async {
+ assert(defaultTargetPlatform == TargetPlatform.android ||
+ defaultTargetPlatform == TargetPlatform.iOS);
+ _throwIfNotInitialized('startImageStream');
+ if (value.isRecordingVideo) {
+ throw CameraException(
+ 'A video recording is already started.',
+ 'startImageStream was called while a video is being recorded.',
+ );
+ }
+ if (value.isStreamingImages) {
+ throw CameraException(
+ 'A camera has started streaming images.',
+ 'startImageStream was called while a camera was streaming images.',
+ );
+ }
+
+ try {
+ _imageStreamSubscription = CameraPlatform.instance
+ .onStreamedFrameAvailable(_cameraId)
+ .listen((CameraImageData imageData) {
+ onAvailable(CameraImage.fromPlatformInterface(imageData));
+ });
+ value = value.copyWith(isStreamingImages: true);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Stop streaming images from platform camera.
+ ///
+ /// Throws a [CameraException] if image streaming was not started or video
+ /// recording was started.
+ ///
+ /// The `stopImageStream` method is only available on Android and iOS (other
+ /// platforms won't be supported in current setup).
+ Future<void> stopImageStream() async {
+ assert(defaultTargetPlatform == TargetPlatform.android ||
+ defaultTargetPlatform == TargetPlatform.iOS);
+ _throwIfNotInitialized('stopImageStream');
+ if (!value.isStreamingImages) {
+ throw CameraException(
+ 'No camera is streaming images',
+ 'stopImageStream was called when no camera is streaming images.',
+ );
+ }
+
+ try {
+ value = value.copyWith(isStreamingImages: false);
+ await _imageStreamSubscription?.cancel();
+ _imageStreamSubscription = null;
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Start a video recording.
+ ///
+ /// You may optionally pass an [onAvailable] callback to also have the
+ /// video frames streamed to this callback.
+ ///
+ /// The video is returned as a [XFile] after calling [stopVideoRecording].
+ /// Throws a [CameraException] if the capture fails.
+ Future<void> startVideoRecording(
+ {onLatestImageAvailable? onAvailable}) async {
+ _throwIfNotInitialized('startVideoRecording');
+ if (value.isRecordingVideo) {
+ throw CameraException(
+ 'A video recording is already started.',
+ 'startVideoRecording was called when a recording is already started.',
+ );
+ }
+
+ Function(CameraImageData image)? streamCallback;
+ if (onAvailable != null) {
+ streamCallback = (CameraImageData imageData) {
+ onAvailable(CameraImage.fromPlatformInterface(imageData));
+ };
+ }
+
+ try {
+ await CameraPlatform.instance.startVideoCapturing(
+ VideoCaptureOptions(_cameraId, streamCallback: streamCallback));
+ value = value.copyWith(
+ isRecordingVideo: true,
+ isRecordingPaused: false,
+ recordingOrientation: Optional<DeviceOrientation>.of(
+ value.lockedCaptureOrientation ?? value.deviceOrientation),
+ isStreamingImages: onAvailable != null);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Stops the video recording and returns the file where it was saved.
+ ///
+ /// Throws a [CameraException] if the capture failed.
+ Future<XFile> stopVideoRecording() async {
+ _throwIfNotInitialized('stopVideoRecording');
+ if (!value.isRecordingVideo) {
+ throw CameraException(
+ 'No video is recording',
+ 'stopVideoRecording was called when no video is recording.',
+ );
+ }
+
+ if (value.isStreamingImages) {
+ stopImageStream();
+ }
+
+ try {
+ final XFile file =
+ await CameraPlatform.instance.stopVideoRecording(_cameraId);
+ value = value.copyWith(
+ isRecordingVideo: false,
+ recordingOrientation: const Optional<DeviceOrientation>.absent(),
+ );
+ return file;
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Pause video recording.
+ ///
+ /// This feature is only available on iOS and Android sdk 24+.
+ Future<void> pauseVideoRecording() async {
+ _throwIfNotInitialized('pauseVideoRecording');
+ if (!value.isRecordingVideo) {
+ throw CameraException(
+ 'No video is recording',
+ 'pauseVideoRecording was called when no video is recording.',
+ );
+ }
+ try {
+ await CameraPlatform.instance.pauseVideoRecording(_cameraId);
+ value = value.copyWith(isRecordingPaused: true);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Resume video recording after pausing.
+ ///
+ /// This feature is only available on iOS and Android sdk 24+.
+ Future<void> resumeVideoRecording() async {
+ _throwIfNotInitialized('resumeVideoRecording');
+ if (!value.isRecordingVideo) {
+ throw CameraException(
+ 'No video is recording',
+ 'resumeVideoRecording was called when no video is recording.',
+ );
+ }
+ try {
+ await CameraPlatform.instance.resumeVideoRecording(_cameraId);
+ value = value.copyWith(isRecordingPaused: false);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Returns a widget showing a live camera preview.
+ Widget buildPreview() {
+ _throwIfNotInitialized('buildPreview');
+ try {
+ return CameraPlatform.instance.buildPreview(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Gets the maximum supported zoom level for the selected camera.
+ Future<double> getMaxZoomLevel() {
+ _throwIfNotInitialized('getMaxZoomLevel');
+ try {
+ return CameraPlatform.instance.getMaxZoomLevel(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Gets the minimum supported zoom level for the selected camera.
+ Future<double> getMinZoomLevel() {
+ _throwIfNotInitialized('getMinZoomLevel');
+ try {
+ return CameraPlatform.instance.getMinZoomLevel(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Set the zoom level for the selected camera.
+ ///
+ /// The supplied [zoom] value should be between 1.0 and the maximum supported
+ /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException`
+ /// when an illegal zoom level is suplied.
+ Future<void> setZoomLevel(double zoom) {
+ _throwIfNotInitialized('setZoomLevel');
+ try {
+ return CameraPlatform.instance.setZoomLevel(_cameraId, zoom);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Sets the flash mode for taking pictures.
+ Future<void> setFlashMode(FlashMode mode) async {
+ try {
+ await CameraPlatform.instance.setFlashMode(_cameraId, mode);
+ value = value.copyWith(flashMode: mode);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Sets the exposure mode for taking pictures.
+ Future<void> setExposureMode(ExposureMode mode) async {
+ try {
+ await CameraPlatform.instance.setExposureMode(_cameraId, mode);
+ value = value.copyWith(exposureMode: mode);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Sets the exposure point for automatically determining the exposure value.
+ ///
+ /// Supplying a `null` value will reset the exposure point to it's default
+ /// value.
+ Future<void> setExposurePoint(Offset? point) async {
+ if (point != null &&
+ (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) {
+ throw ArgumentError(
+ 'The values of point should be anywhere between (0,0) and (1,1).');
+ }
+
+ try {
+ await CameraPlatform.instance.setExposurePoint(
+ _cameraId,
+ point == null
+ ? null
+ : Point<double>(
+ point.dx,
+ point.dy,
+ ),
+ );
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Gets the minimum supported exposure offset for the selected camera in EV units.
+ Future<double> getMinExposureOffset() async {
+ _throwIfNotInitialized('getMinExposureOffset');
+ try {
+ return CameraPlatform.instance.getMinExposureOffset(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Gets the maximum supported exposure offset for the selected camera in EV units.
+ Future<double> getMaxExposureOffset() async {
+ _throwIfNotInitialized('getMaxExposureOffset');
+ try {
+ return CameraPlatform.instance.getMaxExposureOffset(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Gets the supported step size for exposure offset for the selected camera in EV units.
+ ///
+ /// Returns 0 when the camera supports using a free value without stepping.
+ Future<double> getExposureOffsetStepSize() async {
+ _throwIfNotInitialized('getExposureOffsetStepSize');
+ try {
+ return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// 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 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.
+ Future<double> setExposureOffset(double offset) async {
+ _throwIfNotInitialized('setExposureOffset');
+ // Check if offset is in range
+ final List<double> range = await Future.wait(
+ <Future<double>>[getMinExposureOffset(), getMaxExposureOffset()]);
+ if (offset < range[0] || offset > range[1]) {
+ throw CameraException(
+ 'exposureOffsetOutOfBounds',
+ 'The provided exposure offset was outside the supported range for this device.',
+ );
+ }
+
+ // Round to the closest step if needed
+ final double stepSize = await getExposureOffsetStepSize();
+ if (stepSize > 0) {
+ final double inv = 1.0 / stepSize;
+ double roundedOffset = (offset * inv).roundToDouble() / inv;
+ if (roundedOffset > range[1]) {
+ roundedOffset = (offset * inv).floorToDouble() / inv;
+ } else if (roundedOffset < range[0]) {
+ roundedOffset = (offset * inv).ceilToDouble() / inv;
+ }
+ offset = roundedOffset;
+ }
+
+ try {
+ return CameraPlatform.instance.setExposureOffset(_cameraId, offset);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Locks the capture orientation.
+ ///
+ /// If [orientation] is omitted, the current device orientation is used.
+ Future<void> lockCaptureOrientation([DeviceOrientation? orientation]) async {
+ try {
+ await CameraPlatform.instance.lockCaptureOrientation(
+ _cameraId, orientation ?? value.deviceOrientation);
+ value = value.copyWith(
+ lockedCaptureOrientation: Optional<DeviceOrientation>.of(
+ orientation ?? value.deviceOrientation));
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Sets the focus mode for taking pictures.
+ Future<void> setFocusMode(FocusMode mode) async {
+ try {
+ await CameraPlatform.instance.setFocusMode(_cameraId, mode);
+ value = value.copyWith(focusMode: mode);
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Unlocks the capture orientation.
+ Future<void> unlockCaptureOrientation() async {
+ try {
+ await CameraPlatform.instance.unlockCaptureOrientation(_cameraId);
+ value = value.copyWith(
+ lockedCaptureOrientation: const Optional<DeviceOrientation>.absent());
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Sets the focus point for automatically determining the focus value.
+ ///
+ /// Supplying a `null` value will reset the focus point to it's default
+ /// value.
+ Future<void> setFocusPoint(Offset? point) async {
+ if (point != null &&
+ (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) {
+ throw ArgumentError(
+ 'The values of point should be anywhere between (0,0) and (1,1).');
+ }
+ try {
+ await CameraPlatform.instance.setFocusPoint(
+ _cameraId,
+ point == null
+ ? null
+ : Point<double>(
+ point.dx,
+ point.dy,
+ ),
+ );
+ } on PlatformException catch (e) {
+ throw CameraException(e.code, e.message);
+ }
+ }
+
+ /// Releases the resources of this camera.
+ @override
+ Future<void> dispose() async {
+ if (_isDisposed) {
+ return;
+ }
+ _unawaited(_deviceOrientationSubscription?.cancel());
+ _isDisposed = true;
+ super.dispose();
+ if (_initCalled != null) {
+ await _initCalled;
+ await CameraPlatform.instance.dispose(_cameraId);
+ }
+ }
+
+ void _throwIfNotInitialized(String functionName) {
+ if (!value.isInitialized) {
+ throw CameraException(
+ 'Uninitialized CameraController',
+ '$functionName() was called on an uninitialized CameraController.',
+ );
+ }
+ if (_isDisposed) {
+ throw CameraException(
+ 'Disposed CameraController',
+ '$functionName() was called on a disposed CameraController.',
+ );
+ }
+ }
+
+ @override
+ void removeListener(VoidCallback listener) {
+ // Prevent ValueListenableBuilder in CameraPreview widget from causing an
+ // exception to be thrown by attempting to remove its own listener after
+ // the controller has already been disposed.
+ if (!_isDisposed) {
+ super.removeListener(listener);
+ }
+ }
+}
+
+/// A value that might be absent.
+///
+/// Used to represent [DeviceOrientation]s that are optional but also able
+/// to be cleared.
+@immutable
+class Optional<T> extends IterableBase<T> {
+ /// Constructs an empty Optional.
+ const Optional.absent() : _value = null;
+
+ /// Constructs an Optional of the given [value].
+ ///
+ /// Throws [ArgumentError] if [value] is null.
+ Optional.of(T value) : _value = value {
+ // TODO(cbracken): Delete and make this ctor const once mixed-mode
+ // execution is no longer around.
+ ArgumentError.checkNotNull(value);
+ }
+
+ /// Constructs an Optional of the given [value].
+ ///
+ /// If [value] is null, returns [absent()].
+ const Optional.fromNullable(T? value) : _value = value;
+
+ final T? _value;
+
+ /// True when this optional contains a value.
+ bool get isPresent => _value != null;
+
+ /// True when this optional contains no value.
+ bool get isNotPresent => _value == null;
+
+ /// Gets the Optional value.
+ ///
+ /// Throws [StateError] if [value] is null.
+ T get value {
+ if (_value == null) {
+ throw StateError('value called on absent Optional.');
+ }
+ return _value!;
+ }
+
+ /// Executes a function if the Optional value is present.
+ void ifPresent(void Function(T value) ifPresent) {
+ if (isPresent) {
+ ifPresent(_value as T);
+ }
+ }
+
+ /// Execution a function if the Optional value is absent.
+ void ifAbsent(void Function() ifAbsent) {
+ if (!isPresent) {
+ ifAbsent();
+ }
+ }
+
+ /// Gets the Optional value with a default.
+ ///
+ /// The default is returned if the Optional is [absent()].
+ ///
+ /// Throws [ArgumentError] if [defaultValue] is null.
+ T or(T defaultValue) {
+ return _value ?? defaultValue;
+ }
+
+ /// Gets the Optional value, or `null` if there is none.
+ T? get orNull => _value;
+
+ /// Transforms the Optional value.
+ ///
+ /// If the Optional is [absent()], returns [absent()] without applying the transformer.
+ ///
+ /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown.
+ Optional<S> transform<S>(S Function(T value) transformer) {
+ return _value == null
+ ? Optional<S>.absent()
+ : Optional<S>.of(transformer(_value as T));
+ }
+
+ /// Transforms the Optional value.
+ ///
+ /// If the Optional is [absent()], returns [absent()] without applying the transformer.
+ ///
+ /// Returns [absent()] if the transformer returns `null`.
+ Optional<S> transformNullable<S>(S? Function(T value) transformer) {
+ return _value == null
+ ? Optional<S>.absent()
+ : Optional<S>.fromNullable(transformer(_value as T));
+ }
+
+ @override
+ Iterator<T> get iterator =>
+ isPresent ? <T>[_value as T].iterator : Iterable<T>.empty().iterator;
+
+ /// Delegates to the underlying [value] hashCode.
+ @override
+ int get hashCode => _value.hashCode;
+
+ /// Delegates to the underlying [value] operator==.
+ @override
+ bool operator ==(Object o) => o is Optional<T> && o._value == _value;
+
+ @override
+ String toString() {
+ return _value == null
+ ? 'Optional { absent }'
+ : 'Optional { value: $_value }';
+ }
+}
diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart
new file mode 100644
index 0000000..bfcad66
--- /dev/null
+++ b/packages/camera/camera/lib/src/camera_image.dart
@@ -0,0 +1,177 @@
+// 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.
+
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
+// ignore: unnecessary_import
+import 'dart:typed_data';
+
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+
+// TODO(stuartmorgan): Remove all of these classes in a breaking change, and
+// vend the platform interface versions directly. See
+// https://github.com/flutter/flutter/issues/104188
+
+/// A single color plane of image data.
+///
+/// The number and meaning of the planes in an image are determined by the
+/// format of the Image.
+class Plane {
+ Plane._fromPlatformInterface(CameraImagePlane plane)
+ : bytes = plane.bytes,
+ bytesPerPixel = plane.bytesPerPixel,
+ bytesPerRow = plane.bytesPerRow,
+ height = plane.height,
+ width = plane.width;
+
+ // Only used by the deprecated codepath that's kept to avoid breaking changes.
+ // Never called by the plugin itself.
+ Plane._fromPlatformData(Map<dynamic, dynamic> data)
+ : bytes = data['bytes'] as Uint8List,
+ bytesPerPixel = data['bytesPerPixel'] as int?,
+ bytesPerRow = data['bytesPerRow'] as int,
+ height = data['height'] as int?,
+ width = data['width'] as int?;
+
+ /// Bytes representing this plane.
+ final Uint8List bytes;
+
+ /// The distance between adjacent pixel samples on Android, in bytes.
+ ///
+ /// Will be `null` on iOS.
+ final int? bytesPerPixel;
+
+ /// The row stride for this color plane, in bytes.
+ final int bytesPerRow;
+
+ /// Height of the pixel buffer on iOS.
+ ///
+ /// Will be `null` on Android
+ final int? height;
+
+ /// Width of the pixel buffer on iOS.
+ ///
+ /// Will be `null` on Android.
+ final int? width;
+}
+
+/// Describes how pixels are represented in an image.
+class ImageFormat {
+ ImageFormat._fromPlatformInterface(CameraImageFormat format)
+ : group = format.group,
+ raw = format.raw;
+
+ // Only used by the deprecated codepath that's kept to avoid breaking changes.
+ // Never called by the plugin itself.
+ ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw);
+
+ /// Describes the format group the raw image format falls into.
+ final ImageFormatGroup group;
+
+ /// Raw version of the format from the Android or iOS platform.
+ ///
+ /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See
+ /// https://developer.android.com/reference/android/graphics/ImageFormat
+ ///
+ /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers.
+ /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc
+ final dynamic raw;
+}
+
+// Only used by the deprecated codepath that's kept to avoid breaking changes.
+// Never called by the plugin itself.
+ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) {
+ if (defaultTargetPlatform == TargetPlatform.android) {
+ switch (rawFormat) {
+ // android.graphics.ImageFormat.YUV_420_888
+ case 35:
+ return ImageFormatGroup.yuv420;
+ // android.graphics.ImageFormat.JPEG
+ case 256:
+ return ImageFormatGroup.jpeg;
+ }
+ }
+
+ if (defaultTargetPlatform == TargetPlatform.iOS) {
+ switch (rawFormat) {
+ // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
+ case 875704438:
+ return ImageFormatGroup.yuv420;
+ // kCVPixelFormatType_32BGRA
+ case 1111970369:
+ return ImageFormatGroup.bgra8888;
+ }
+ }
+
+ return ImageFormatGroup.unknown;
+}
+
+/// A single complete image buffer from the platform camera.
+///
+/// This class allows for direct application access to the pixel data of an
+/// Image through one or more [Uint8List]. Each buffer is encapsulated in a
+/// [Plane] that describes the layout of the pixel data in that plane. The
+/// [CameraImage] is not directly usable as a UI resource.
+///
+/// Although not all image formats are planar on iOS, we treat 1-dimensional
+/// images as single planar images.
+class CameraImage {
+ /// Creates a [CameraImage] from the platform interface version.
+ CameraImage.fromPlatformInterface(CameraImageData data)
+ : format = ImageFormat._fromPlatformInterface(data.format),
+ height = data.height,
+ width = data.width,
+ planes = List<Plane>.unmodifiable(data.planes.map<Plane>(
+ (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))),
+ lensAperture = data.lensAperture,
+ sensorExposureTime = data.sensorExposureTime,
+ sensorSensitivity = data.sensorSensitivity;
+
+ /// Creates a [CameraImage] from method channel data.
+ @Deprecated('Use fromPlatformInterface instead')
+ CameraImage.fromPlatformData(Map<dynamic, dynamic> data)
+ : format = ImageFormat._fromPlatformData(data['format']),
+ height = data['height'] as int,
+ width = data['width'] as int,
+ lensAperture = data['lensAperture'] as double?,
+ sensorExposureTime = data['sensorExposureTime'] as int?,
+ sensorSensitivity = data['sensorSensitivity'] as double?,
+ planes = List<Plane>.unmodifiable((data['planes'] as List<dynamic>)
+ .map<Plane>((dynamic planeData) =>
+ Plane._fromPlatformData(planeData as Map<dynamic, dynamic>)));
+
+ /// Format of the image provided.
+ ///
+ /// Determines the number of planes needed to represent the image, and
+ /// the general layout of the pixel data in each [Uint8List].
+ final ImageFormat format;
+
+ /// Height of the image in pixels.
+ ///
+ /// For formats where some color channels are subsampled, this is the height
+ /// of the largest-resolution plane.
+ final int height;
+
+ /// Width of the image in pixels.
+ ///
+ /// For formats where some color channels are subsampled, this is the width
+ /// of the largest-resolution plane.
+ final int width;
+
+ /// The pixels planes for this image.
+ ///
+ /// The number of planes is determined by the format of the image.
+ final List<Plane> planes;
+
+ /// The aperture settings for this image.
+ ///
+ /// Represented as an f-stop value.
+ final double? lensAperture;
+
+ /// The sensor exposure time for this image in nanoseconds.
+ final int? sensorExposureTime;
+
+ /// The sensor sensitivity in standard ISO arithmetic units.
+ final double? sensorSensitivity;
+}
diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart
new file mode 100644
index 0000000..d8eadd8
--- /dev/null
+++ b/packages/camera/camera/lib/src/camera_preview.dart
@@ -0,0 +1,82 @@
+// 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/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import '../camera.dart';
+
+/// A widget showing a live camera preview.
+class CameraPreview extends StatelessWidget {
+ /// Creates a preview widget for the given camera controller.
+ const CameraPreview(this.controller, {Key? key, this.child})
+ : super(key: key);
+
+ /// The controller for the camera that the preview is shown for.
+ final CameraController controller;
+
+ /// A widget to overlay on top of the camera preview
+ final Widget? child;
+
+ @override
+ Widget build(BuildContext context) {
+ return controller.value.isInitialized
+ ? ValueListenableBuilder<CameraValue>(
+ valueListenable: controller,
+ builder: (BuildContext context, Object? value, Widget? child) {
+ return AspectRatio(
+ aspectRatio: _isLandscape()
+ ? controller.value.aspectRatio
+ : (1 / controller.value.aspectRatio),
+ child: Stack(
+ fit: StackFit.expand,
+ children: <Widget>[
+ _wrapInRotatedBox(child: controller.buildPreview()),
+ child ?? Container(),
+ ],
+ ),
+ );
+ },
+ child: child,
+ )
+ : Container();
+ }
+
+ Widget _wrapInRotatedBox({required Widget child}) {
+ if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
+ return child;
+ }
+
+ return RotatedBox(
+ quarterTurns: _getQuarterTurns(),
+ child: child,
+ );
+ }
+
+ bool _isLandscape() {
+ return <DeviceOrientation>[
+ DeviceOrientation.landscapeLeft,
+ DeviceOrientation.landscapeRight
+ ].contains(_getApplicableOrientation());
+ }
+
+ int _getQuarterTurns() {
+ final Map<DeviceOrientation, int> turns = <DeviceOrientation, int>{
+ DeviceOrientation.portraitUp: 0,
+ DeviceOrientation.landscapeRight: 1,
+ DeviceOrientation.portraitDown: 2,
+ DeviceOrientation.landscapeLeft: 3,
+ };
+ return turns[_getApplicableOrientation()]!;
+ }
+
+ DeviceOrientation _getApplicableOrientation() {
+ return controller.value.isRecordingVideo
+ ? controller.value.recordingOrientation!
+ : (controller.value.previewPauseOrientation ??
+ controller.value.lockedCaptureOrientation ??
+ controller.value.deviceOrientation);
+ }
+}
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
new file mode 100644
index 0000000..1b902ab
--- /dev/null
+++ b/packages/camera/camera/pubspec.yaml
@@ -0,0 +1,40 @@
+name: camera
+description: A Flutter plugin for controlling the camera. Supports previewing
+ the camera feed, capturing images and video, and streaming image buffers to
+ Dart.
+repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
+version: 0.10.3
+
+environment:
+ sdk: ">=2.14.0 <3.0.0"
+ flutter: ">=3.0.0"
+
+flutter:
+ plugin:
+ platforms:
+ android:
+ default_package: camera_android
+ ios:
+ default_package: camera_avfoundation
+ web:
+ default_package: camera_web
+
+dependencies:
+ camera_android: ^0.10.1
+ camera_avfoundation: ^0.9.9
+ camera_platform_interface: ^2.3.2
+ camera_web: ^0.3.1
+ flutter:
+ sdk: flutter
+ flutter_plugin_android_lifecycle: ^2.0.2
+ quiver: ^3.0.0
+
+dev_dependencies:
+ flutter_driver:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+ mockito: ^5.0.0
+ plugin_platform_interface: ^2.0.0
+ video_player: ^2.0.0
diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart
new file mode 100644
index 0000000..29b5cce
--- /dev/null
+++ b/packages/camera/camera/test/camera_image_stream_test.dart
@@ -0,0 +1,243 @@
+// 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 'dart:async';
+
+import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'camera_test.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ late MockStreamingCameraPlatform mockPlatform;
+
+ setUp(() {
+ mockPlatform = MockStreamingCameraPlatform();
+ CameraPlatform.instance = mockPlatform;
+ });
+
+ test('startImageStream() throws $CameraException when uninitialized', () {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ () => cameraController.startImageStream((CameraImage image) => null),
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'startImageStream() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('startImageStream() throws $CameraException when recording videos',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+
+ cameraController.value =
+ cameraController.value.copyWith(isRecordingVideo: true);
+
+ expect(
+ () => cameraController.startImageStream((CameraImage image) => null),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'A video recording is already started.',
+ 'startImageStream was called while a video is being recorded.',
+ )));
+ });
+ test(
+ 'startImageStream() throws $CameraException when already streaming images',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ cameraController.value =
+ cameraController.value.copyWith(isStreamingImages: true);
+ expect(
+ () => cameraController.startImageStream((CameraImage image) => null),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'A camera has started streaming images.',
+ 'startImageStream was called while a camera was streaming images.',
+ )));
+ });
+
+ test('startImageStream() calls CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.startImageStream((CameraImage image) => null);
+
+ expect(mockPlatform.streamCallLog,
+ <String>['onStreamedFrameAvailable', 'listen']);
+ });
+
+ test('stopImageStream() throws $CameraException when uninitialized', () {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ cameraController.stopImageStream,
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'stopImageStream() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('stopImageStream() throws $CameraException when not streaming images',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ expect(
+ cameraController.stopImageStream,
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'No camera is streaming images',
+ 'stopImageStream was called when no camera is streaming images.',
+ )));
+ });
+
+ test('stopImageStream() intended behaviour', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ await cameraController.startImageStream((CameraImage image) => null);
+ await cameraController.stopImageStream();
+
+ expect(mockPlatform.streamCallLog,
+ <String>['onStreamedFrameAvailable', 'listen', 'cancel']);
+ });
+
+ test('startVideoRecording() can stream images', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+
+ cameraController.startVideoRecording(
+ onAvailable: (CameraImage image) => null);
+
+ expect(
+ mockPlatform.streamCallLog.contains('startVideoCapturing with stream'),
+ isTrue);
+ });
+
+ test('startVideoRecording() by default does not stream', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+
+ cameraController.startVideoRecording();
+
+ expect(mockPlatform.streamCallLog.contains('startVideoCapturing'), isTrue);
+ });
+}
+
+class MockStreamingCameraPlatform extends MockCameraPlatform {
+ List<String> streamCallLog = <String>[];
+
+ StreamController<CameraImageData>? _streamController;
+
+ @override
+ Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
+ {CameraImageStreamOptions? options}) {
+ streamCallLog.add('onStreamedFrameAvailable');
+ _streamController = StreamController<CameraImageData>(
+ onListen: _onFrameStreamListen,
+ onCancel: _onFrameStreamCancel,
+ );
+ return _streamController!.stream;
+ }
+
+ @override
+ Future<XFile> startVideoRecording(int cameraId,
+ {Duration? maxVideoDuration}) {
+ streamCallLog.add('startVideoRecording');
+ return super
+ .startVideoRecording(cameraId, maxVideoDuration: maxVideoDuration);
+ }
+
+ @override
+ Future<void> startVideoCapturing(VideoCaptureOptions options) {
+ if (options.streamCallback == null) {
+ streamCallLog.add('startVideoCapturing');
+ } else {
+ streamCallLog.add('startVideoCapturing with stream');
+ }
+ return super.startVideoCapturing(options);
+ }
+
+ void _onFrameStreamListen() {
+ streamCallLog.add('listen');
+ }
+
+ FutureOr<void> _onFrameStreamCancel() async {
+ streamCallLog.add('cancel');
+ _streamController = null;
+ }
+}
diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart
new file mode 100644
index 0000000..ecf4b50
--- /dev/null
+++ b/packages/camera/camera/test/camera_image_test.dart
@@ -0,0 +1,187 @@
+// 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.
+
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
+// ignore: unnecessary_import
+import 'dart:typed_data';
+
+import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ test('translates correctly from platform interface classes', () {
+ final CameraImageData originalImage = CameraImageData(
+ format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 1234),
+ planes: <CameraImagePlane>[
+ CameraImagePlane(
+ bytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
+ bytesPerRow: 20,
+ bytesPerPixel: 3,
+ width: 200,
+ height: 100,
+ ),
+ CameraImagePlane(
+ bytes: Uint8List.fromList(<int>[5, 6, 7, 8]),
+ bytesPerRow: 18,
+ bytesPerPixel: 4,
+ width: 220,
+ height: 110,
+ ),
+ ],
+ width: 640,
+ height: 480,
+ lensAperture: 2.5,
+ sensorExposureTime: 5,
+ sensorSensitivity: 1.3,
+ );
+
+ final CameraImage image = CameraImage.fromPlatformInterface(originalImage);
+ // Simple values.
+ expect(image.width, 640);
+ expect(image.height, 480);
+ expect(image.lensAperture, 2.5);
+ expect(image.sensorExposureTime, 5);
+ expect(image.sensorSensitivity, 1.3);
+ // Format.
+ expect(image.format.group, ImageFormatGroup.jpeg);
+ expect(image.format.raw, 1234);
+ // Planes.
+ expect(image.planes.length, originalImage.planes.length);
+ for (int i = 0; i < image.planes.length; i++) {
+ expect(
+ image.planes[i].bytes.length, originalImage.planes[i].bytes.length);
+ for (int j = 0; j < image.planes[i].bytes.length; j++) {
+ expect(image.planes[i].bytes[j], originalImage.planes[i].bytes[j]);
+ }
+ expect(
+ image.planes[i].bytesPerPixel, originalImage.planes[i].bytesPerPixel);
+ expect(image.planes[i].bytesPerRow, originalImage.planes[i].bytesPerRow);
+ expect(image.planes[i].width, originalImage.planes[i].width);
+ expect(image.planes[i].height, originalImage.planes[i].height);
+ }
+ });
+
+ group('legacy constructors', () {
+ test('$CameraImage can be created', () {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+ final CameraImage cameraImage =
+ CameraImage.fromPlatformData(<dynamic, dynamic>{
+ 'format': 35,
+ 'height': 1,
+ 'width': 4,
+ 'lensAperture': 1.8,
+ 'sensorExposureTime': 9991324,
+ 'sensorSensitivity': 92.0,
+ 'planes': <dynamic>[
+ <dynamic, dynamic>{
+ 'bytes': Uint8List.fromList(<int>[1, 2, 3, 4]),
+ 'bytesPerPixel': 1,
+ 'bytesPerRow': 4,
+ 'height': 1,
+ 'width': 4
+ }
+ ]
+ });
+ expect(cameraImage.height, 1);
+ expect(cameraImage.width, 4);
+ expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+ expect(cameraImage.planes.length, 1);
+ });
+
+ test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () {
+ debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+
+ final CameraImage cameraImage =
+ CameraImage.fromPlatformData(<dynamic, dynamic>{
+ 'format': 875704438,
+ 'height': 1,
+ 'width': 4,
+ 'lensAperture': 1.8,
+ 'sensorExposureTime': 9991324,
+ 'sensorSensitivity': 92.0,
+ 'planes': <dynamic>[
+ <dynamic, dynamic>{
+ 'bytes': Uint8List.fromList(<int>[1, 2, 3, 4]),
+ 'bytesPerPixel': 1,
+ 'bytesPerRow': 4,
+ 'height': 1,
+ 'width': 4
+ }
+ ]
+ });
+ expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+ });
+
+ test('$CameraImage has ImageFormatGroup.yuv420 for Android', () {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+ final CameraImage cameraImage =
+ CameraImage.fromPlatformData(<dynamic, dynamic>{
+ 'format': 35,
+ 'height': 1,
+ 'width': 4,
+ 'lensAperture': 1.8,
+ 'sensorExposureTime': 9991324,
+ 'sensorSensitivity': 92.0,
+ 'planes': <dynamic>[
+ <dynamic, dynamic>{
+ 'bytes': Uint8List.fromList(<int>[1, 2, 3, 4]),
+ 'bytesPerPixel': 1,
+ 'bytesPerRow': 4,
+ 'height': 1,
+ 'width': 4
+ }
+ ]
+ });
+ expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+ });
+
+ test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () {
+ debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+
+ final CameraImage cameraImage =
+ CameraImage.fromPlatformData(<dynamic, dynamic>{
+ 'format': 1111970369,
+ 'height': 1,
+ 'width': 4,
+ 'lensAperture': 1.8,
+ 'sensorExposureTime': 9991324,
+ 'sensorSensitivity': 92.0,
+ 'planes': <dynamic>[
+ <dynamic, dynamic>{
+ 'bytes': Uint8List.fromList(<int>[1, 2, 3, 4]),
+ 'bytesPerPixel': 1,
+ 'bytesPerRow': 4,
+ 'height': 1,
+ 'width': 4
+ }
+ ]
+ });
+ expect(cameraImage.format.group, ImageFormatGroup.bgra8888);
+ });
+ test('$CameraImage has ImageFormatGroup.unknown', () {
+ final CameraImage cameraImage =
+ CameraImage.fromPlatformData(<dynamic, dynamic>{
+ 'format': null,
+ 'height': 1,
+ 'width': 4,
+ 'lensAperture': 1.8,
+ 'sensorExposureTime': 9991324,
+ 'sensorSensitivity': 92.0,
+ 'planes': <dynamic>[
+ <dynamic, dynamic>{
+ 'bytes': Uint8List.fromList(<int>[1, 2, 3, 4]),
+ 'bytesPerPixel': 1,
+ 'bytesPerRow': 4,
+ 'height': 1,
+ 'width': 4
+ }
+ ]
+ });
+ expect(cameraImage.format.group, ImageFormatGroup.unknown);
+ });
+ });
+}
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
new file mode 100644
index 0000000..6677fcf
--- /dev/null
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -0,0 +1,244 @@
+// 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:camera/camera.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+class FakeController extends ValueNotifier<CameraValue>
+ implements CameraController {
+ FakeController() : super(const CameraValue.uninitialized());
+
+ @override
+ Future<void> dispose() async {
+ super.dispose();
+ }
+
+ @override
+ Widget buildPreview() {
+ return const Texture(textureId: CameraController.kUninitializedCameraId);
+ }
+
+ @override
+ int get cameraId => CameraController.kUninitializedCameraId;
+
+ @override
+ void debugCheckIsDisposed() {}
+
+ @override
+ CameraDescription get description => const CameraDescription(
+ name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);
+
+ @override
+ bool get enableAudio => false;
+
+ @override
+ Future<double> getExposureOffsetStepSize() async => 1.0;
+
+ @override
+ Future<double> getMaxExposureOffset() async => 1.0;
+
+ @override
+ Future<double> getMaxZoomLevel() async => 1.0;
+
+ @override
+ Future<double> getMinExposureOffset() async => 1.0;
+
+ @override
+ Future<double> getMinZoomLevel() async => 1.0;
+
+ @override
+ ImageFormatGroup? get imageFormatGroup => null;
+
+ @override
+ Future<void> initialize() async {}
+
+ @override
+ Future<void> lockCaptureOrientation([DeviceOrientation? orientation]) async {}
+
+ @override
+ Future<void> pauseVideoRecording() async {}
+
+ @override
+ Future<void> prepareForVideoRecording() async {}
+
+ @override
+ ResolutionPreset get resolutionPreset => ResolutionPreset.low;
+
+ @override
+ Future<void> resumeVideoRecording() async {}
+
+ @override
+ Future<void> setExposureMode(ExposureMode mode) async {}
+
+ @override
+ Future<double> setExposureOffset(double offset) async => offset;
+
+ @override
+ Future<void> setExposurePoint(Offset? point) async {}
+
+ @override
+ Future<void> setFlashMode(FlashMode mode) async {}
+
+ @override
+ Future<void> setFocusMode(FocusMode mode) async {}
+
+ @override
+ Future<void> setFocusPoint(Offset? point) async {}
+
+ @override
+ Future<void> setZoomLevel(double zoom) async {}
+
+ @override
+ Future<void> startImageStream(onLatestImageAvailable onAvailable) async {}
+
+ @override
+ Future<void> startVideoRecording(
+ {onLatestImageAvailable? onAvailable}) async {}
+
+ @override
+ Future<void> stopImageStream() async {}
+
+ @override
+ Future<XFile> stopVideoRecording() async => XFile('');
+
+ @override
+ Future<XFile> takePicture() async => XFile('');
+
+ @override
+ Future<void> unlockCaptureOrientation() async {}
+
+ @override
+ Future<void> pausePreview() async {}
+
+ @override
+ Future<void> resumePreview() async {}
+}
+
+void main() {
+ group('RotatedBox (Android only)', () {
+ testWidgets(
+ 'when recording rotatedBox should turn according to recording orientation',
+ (
+ WidgetTester tester,
+ ) async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+ final FakeController controller = FakeController();
+ controller.value = controller.value.copyWith(
+ isInitialized: true,
+ isRecordingVideo: true,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation:
+ const Optional<DeviceOrientation>.fromNullable(
+ DeviceOrientation.landscapeRight),
+ recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+ DeviceOrientation.landscapeLeft),
+ previewSize: const Size(480, 640),
+ );
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CameraPreview(controller),
+ ),
+ );
+ expect(find.byType(RotatedBox), findsOneWidget);
+
+ final RotatedBox rotatedBox =
+ tester.widget<RotatedBox>(find.byType(RotatedBox));
+ expect(rotatedBox.quarterTurns, 3);
+
+ debugDefaultTargetPlatformOverride = null;
+ });
+
+ testWidgets(
+ 'when orientation locked rotatedBox should turn according to locked orientation',
+ (
+ WidgetTester tester,
+ ) async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+ final FakeController controller = FakeController();
+ controller.value = controller.value.copyWith(
+ isInitialized: true,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation:
+ const Optional<DeviceOrientation>.fromNullable(
+ DeviceOrientation.landscapeRight),
+ recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+ DeviceOrientation.landscapeLeft),
+ previewSize: const Size(480, 640),
+ );
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CameraPreview(controller),
+ ),
+ );
+ expect(find.byType(RotatedBox), findsOneWidget);
+
+ final RotatedBox rotatedBox =
+ tester.widget<RotatedBox>(find.byType(RotatedBox));
+ expect(rotatedBox.quarterTurns, 1);
+
+ debugDefaultTargetPlatformOverride = null;
+ });
+
+ testWidgets(
+ 'when not locked and not recording rotatedBox should turn according to device orientation',
+ (
+ WidgetTester tester,
+ ) async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+ final FakeController controller = FakeController();
+ controller.value = controller.value.copyWith(
+ isInitialized: true,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+ DeviceOrientation.landscapeLeft),
+ previewSize: const Size(480, 640),
+ );
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CameraPreview(controller),
+ ),
+ );
+ expect(find.byType(RotatedBox), findsOneWidget);
+
+ final RotatedBox rotatedBox =
+ tester.widget<RotatedBox>(find.byType(RotatedBox));
+ expect(rotatedBox.quarterTurns, 0);
+
+ debugDefaultTargetPlatformOverride = null;
+ });
+ }, skip: kIsWeb);
+
+ testWidgets('when not on Android there should not be a rotated box',
+ (WidgetTester tester) async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+ final FakeController controller = FakeController();
+ controller.value = controller.value.copyWith(
+ isInitialized: true,
+ previewSize: const Size(480, 640),
+ );
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CameraPreview(controller),
+ ),
+ );
+ expect(find.byType(RotatedBox), findsNothing);
+ expect(find.byType(Texture), findsOneWidget);
+ debugDefaultTargetPlatformOverride = null;
+ });
+}
diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart
new file mode 100644
index 0000000..ab8354f
--- /dev/null
+++ b/packages/camera/camera/test/camera_test.dart
@@ -0,0 +1,1537 @@
+// 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 'dart:async';
+import 'dart:math';
+
+import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+List<CameraDescription> get mockAvailableCameras => <CameraDescription>[
+ const CameraDescription(
+ name: 'camBack',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ const CameraDescription(
+ name: 'camFront',
+ lensDirection: CameraLensDirection.front,
+ sensorOrientation: 180),
+ ];
+
+int get mockInitializeCamera => 13;
+
+CameraInitializedEvent get mockOnCameraInitializedEvent =>
+ const CameraInitializedEvent(
+ 13,
+ 75,
+ 75,
+ ExposureMode.auto,
+ true,
+ FocusMode.auto,
+ true,
+ );
+
+DeviceOrientationChangedEvent get mockOnDeviceOrientationChangedEvent =>
+ const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp);
+
+CameraClosingEvent get mockOnCameraClosingEvent => const CameraClosingEvent(13);
+
+CameraErrorEvent get mockOnCameraErrorEvent =>
+ const CameraErrorEvent(13, 'closing');
+
+XFile mockTakePicture = XFile('foo/bar.png');
+
+XFile mockVideoRecordingXFile = XFile('foo/bar.mpeg');
+
+bool mockPlatformException = false;
+
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ group('camera', () {
+ test('debugCheckIsDisposed should not throw assertion error when disposed',
+ () {
+ const MockCameraDescription description = MockCameraDescription();
+ final CameraController controller = CameraController(
+ description,
+ ResolutionPreset.low,
+ );
+
+ controller.dispose();
+
+ expect(controller.debugCheckIsDisposed, returnsNormally);
+ });
+
+ test('debugCheckIsDisposed should throw assertion error when not disposed',
+ () {
+ const MockCameraDescription description = MockCameraDescription();
+ final CameraController controller = CameraController(
+ description,
+ ResolutionPreset.low,
+ );
+
+ expect(
+ () => controller.debugCheckIsDisposed(),
+ throwsAssertionError,
+ );
+ });
+
+ test('availableCameras() has camera', () async {
+ CameraPlatform.instance = MockCameraPlatform();
+
+ final List<CameraDescription> camList = await availableCameras();
+
+ expect(camList, equals(mockAvailableCameras));
+ });
+ });
+
+ group('$CameraController', () {
+ setUpAll(() {
+ CameraPlatform.instance = MockCameraPlatform();
+ });
+
+ test('Can be initialized', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ expect(cameraController.value.aspectRatio, 1);
+ expect(cameraController.value.previewSize, const Size(75, 75));
+ expect(cameraController.value.isInitialized, isTrue);
+ });
+
+ test('can be disposed', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ expect(cameraController.value.aspectRatio, 1);
+ expect(cameraController.value.previewSize, const Size(75, 75));
+ expect(cameraController.value.isInitialized, isTrue);
+
+ await cameraController.dispose();
+
+ verify(CameraPlatform.instance.dispose(13)).called(1);
+ });
+
+ test('initialize() throws CameraException when disposed', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ expect(cameraController.value.aspectRatio, 1);
+ expect(cameraController.value.previewSize, const Size(75, 75));
+ expect(cameraController.value.isInitialized, isTrue);
+
+ await cameraController.dispose();
+
+ verify(CameraPlatform.instance.dispose(13)).called(1);
+
+ expect(
+ cameraController.initialize,
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'Error description',
+ 'initialize was called on a disposed CameraController',
+ )));
+ });
+
+ test('initialize() throws $CameraException on $PlatformException ',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ mockPlatformException = true;
+
+ expect(
+ cameraController.initialize,
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'foo',
+ 'bar',
+ )));
+ mockPlatformException = false;
+ });
+
+ test('initialize() sets imageFormat', () async {
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max,
+ imageFormatGroup: ImageFormatGroup.yuv420,
+ );
+ await cameraController.initialize();
+ verify(CameraPlatform.instance
+ .initializeCamera(13, imageFormatGroup: ImageFormatGroup.yuv420))
+ .called(1);
+ });
+
+ test('prepareForVideoRecording() calls $CameraPlatform ', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.prepareForVideoRecording();
+
+ verify(CameraPlatform.instance.prepareForVideoRecording()).called(1);
+ });
+
+ test('takePicture() throws $CameraException when uninitialized ', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ expect(
+ cameraController.takePicture(),
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'takePicture() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('takePicture() throws $CameraException when takePicture is true',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ cameraController.value =
+ cameraController.value.copyWith(isTakingPicture: true);
+ expect(
+ cameraController.takePicture(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'Previous capture has not returned yet.',
+ 'takePicture was called before the previous capture returned.',
+ )));
+ });
+
+ test('takePicture() returns $XFile', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ final XFile xFile = await cameraController.takePicture();
+
+ expect(xFile.path, mockTakePicture.path);
+ });
+
+ test('takePicture() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ mockPlatformException = true;
+ expect(
+ cameraController.takePicture(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'foo',
+ 'bar',
+ )));
+ mockPlatformException = false;
+ });
+
+ test('startVideoRecording() throws $CameraException when uninitialized',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ cameraController.startVideoRecording(),
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'startVideoRecording() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+ test('startVideoRecording() throws $CameraException when recording videos',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+
+ cameraController.value =
+ cameraController.value.copyWith(isRecordingVideo: true);
+
+ expect(
+ cameraController.startVideoRecording(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'A video recording is already started.',
+ 'startVideoRecording was called when a recording is already started.',
+ )));
+ });
+
+ test('getMaxZoomLevel() throws $CameraException when uninitialized',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ cameraController.getMaxZoomLevel,
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'getMaxZoomLevel() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('getMaxZoomLevel() throws $CameraException when disposed', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ await cameraController.dispose();
+
+ expect(
+ cameraController.getMaxZoomLevel,
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Disposed CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'getMaxZoomLevel() was called on a disposed CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test(
+ 'getMaxZoomLevel() throws $CameraException when a platform exception occured.',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera))
+ .thenThrow(CameraException(
+ 'TEST_ERROR',
+ 'This is a test error messge',
+ ));
+
+ expect(
+ cameraController.getMaxZoomLevel,
+ throwsA(isA<CameraException>()
+ .having(
+ (CameraException error) => error.code, 'code', 'TEST_ERROR')
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'This is a test error messge',
+ )));
+ });
+
+ test('getMaxZoomLevel() returns max zoom level.', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera))
+ .thenAnswer((_) => Future<double>.value(42.0));
+
+ final double maxZoomLevel = await cameraController.getMaxZoomLevel();
+ expect(maxZoomLevel, 42.0);
+ });
+
+ test('getMinZoomLevel() throws $CameraException when uninitialized',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ cameraController.getMinZoomLevel,
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'getMinZoomLevel() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('getMinZoomLevel() throws $CameraException when disposed', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ await cameraController.dispose();
+
+ expect(
+ cameraController.getMinZoomLevel,
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Disposed CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'getMinZoomLevel() was called on a disposed CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test(
+ 'getMinZoomLevel() throws $CameraException when a platform exception occured.',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera))
+ .thenThrow(CameraException(
+ 'TEST_ERROR',
+ 'This is a test error messge',
+ ));
+
+ expect(
+ cameraController.getMinZoomLevel,
+ throwsA(isA<CameraException>()
+ .having(
+ (CameraException error) => error.code, 'code', 'TEST_ERROR')
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'This is a test error messge',
+ )));
+ });
+
+ test('getMinZoomLevel() returns max zoom level.', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera))
+ .thenAnswer((_) => Future<double>.value(42.0));
+
+ final double maxZoomLevel = await cameraController.getMinZoomLevel();
+ expect(maxZoomLevel, 42.0);
+ });
+
+ test('setZoomLevel() throws $CameraException when uninitialized', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ expect(
+ () => cameraController.setZoomLevel(42.0),
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Uninitialized CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'setZoomLevel() was called on an uninitialized CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test('setZoomLevel() throws $CameraException when disposed', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ await cameraController.dispose();
+
+ expect(
+ () => cameraController.setZoomLevel(42.0),
+ throwsA(
+ isA<CameraException>()
+ .having(
+ (CameraException error) => error.code,
+ 'code',
+ 'Disposed CameraController',
+ )
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'setZoomLevel() was called on a disposed CameraController.',
+ ),
+ ),
+ );
+ });
+
+ test(
+ 'setZoomLevel() throws $CameraException when a platform exception occured.',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ when(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0))
+ .thenThrow(CameraException(
+ 'TEST_ERROR',
+ 'This is a test error messge',
+ ));
+
+ expect(
+ () => cameraController.setZoomLevel(42),
+ throwsA(isA<CameraException>()
+ .having(
+ (CameraException error) => error.code, 'code', 'TEST_ERROR')
+ .having(
+ (CameraException error) => error.description,
+ 'description',
+ 'This is a test error messge',
+ )));
+
+ reset(CameraPlatform.instance);
+ });
+
+ test(
+ 'setZoomLevel() completes and calls method channel with correct value.',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+
+ await cameraController.initialize();
+ await cameraController.setZoomLevel(42.0);
+
+ verify(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0))
+ .called(1);
+ });
+
+ test('setFlashMode() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.setFlashMode(FlashMode.always);
+
+ verify(CameraPlatform.instance
+ .setFlashMode(cameraController.cameraId, FlashMode.always))
+ .called(1);
+ });
+
+ test('setFlashMode() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .setFlashMode(cameraController.cameraId, FlashMode.always))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.setFlashMode(FlashMode.always),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('setExposureMode() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.setExposureMode(ExposureMode.auto);
+
+ verify(CameraPlatform.instance
+ .setExposureMode(cameraController.cameraId, ExposureMode.auto))
+ .called(1);
+ });
+
+ test('setExposureMode() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .setExposureMode(cameraController.cameraId, ExposureMode.auto))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.setExposureMode(ExposureMode.auto),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('setExposurePoint() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.setExposurePoint(const Offset(0.5, 0.5));
+
+ verify(CameraPlatform.instance.setExposurePoint(
+ cameraController.cameraId, const Point<double>(0.5, 0.5)))
+ .called(1);
+ });
+
+ test('setExposurePoint() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance.setExposurePoint(
+ cameraController.cameraId, const Point<double>(0.5, 0.5)))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.setExposurePoint(const Offset(0.5, 0.5)),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('getMinExposureOffset() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) => Future<double>.value(0.0));
+
+ await cameraController.getMinExposureOffset();
+
+ verify(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .called(1);
+ });
+
+ test('getMinExposureOffset() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenThrow(
+ CameraException(
+ 'TEST_ERROR',
+ 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.getMinExposureOffset(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('getMaxExposureOffset() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) => Future<double>.value(1.0));
+
+ await cameraController.getMaxExposureOffset();
+
+ verify(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .called(1);
+ });
+
+ test('getMaxExposureOffset() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenThrow(
+ CameraException(
+ 'TEST_ERROR',
+ 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.getMaxExposureOffset(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('getExposureOffsetStepSize() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenAnswer((_) => Future<double>.value(0.0));
+
+ await cameraController.getExposureOffsetStepSize();
+
+ verify(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .called(1);
+ });
+
+ test(
+ 'getExposureOffsetStepSize() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenThrow(
+ CameraException(
+ 'TEST_ERROR',
+ 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.getExposureOffsetStepSize(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('setExposureOffset() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => -1.0);
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => 2.0);
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenAnswer((_) async => 1.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 1.0))
+ .thenAnswer((_) async => 1.0);
+
+ await cameraController.setExposureOffset(1.0);
+
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 1.0))
+ .called(1);
+ });
+
+ test('setExposureOffset() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => -1.0);
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => 2.0);
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenAnswer((_) async => 1.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 1.0))
+ .thenThrow(
+ CameraException(
+ 'TEST_ERROR',
+ 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.setExposureOffset(1.0),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test(
+ 'setExposureOffset() throws $CameraException when offset is out of bounds',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => -1.0);
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => 2.0);
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenAnswer((_) async => 1.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.0))
+ .thenAnswer((_) async => 0.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -1.0))
+ .thenAnswer((_) async => 0.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 2.0))
+ .thenAnswer((_) async => 0.0);
+
+ expect(
+ cameraController.setExposureOffset(3.0),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'exposureOffsetOutOfBounds',
+ 'The provided exposure offset was outside the supported range for this device.',
+ )));
+ expect(
+ cameraController.setExposureOffset(-2.0),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'exposureOffsetOutOfBounds',
+ 'The provided exposure offset was outside the supported range for this device.',
+ )));
+
+ await cameraController.setExposureOffset(0.0);
+ await cameraController.setExposureOffset(-1.0);
+ await cameraController.setExposureOffset(2.0);
+
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.0))
+ .called(1);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -1.0))
+ .called(1);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 2.0))
+ .called(1);
+ });
+
+ test('setExposureOffset() rounds offset to nearest step', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance
+ .getMinExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => -1.2);
+ when(CameraPlatform.instance
+ .getMaxExposureOffset(cameraController.cameraId))
+ .thenAnswer((_) async => 1.2);
+ when(CameraPlatform.instance
+ .getExposureOffsetStepSize(cameraController.cameraId))
+ .thenAnswer((_) async => 0.4);
+
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -1.2))
+ .thenAnswer((_) async => -1.2);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -0.8))
+ .thenAnswer((_) async => -0.8);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -0.4))
+ .thenAnswer((_) async => -0.4);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.0))
+ .thenAnswer((_) async => 0.0);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.4))
+ .thenAnswer((_) async => 0.4);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.8))
+ .thenAnswer((_) async => 0.8);
+ when(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 1.2))
+ .thenAnswer((_) async => 1.2);
+
+ await cameraController.setExposureOffset(1.2);
+ await cameraController.setExposureOffset(-1.2);
+ await cameraController.setExposureOffset(0.1);
+ await cameraController.setExposureOffset(0.2);
+ await cameraController.setExposureOffset(0.3);
+ await cameraController.setExposureOffset(0.4);
+ await cameraController.setExposureOffset(0.5);
+ await cameraController.setExposureOffset(0.6);
+ await cameraController.setExposureOffset(0.7);
+ await cameraController.setExposureOffset(-0.1);
+ await cameraController.setExposureOffset(-0.2);
+ await cameraController.setExposureOffset(-0.3);
+ await cameraController.setExposureOffset(-0.4);
+ await cameraController.setExposureOffset(-0.5);
+ await cameraController.setExposureOffset(-0.6);
+ await cameraController.setExposureOffset(-0.7);
+
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.8))
+ .called(2);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -0.8))
+ .called(2);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.0))
+ .called(2);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, 0.4))
+ .called(4);
+ verify(CameraPlatform.instance
+ .setExposureOffset(cameraController.cameraId, -0.4))
+ .called(4);
+ });
+
+ test('pausePreview() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value = cameraController.value
+ .copyWith(deviceOrientation: DeviceOrientation.portraitUp);
+
+ await cameraController.pausePreview();
+
+ verify(CameraPlatform.instance.pausePreview(cameraController.cameraId))
+ .called(1);
+ expect(cameraController.value.isPreviewPaused, equals(true));
+ expect(cameraController.value.previewPauseOrientation,
+ DeviceOrientation.portraitUp);
+ });
+
+ test('pausePreview() does not call $CameraPlatform when already paused',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value =
+ cameraController.value.copyWith(isPreviewPaused: true);
+
+ await cameraController.pausePreview();
+
+ verifyNever(
+ CameraPlatform.instance.pausePreview(cameraController.cameraId));
+ expect(cameraController.value.isPreviewPaused, equals(true));
+ });
+
+ test(
+ 'pausePreview() sets previewPauseOrientation according to locked orientation',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value = cameraController.value.copyWith(
+ isPreviewPaused: false,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation:
+ Optional<DeviceOrientation>.of(DeviceOrientation.landscapeRight));
+
+ await cameraController.pausePreview();
+
+ expect(cameraController.value.deviceOrientation,
+ equals(DeviceOrientation.portraitUp));
+ expect(cameraController.value.previewPauseOrientation,
+ equals(DeviceOrientation.landscapeRight));
+ });
+
+ test('pausePreview() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance.pausePreview(cameraController.cameraId))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.pausePreview(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('resumePreview() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value =
+ cameraController.value.copyWith(isPreviewPaused: true);
+
+ await cameraController.resumePreview();
+
+ verify(CameraPlatform.instance.resumePreview(cameraController.cameraId))
+ .called(1);
+ expect(cameraController.value.isPreviewPaused, equals(false));
+ });
+
+ test('resumePreview() does not call $CameraPlatform when not paused',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value =
+ cameraController.value.copyWith(isPreviewPaused: false);
+
+ await cameraController.resumePreview();
+
+ verifyNever(
+ CameraPlatform.instance.resumePreview(cameraController.cameraId));
+ expect(cameraController.value.isPreviewPaused, equals(false));
+ });
+
+ test('resumePreview() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ cameraController.value =
+ cameraController.value.copyWith(isPreviewPaused: true);
+ when(CameraPlatform.instance.resumePreview(cameraController.cameraId))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.resumePreview(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('lockCaptureOrientation() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.lockCaptureOrientation();
+ expect(cameraController.value.lockedCaptureOrientation,
+ equals(DeviceOrientation.portraitUp));
+ await cameraController
+ .lockCaptureOrientation(DeviceOrientation.landscapeRight);
+ expect(cameraController.value.lockedCaptureOrientation,
+ equals(DeviceOrientation.landscapeRight));
+
+ verify(CameraPlatform.instance.lockCaptureOrientation(
+ cameraController.cameraId, DeviceOrientation.portraitUp))
+ .called(1);
+ verify(CameraPlatform.instance.lockCaptureOrientation(
+ cameraController.cameraId, DeviceOrientation.landscapeRight))
+ .called(1);
+ });
+
+ test(
+ 'lockCaptureOrientation() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance.lockCaptureOrientation(
+ cameraController.cameraId, DeviceOrientation.portraitUp))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+
+ test('unlockCaptureOrientation() calls $CameraPlatform', () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+
+ await cameraController.unlockCaptureOrientation();
+ expect(cameraController.value.lockedCaptureOrientation, equals(null));
+
+ verify(CameraPlatform.instance
+ .unlockCaptureOrientation(cameraController.cameraId))
+ .called(1);
+ });
+
+ test(
+ 'unlockCaptureOrientation() throws $CameraException on $PlatformException',
+ () async {
+ final CameraController cameraController = CameraController(
+ const CameraDescription(
+ name: 'cam',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90),
+ ResolutionPreset.max);
+ await cameraController.initialize();
+ when(CameraPlatform.instance
+ .unlockCaptureOrientation(cameraController.cameraId))
+ .thenThrow(
+ PlatformException(
+ code: 'TEST_ERROR',
+ message: 'This is a test error message',
+ ),
+ );
+
+ expect(
+ cameraController.unlockCaptureOrientation(),
+ throwsA(isA<CameraException>().having(
+ (CameraException error) => error.description,
+ 'TEST_ERROR',
+ 'This is a test error message',
+ )));
+ });
+ });
+}
+
+class MockCameraPlatform extends Mock
+ with MockPlatformInterfaceMixin
+ implements CameraPlatform {
+ @override
+ Future<void> initializeCamera(
+ int? cameraId, {
+ ImageFormatGroup? imageFormatGroup = ImageFormatGroup.unknown,
+ }) async =>
+ super.noSuchMethod(Invocation.method(
+ #initializeCamera,
+ <Object?>[cameraId],
+ <Symbol, dynamic>{
+ #imageFormatGroup: imageFormatGroup,
+ },
+ ));
+
+ @override
+ Future<void> dispose(int? cameraId) async {
+ return super.noSuchMethod(Invocation.method(#dispose, <Object?>[cameraId]));
+ }
+
+ @override
+ Future<List<CameraDescription>> availableCameras() =>
+ Future<List<CameraDescription>>.value(mockAvailableCameras);
+
+ @override
+ Future<int> createCamera(
+ CameraDescription description,
+ ResolutionPreset? resolutionPreset, {
+ bool enableAudio = false,
+ }) =>
+ mockPlatformException
+ ? throw PlatformException(code: 'foo', message: 'bar')
+ : Future<int>.value(mockInitializeCamera);
+
+ @override
+ Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
+ Stream<CameraInitializedEvent>.value(mockOnCameraInitializedEvent);
+
+ @override
+ Stream<CameraClosingEvent> onCameraClosing(int cameraId) =>
+ Stream<CameraClosingEvent>.value(mockOnCameraClosingEvent);
+
+ @override
+ Stream<CameraErrorEvent> onCameraError(int cameraId) =>
+ Stream<CameraErrorEvent>.value(mockOnCameraErrorEvent);
+
+ @override
+ Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() =>
+ Stream<DeviceOrientationChangedEvent>.value(
+ mockOnDeviceOrientationChangedEvent);
+
+ @override
+ Future<XFile> takePicture(int cameraId) => mockPlatformException
+ ? throw PlatformException(code: 'foo', message: 'bar')
+ : Future<XFile>.value(mockTakePicture);
+
+ @override
+ Future<void> prepareForVideoRecording() async =>
+ super.noSuchMethod(Invocation.method(#prepareForVideoRecording, null));
+
+ @override
+ Future<XFile> startVideoRecording(int cameraId,
+ {Duration? maxVideoDuration}) =>
+ Future<XFile>.value(mockVideoRecordingXFile);
+
+ @override
+ Future<void> startVideoCapturing(VideoCaptureOptions options) {
+ return startVideoRecording(options.cameraId,
+ maxVideoDuration: options.maxDuration);
+ }
+
+ @override
+ Future<void> lockCaptureOrientation(
+ int? cameraId, DeviceOrientation? orientation) async =>
+ super.noSuchMethod(Invocation.method(
+ #lockCaptureOrientation, <Object?>[cameraId, orientation]));
+
+ @override
+ Future<void> unlockCaptureOrientation(int? cameraId) async =>
+ super.noSuchMethod(
+ Invocation.method(#unlockCaptureOrientation, <Object?>[cameraId]));
+
+ @override
+ Future<void> pausePreview(int? cameraId) async =>
+ super.noSuchMethod(Invocation.method(#pausePreview, <Object?>[cameraId]));
+
+ @override
+ Future<void> resumePreview(int? cameraId) async => super
+ .noSuchMethod(Invocation.method(#resumePreview, <Object?>[cameraId]));
+
+ @override
+ Future<double> getMaxZoomLevel(int? cameraId) async => super.noSuchMethod(
+ Invocation.method(#getMaxZoomLevel, <Object?>[cameraId]),
+ returnValue: Future<double>.value(1.0),
+ ) as Future<double>;
+
+ @override
+ Future<double> getMinZoomLevel(int? cameraId) async => super.noSuchMethod(
+ Invocation.method(#getMinZoomLevel, <Object?>[cameraId]),
+ returnValue: Future<double>.value(0.0),
+ ) as Future<double>;
+
+ @override
+ Future<void> setZoomLevel(int? cameraId, double? zoom) async =>
+ super.noSuchMethod(
+ Invocation.method(#setZoomLevel, <Object?>[cameraId, zoom]));
+
+ @override
+ Future<void> setFlashMode(int? cameraId, FlashMode? mode) async =>
+ super.noSuchMethod(
+ Invocation.method(#setFlashMode, <Object?>[cameraId, mode]));
+
+ @override
+ Future<void> setExposureMode(int? cameraId, ExposureMode? mode) async =>
+ super.noSuchMethod(
+ Invocation.method(#setExposureMode, <Object?>[cameraId, mode]));
+
+ @override
+ Future<void> setExposurePoint(int? cameraId, Point<double>? point) async =>
+ super.noSuchMethod(
+ Invocation.method(#setExposurePoint, <Object?>[cameraId, point]));
+
+ @override
+ Future<double> getMinExposureOffset(int? cameraId) async =>
+ super.noSuchMethod(
+ Invocation.method(#getMinExposureOffset, <Object?>[cameraId]),
+ returnValue: Future<double>.value(0.0),
+ ) as Future<double>;
+
+ @override
+ Future<double> getMaxExposureOffset(int? cameraId) async =>
+ super.noSuchMethod(
+ Invocation.method(#getMaxExposureOffset, <Object?>[cameraId]),
+ returnValue: Future<double>.value(1.0),
+ ) as Future<double>;
+
+ @override
+ Future<double> getExposureOffsetStepSize(int? cameraId) async =>
+ super.noSuchMethod(
+ Invocation.method(#getExposureOffsetStepSize, <Object?>[cameraId]),
+ returnValue: Future<double>.value(1.0),
+ ) as Future<double>;
+
+ @override
+ Future<double> setExposureOffset(int? cameraId, double? offset) async =>
+ super.noSuchMethod(
+ Invocation.method(#setExposureOffset, <Object?>[cameraId, offset]),
+ returnValue: Future<double>.value(1.0),
+ ) as Future<double>;
+}
+
+class MockCameraDescription extends CameraDescription {
+ /// Creates a new camera description with the given properties.
+ const MockCameraDescription()
+ : super(
+ name: 'Test',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 0,
+ );
+
+ @override
+ CameraLensDirection get lensDirection => CameraLensDirection.back;
+
+ @override
+ String get name => 'back';
+}
diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart
new file mode 100644
index 0000000..37168db
--- /dev/null
+++ b/packages/camera/camera/test/camera_value_test.dart
@@ -0,0 +1,150 @@
+// 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.
+
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316)
+// ignore: unnecessary_import
+import 'dart:ui';
+
+import 'package:camera/camera.dart';
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316)
+// ignore: unnecessary_import
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('camera_value', () {
+ test('Can be created', () {
+ const CameraValue cameraValue = CameraValue(
+ isInitialized: false,
+ previewSize: Size(10, 10),
+ isRecordingPaused: false,
+ isRecordingVideo: false,
+ isTakingPicture: false,
+ isStreamingImages: false,
+ flashMode: FlashMode.auto,
+ exposureMode: ExposureMode.auto,
+ exposurePointSupported: true,
+ focusMode: FocusMode.auto,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation: DeviceOrientation.portraitUp,
+ recordingOrientation: DeviceOrientation.portraitUp,
+ focusPointSupported: true,
+ previewPauseOrientation: DeviceOrientation.portraitUp,
+ );
+
+ expect(cameraValue, isA<CameraValue>());
+ expect(cameraValue.isInitialized, isFalse);
+ expect(cameraValue.errorDescription, null);
+ expect(cameraValue.previewSize, const Size(10, 10));
+ expect(cameraValue.isRecordingPaused, isFalse);
+ expect(cameraValue.isRecordingVideo, isFalse);
+ expect(cameraValue.isTakingPicture, isFalse);
+ expect(cameraValue.isStreamingImages, isFalse);
+ expect(cameraValue.flashMode, FlashMode.auto);
+ expect(cameraValue.exposureMode, ExposureMode.auto);
+ expect(cameraValue.exposurePointSupported, true);
+ expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp);
+ expect(
+ cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp);
+ expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp);
+ expect(cameraValue.isPreviewPaused, false);
+ expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp);
+ });
+
+ test('Can be created as uninitialized', () {
+ const CameraValue cameraValue = CameraValue.uninitialized();
+
+ expect(cameraValue, isA<CameraValue>());
+ expect(cameraValue.isInitialized, isFalse);
+ expect(cameraValue.errorDescription, null);
+ expect(cameraValue.previewSize, null);
+ expect(cameraValue.isRecordingPaused, isFalse);
+ expect(cameraValue.isRecordingVideo, isFalse);
+ expect(cameraValue.isTakingPicture, isFalse);
+ expect(cameraValue.isStreamingImages, isFalse);
+ expect(cameraValue.flashMode, FlashMode.auto);
+ expect(cameraValue.exposureMode, ExposureMode.auto);
+ expect(cameraValue.exposurePointSupported, false);
+ expect(cameraValue.focusMode, FocusMode.auto);
+ expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp);
+ expect(cameraValue.lockedCaptureOrientation, null);
+ expect(cameraValue.recordingOrientation, null);
+ expect(cameraValue.isPreviewPaused, isFalse);
+ expect(cameraValue.previewPauseOrientation, null);
+ });
+
+ test('Can be copied with isInitialized', () {
+ const CameraValue cv = CameraValue.uninitialized();
+ final CameraValue cameraValue = cv.copyWith(isInitialized: true);
+
+ expect(cameraValue, isA<CameraValue>());
+ expect(cameraValue.isInitialized, isTrue);
+ expect(cameraValue.errorDescription, null);
+ expect(cameraValue.previewSize, null);
+ expect(cameraValue.isRecordingPaused, isFalse);
+ expect(cameraValue.isRecordingVideo, isFalse);
+ expect(cameraValue.isTakingPicture, isFalse);
+ expect(cameraValue.isStreamingImages, isFalse);
+ expect(cameraValue.flashMode, FlashMode.auto);
+ expect(cameraValue.focusMode, FocusMode.auto);
+ expect(cameraValue.exposureMode, ExposureMode.auto);
+ expect(cameraValue.exposurePointSupported, false);
+ expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp);
+ expect(cameraValue.lockedCaptureOrientation, null);
+ expect(cameraValue.recordingOrientation, null);
+ expect(cameraValue.isPreviewPaused, isFalse);
+ expect(cameraValue.previewPauseOrientation, null);
+ });
+
+ test('Has aspectRatio after setting size', () {
+ const CameraValue cv = CameraValue.uninitialized();
+ final CameraValue cameraValue =
+ cv.copyWith(isInitialized: true, previewSize: const Size(20, 10));
+
+ expect(cameraValue.aspectRatio, 2.0);
+ });
+
+ test('hasError is true after setting errorDescription', () {
+ const CameraValue cv = CameraValue.uninitialized();
+ final CameraValue cameraValue = cv.copyWith(errorDescription: 'error');
+
+ expect(cameraValue.hasError, isTrue);
+ expect(cameraValue.errorDescription, 'error');
+ });
+
+ test('Recording paused is false when not recording', () {
+ const CameraValue cv = CameraValue.uninitialized();
+ final CameraValue cameraValue = cv.copyWith(
+ isInitialized: true,
+ isRecordingVideo: false,
+ isRecordingPaused: true);
+
+ expect(cameraValue.isRecordingPaused, isFalse);
+ });
+
+ test('toString() works as expected', () {
+ const CameraValue cameraValue = CameraValue(
+ isInitialized: false,
+ previewSize: Size(10, 10),
+ isRecordingPaused: false,
+ isRecordingVideo: false,
+ isTakingPicture: false,
+ isStreamingImages: false,
+ flashMode: FlashMode.auto,
+ exposureMode: ExposureMode.auto,
+ focusMode: FocusMode.auto,
+ exposurePointSupported: true,
+ focusPointSupported: true,
+ deviceOrientation: DeviceOrientation.portraitUp,
+ lockedCaptureOrientation: DeviceOrientation.portraitUp,
+ recordingOrientation: DeviceOrientation.portraitUp,
+ isPreviewPaused: true,
+ previewPauseOrientation: DeviceOrientation.portraitUp);
+
+ expect(cameraValue.toString(),
+ 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)');
+ });
+ });
+}
diff --git a/packages/camera/camera_android/AUTHORS b/packages/camera/camera_android/AUTHORS
new file mode 100644
index 0000000..493a0b4
--- /dev/null
+++ b/packages/camera/camera_android/AUTHORS
@@ -0,0 +1,66 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
+The Chromium Authors
+German Saprykin <saprykin.h@gmail.com>
+Benjamin Sauer <sauer.benjamin@gmail.com>
+larsenthomasj@gmail.com
+Ali Bitek <alibitek@protonmail.ch>
+Pol Batlló <pol.batllo@gmail.com>
+Anatoly Pulyaevskiy
+Hayden Flinner <haydenflinner@gmail.com>
+Stefano Rodriguez <hlsroddy@gmail.com>
+Salvatore Giordano <salvatoregiordanoo@gmail.com>
+Brian Armstrong <brian@flutter.institute>
+Paul DeMarco <paulmdemarco@gmail.com>
+Fabricio Nogueira <feufeu@gmail.com>
+Simon Lightfoot <simon@devangels.london>
+Ashton Thomas <ashton@acrinta.com>
+Thomas Danner <thmsdnnr@gmail.com>
+Diego Velásquez <diego.velasquez.lopez@gmail.com>
+Hajime Nakamura <nkmrhj@gmail.com>
+Tuyển Vũ Xuân <netsoft1985@gmail.com>
+Miguel Ruivo <miguel@miguelruivo.com>
+Sarthak Verma <sarthak@artiosys.com>
+Mike Diarmid <mike@invertase.io>
+Invertase <oss@invertase.io>
+Elliot Hesp <elliot@invertase.io>
+Vince Varga <vince.varga@smaho.com>
+Aawaz Gyawali <awazgyawali@gmail.com>
+EUI Limited <ian.evans3@admiralgroup.co.uk>
+Katarina Sheremet <katarina@sheremet.ch>
+Thomas Stockx <thomas@stockxit.com>
+Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
+Ozkan Eksi <ozeksi@gmail.com>
+Rishab Nayak <rishab@bu.edu>
+ko2ic <ko2ic.dev@gmail.com>
+Jonathan Younger <jonathan@daikini.com>
+Jose Sanchez <josesm82@gmail.com>
+Debkanchan Samadder <debu.samadder@gmail.com>
+Audrius Karosevicius <audrius.karosevicius@gmail.com>
+Lukasz Piliszczuk <lukasz@intheloup.io>
+SoundReply Solutions GmbH <ch@soundreply.com>
+Rafal Wachol <rwachol@gmail.com>
+Pau Picas <pau.picas@gmail.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Alexandru Tuca <salexandru.tuca@outlook.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Rhodes Davis Jr. <rody.davis.jr@gmail.com>
+Luigi Agosti <luigi@tengio.com>
+Quentin Le Guennec <quentin@tengio.com>
+Koushik Ravikumar <koushik@tengio.com>
+Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
+Ryo Miyake <ryo@miyake.id>
+Théo Champion <contact.theochampion@gmail.com>
+Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
+Eitan Schwartz <eshvartz@gmail.com>
+Chris Rutkowski <chrisrutkowski89@gmail.com>
+Juan Alvarez <juan.alvarez@resideo.com>
+Aleksandr Yurkovskiy <sanekyy@gmail.com>
+Anton Borries <mail@antonborri.es>
+Alex Li <google@alexv525.com>
+Rahul Raj <64.rahulraj@gmail.com>
diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md
new file mode 100644
index 0000000..4609b40
--- /dev/null
+++ b/packages/camera/camera_android/CHANGELOG.md
@@ -0,0 +1,75 @@
+## 0.10.4
+
+* Temporarily fixes issue with requested video profiles being null by falling back to deprecated behavior in that case.
+
+## 0.10.3
+
+* Adds back use of Optional type.
+* Updates minimum Flutter version to 3.0.
+
+## 0.10.2+3
+
+* Updates code for stricter lint checks.
+
+## 0.10.2+2
+
+* Fixes zoom computation for virtual cameras hiding physical cameras in Android 11+.
+* Removes the unused CameraZoom class from the codebase.
+
+## 0.10.2+1
+
+* Updates code for stricter lint checks.
+
+## 0.10.2
+
+* Remove usage of deprecated quiver Optional type.
+
+## 0.10.1
+
+* Implements an option to also stream when recording a video.
+
+## 0.10.0+5
+
+* Fixes `ArrayIndexOutOfBoundsException` when the permission request is interrupted.
+
+## 0.10.0+4
+
+* Upgrades `androidx.annotation` version to 1.5.0.
+
+## 0.10.0+3
+
+* Updates code for `no_leading_underscores_for_local_identifiers` lint.
+
+## 0.10.0+2
+
+* Removes call to `join` on the camera's background `HandlerThread`.
+* Updates minimum Flutter version to 2.10.
+
+## 0.10.0+1
+
+* Fixes avoid_redundant_argument_values lint warnings and minor typos.
+
+## 0.10.0
+
+* **Breaking Change** Updates Android camera access permission error codes to be consistent with other platforms. If your app still handles the legacy `cameraPermission` exception, please update it to handle the new permission exception codes that are noted in the README.
+* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750).
+
+## 0.9.8+3
+
+* Skips duplicate calls to stop background thread and removes unnecessary closings of camera capture sessions on Android.
+
+## 0.9.8+2
+
+* Fixes exception in registerWith caused by the switch to an in-package method channel.
+
+## 0.9.8+1
+
+* Ignores deprecation warnings for upcoming styleFrom button API changes.
+
+## 0.9.8
+
+* Switches to internal method channel implementation.
+
+## 0.9.7+1
+
+* Splits from `camera` as a federated implementation.
diff --git a/packages/camera/camera_android/LICENSE b/packages/camera/camera_android/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/camera/camera_android/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/camera/camera_android/README.md b/packages/camera/camera_android/README.md
new file mode 100644
index 0000000..de8897c
--- /dev/null
+++ b/packages/camera/camera_android/README.md
@@ -0,0 +1,11 @@
+# camera\_android
+
+The Android implementation of [`camera`][1].
+
+## Usage
+
+This package is [endorsed][2], which means you can simply use `camera`
+normally. This package will be automatically included in your app when you do.
+
+[1]: https://pub.dev/packages/camera
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle
new file mode 100644
index 0000000..9c403e0
--- /dev/null
+++ b/packages/camera/camera_android/android/build.gradle
@@ -0,0 +1,66 @@
+group 'io.flutter.plugins.camera'
+version '1.0-SNAPSHOT'
+def args = ["-Xlint:deprecation","-Xlint:unchecked"]
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.0.2'
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+project.getTasks().withType(JavaCompile){
+ options.compilerArgs.addAll(args)
+}
+
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ targetSdkVersion 31
+ minSdkVersion 21
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ lintOptions {
+ disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency'
+ baseline file("lint-baseline.xml")
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+
+ testOptions {
+ unitTests.includeAndroidResources = true
+ unitTests.returnDefaultValues = true
+ unitTests.all {
+ testLogging {
+ events "passed", "skipped", "failed", "standardOut", "standardError"
+ outputs.upToDateWhen {false}
+ showStandardStreams = true
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.annotation:annotation:1.5.0'
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-inline:5.0.0'
+ testImplementation 'androidx.test:core:1.4.0'
+ testImplementation 'org.robolectric:robolectric:4.5'
+}
diff --git a/packages/camera/camera_android/android/lint-baseline.xml b/packages/camera/camera_android/android/lint-baseline.xml
new file mode 100644
index 0000000..4ddaafa
--- /dev/null
+++ b/packages/camera/camera_android/android/lint-baseline.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 3.5.0" client="gradle" variant="debug" version="3.5.0">
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+ line="73"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (x >= 0 && x <= 1);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+ line="74"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (y >= 0 && y <= 1);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java"
+ line="75"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (maxBoundaries == null || maxBoundaries.getWidth() > 0);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+ line="16"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (maxBoundaries == null || maxBoundaries.getHeight() > 0);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+ line="17"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (x >= 0 && x <= 1);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+ line="50"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Assert"
+ message="Assertions are unreliable in Dalvik and unimplemented in ART. Use `BuildConfig.DEBUG` conditional checks instead."
+ errorLine1=" assert (y >= 0 && y <= 1);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraRegions.java"
+ line="51"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="SwitchIntDef"
+ message="Switch statement on an `int` with known associated constant missing case `Configuration.ORIENTATION_SQUARE`, `Configuration.ORIENTATION_UNDEFINED`"
+ errorLine1=" switch (orientation) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java"
+ line="143"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="SwitchIntDef"
+ message="Switch statement on an `int` with known associated constant missing case `Configuration.ORIENTATION_SQUARE`, `Configuration.ORIENTATION_UNDEFINED`"
+ errorLine1=" switch (orientation) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java"
+ line="264"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is never < 21"
+ errorLine1=" if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/io/flutter/plugins/camera/CameraPlugin.java"
+ line="102"
+ column="9"/>
+ </issue>
+
+</issues>
diff --git a/packages/camera/camera_android/android/settings.gradle b/packages/camera/camera_android/android/settings.gradle
new file mode 100644
index 0000000..94a1bae
--- /dev/null
+++ b/packages/camera/camera_android/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'camera_android'
diff --git a/packages/camera/camera_android/android/src/main/AndroidManifest.xml b/packages/camera/camera_android/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d80d364
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="io.flutter.plugins.camera">
+ <uses-permission android:name="android.permission.CAMERA"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+</manifest>
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java
new file mode 100644
index 0000000..b02d686
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -0,0 +1,1273 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.media.Image;
+import android.media.ImageReader;
+import android.media.MediaRecorder;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugins.camera.features.CameraFeature;
+import io.flutter.plugins.camera.features.CameraFeatureFactory;
+import io.flutter.plugins.camera.features.CameraFeatures;
+import io.flutter.plugins.camera.features.Point;
+import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
+import io.flutter.plugins.camera.features.autofocus.FocusMode;
+import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature;
+import io.flutter.plugins.camera.features.exposurelock.ExposureMode;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature;
+import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature;
+import io.flutter.plugins.camera.features.flash.FlashFeature;
+import io.flutter.plugins.camera.features.flash.FlashMode;
+import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
+import io.flutter.plugins.camera.media.MediaRecorderBuilder;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
+import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
+import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executors;
+
+@FunctionalInterface
+interface ErrorCallback {
+ void onError(String errorCode, String errorMessage);
+}
+
+/** A mockable wrapper for CameraDevice calls. */
+interface CameraDeviceWrapper {
+ @NonNull
+ CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException;
+
+ @TargetApi(VERSION_CODES.P)
+ void createCaptureSession(SessionConfiguration config) throws CameraAccessException;
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ void createCaptureSession(
+ @NonNull List<Surface> outputs,
+ @NonNull CameraCaptureSession.StateCallback callback,
+ @Nullable Handler handler)
+ throws CameraAccessException;
+
+ void close();
+}
+
+class Camera
+ implements CameraCaptureCallback.CameraCaptureStateListener,
+ ImageReader.OnImageAvailableListener {
+ private static final String TAG = "Camera";
+
+ private static final HashMap<String, Integer> supportedImageFormats;
+
+ // Current supported outputs.
+ static {
+ supportedImageFormats = new HashMap<>();
+ supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888);
+ supportedImageFormats.put("jpeg", ImageFormat.JPEG);
+ }
+
+ /**
+ * Holds all of the camera features/settings and will be used to update the request builder when
+ * one changes.
+ */
+ private final CameraFeatures cameraFeatures;
+
+ private final SurfaceTextureEntry flutterTexture;
+ private final boolean enableAudio;
+ private final Context applicationContext;
+ private final DartMessenger dartMessenger;
+ private final CameraProperties cameraProperties;
+ private final CameraFeatureFactory cameraFeatureFactory;
+ private final Activity activity;
+ /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */
+ private final CameraCaptureCallback cameraCaptureCallback;
+ /** A {@link Handler} for running tasks in the background. */
+ private Handler backgroundHandler;
+
+ /** An additional thread for running tasks that shouldn't block the UI. */
+ private HandlerThread backgroundHandlerThread;
+
+ private CameraDeviceWrapper cameraDevice;
+ private CameraCaptureSession captureSession;
+ private ImageReader pictureImageReader;
+ private ImageReader imageStreamReader;
+ /** {@link CaptureRequest.Builder} for the camera preview */
+ private CaptureRequest.Builder previewRequestBuilder;
+
+ private MediaRecorder mediaRecorder;
+ /** True when recording video. */
+ private boolean recordingVideo;
+ /** True when the preview is paused. */
+ private boolean pausedPreview;
+
+ private File captureFile;
+
+ /** Holds the current capture timeouts */
+ private CaptureTimeoutsWrapper captureTimeouts;
+ /** Holds the last known capture properties */
+ private CameraCaptureProperties captureProps;
+
+ private MethodChannel.Result flutterResult;
+
+ /** A CameraDeviceWrapper implementation that forwards calls to a CameraDevice. */
+ private class DefaultCameraDeviceWrapper implements CameraDeviceWrapper {
+ private final CameraDevice cameraDevice;
+
+ private DefaultCameraDeviceWrapper(CameraDevice cameraDevice) {
+ this.cameraDevice = cameraDevice;
+ }
+
+ @NonNull
+ @Override
+ public CaptureRequest.Builder createCaptureRequest(int templateType)
+ throws CameraAccessException {
+ return cameraDevice.createCaptureRequest(templateType);
+ }
+
+ @TargetApi(VERSION_CODES.P)
+ @Override
+ public void createCaptureSession(SessionConfiguration config) throws CameraAccessException {
+ cameraDevice.createCaptureSession(config);
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ @SuppressWarnings("deprecation")
+ @Override
+ public void createCaptureSession(
+ @NonNull List<Surface> outputs,
+ @NonNull CameraCaptureSession.StateCallback callback,
+ @Nullable Handler handler)
+ throws CameraAccessException {
+ cameraDevice.createCaptureSession(outputs, callback, backgroundHandler);
+ }
+
+ @Override
+ public void close() {
+ cameraDevice.close();
+ }
+ }
+
+ public Camera(
+ final Activity activity,
+ final SurfaceTextureEntry flutterTexture,
+ final CameraFeatureFactory cameraFeatureFactory,
+ final DartMessenger dartMessenger,
+ final CameraProperties cameraProperties,
+ final ResolutionPreset resolutionPreset,
+ final boolean enableAudio) {
+
+ if (activity == null) {
+ throw new IllegalStateException("No activity available!");
+ }
+ this.activity = activity;
+ this.enableAudio = enableAudio;
+ this.flutterTexture = flutterTexture;
+ this.dartMessenger = dartMessenger;
+ this.applicationContext = activity.getApplicationContext();
+ this.cameraProperties = cameraProperties;
+ this.cameraFeatureFactory = cameraFeatureFactory;
+ this.cameraFeatures =
+ CameraFeatures.init(
+ cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset);
+
+ // Create capture callback.
+ captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000);
+ captureProps = new CameraCaptureProperties();
+ cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts, captureProps);
+
+ startBackgroundThread();
+ }
+
+ @Override
+ public void onConverged() {
+ takePictureAfterPrecapture();
+ }
+
+ @Override
+ public void onPrecapture() {
+ runPrecaptureSequence();
+ }
+
+ /**
+ * Updates the builder settings with all of the available features.
+ *
+ * @param requestBuilder request builder to update.
+ */
+ private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) {
+ for (CameraFeature feature : cameraFeatures.getAllFeatures()) {
+ Log.d(TAG, "Updating builder with feature: " + feature.getDebugName());
+ feature.updateBuilder(requestBuilder);
+ }
+ }
+
+ private void prepareMediaRecorder(String outputFilePath) throws IOException {
+ Log.i(TAG, "prepareMediaRecorder");
+
+ if (mediaRecorder != null) {
+ mediaRecorder.release();
+ }
+
+ final PlatformChannel.DeviceOrientation lockedOrientation =
+ ((SensorOrientationFeature) cameraFeatures.getSensorOrientation())
+ .getLockedCaptureOrientation();
+
+ MediaRecorderBuilder mediaRecorderBuilder;
+
+ // TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is null
+ // once this has largely been fixed on the Android side. https://github.com/flutter/flutter/issues/119668
+ EncoderProfiles recordingProfile = getRecordingProfile();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && recordingProfile != null) {
+ mediaRecorderBuilder = new MediaRecorderBuilder(recordingProfile, outputFilePath);
+ } else {
+ mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath);
+ }
+
+ mediaRecorder =
+ mediaRecorderBuilder
+ .setEnableAudio(enableAudio)
+ .setMediaOrientation(
+ lockedOrientation == null
+ ? getDeviceOrientationManager().getVideoOrientation()
+ : getDeviceOrientationManager().getVideoOrientation(lockedOrientation))
+ .build();
+ }
+
+ @SuppressLint("MissingPermission")
+ public void open(String imageFormatGroup) throws CameraAccessException {
+ final ResolutionFeature resolutionFeature = cameraFeatures.getResolution();
+
+ if (!resolutionFeature.checkIsSupported()) {
+ // Tell the user that the camera they are trying to open is not supported,
+ // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name
+ // not being a valid parsable integer.
+ dartMessenger.sendCameraErrorEvent(
+ "Camera with name \""
+ + cameraProperties.getCameraName()
+ + "\" is not supported by this plugin.");
+ return;
+ }
+
+ // Always capture using JPEG format.
+ pictureImageReader =
+ ImageReader.newInstance(
+ resolutionFeature.getCaptureSize().getWidth(),
+ resolutionFeature.getCaptureSize().getHeight(),
+ ImageFormat.JPEG,
+ 1);
+
+ // For image streaming, use the provided image format or fall back to YUV420.
+ Integer imageFormat = supportedImageFormats.get(imageFormatGroup);
+ if (imageFormat == null) {
+ Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420");
+ imageFormat = ImageFormat.YUV_420_888;
+ }
+ imageStreamReader =
+ ImageReader.newInstance(
+ resolutionFeature.getPreviewSize().getWidth(),
+ resolutionFeature.getPreviewSize().getHeight(),
+ imageFormat,
+ 1);
+
+ // Open the camera.
+ CameraManager cameraManager = CameraUtils.getCameraManager(activity);
+ cameraManager.openCamera(
+ cameraProperties.getCameraName(),
+ new CameraDevice.StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice device) {
+ cameraDevice = new DefaultCameraDeviceWrapper(device);
+ try {
+ startPreview();
+ dartMessenger.sendCameraInitializedEvent(
+ resolutionFeature.getPreviewSize().getWidth(),
+ resolutionFeature.getPreviewSize().getHeight(),
+ cameraFeatures.getExposureLock().getValue(),
+ cameraFeatures.getAutoFocus().getValue(),
+ cameraFeatures.getExposurePoint().checkIsSupported(),
+ cameraFeatures.getFocusPoint().checkIsSupported());
+ } catch (CameraAccessException e) {
+ dartMessenger.sendCameraErrorEvent(e.getMessage());
+ close();
+ }
+ }
+
+ @Override
+ public void onClosed(@NonNull CameraDevice camera) {
+ Log.i(TAG, "open | onClosed");
+
+ // Prevents calls to methods that would otherwise result in IllegalStateException exceptions.
+ cameraDevice = null;
+ closeCaptureSession();
+ dartMessenger.sendCameraClosingEvent();
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+ Log.i(TAG, "open | onDisconnected");
+
+ close();
+ dartMessenger.sendCameraErrorEvent("The camera was disconnected.");
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
+ Log.i(TAG, "open | onError");
+
+ close();
+ String errorDescription;
+ switch (errorCode) {
+ case ERROR_CAMERA_IN_USE:
+ errorDescription = "The camera device is in use already.";
+ break;
+ case ERROR_MAX_CAMERAS_IN_USE:
+ errorDescription = "Max cameras in use";
+ break;
+ case ERROR_CAMERA_DISABLED:
+ errorDescription = "The camera device could not be opened due to a device policy.";
+ break;
+ case ERROR_CAMERA_DEVICE:
+ errorDescription = "The camera device has encountered a fatal error";
+ break;
+ case ERROR_CAMERA_SERVICE:
+ errorDescription = "The camera service has encountered a fatal error.";
+ break;
+ default:
+ errorDescription = "Unknown camera error";
+ }
+ dartMessenger.sendCameraErrorEvent(errorDescription);
+ }
+ },
+ backgroundHandler);
+ }
+
+ @VisibleForTesting
+ void createCaptureSession(int templateType, Surface... surfaces) throws CameraAccessException {
+ createCaptureSession(templateType, null, surfaces);
+ }
+
+ private void createCaptureSession(
+ int templateType, Runnable onSuccessCallback, Surface... surfaces)
+ throws CameraAccessException {
+ // Close any existing capture session.
+ captureSession = null;
+
+ // Create a new capture builder.
+ previewRequestBuilder = cameraDevice.createCaptureRequest(templateType);
+
+ // Build Flutter surface to render to.
+ ResolutionFeature resolutionFeature = cameraFeatures.getResolution();
+ SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture();
+ surfaceTexture.setDefaultBufferSize(
+ resolutionFeature.getPreviewSize().getWidth(),
+ resolutionFeature.getPreviewSize().getHeight());
+ Surface flutterSurface = new Surface(surfaceTexture);
+ previewRequestBuilder.addTarget(flutterSurface);
+
+ List<Surface> remainingSurfaces = Arrays.asList(surfaces);
+ if (templateType != CameraDevice.TEMPLATE_PREVIEW) {
+ // If it is not preview mode, add all surfaces as targets.
+ for (Surface surface : remainingSurfaces) {
+ previewRequestBuilder.addTarget(surface);
+ }
+ }
+
+ // Update camera regions.
+ Size cameraBoundaries =
+ CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder);
+ cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries);
+ cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries);
+
+ // Prepare the callback.
+ CameraCaptureSession.StateCallback callback =
+ new CameraCaptureSession.StateCallback() {
+ boolean captureSessionClosed = false;
+
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession session) {
+ Log.i(TAG, "CameraCaptureSession onConfigured");
+ // Camera was already closed.
+ if (cameraDevice == null || captureSessionClosed) {
+ dartMessenger.sendCameraErrorEvent("The camera was closed during configuration.");
+ return;
+ }
+ captureSession = session;
+
+ Log.i(TAG, "Updating builder settings");
+ updateBuilderSettings(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
+ }
+
+ @Override
+ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
+ Log.i(TAG, "CameraCaptureSession onConfigureFailed");
+ dartMessenger.sendCameraErrorEvent("Failed to configure camera session.");
+ }
+
+ @Override
+ public void onClosed(@NonNull CameraCaptureSession session) {
+ Log.i(TAG, "CameraCaptureSession onClosed");
+ captureSessionClosed = true;
+ }
+ };
+
+ // Start the session.
+ if (VERSION.SDK_INT >= VERSION_CODES.P) {
+ // Collect all surfaces to render to.
+ List<OutputConfiguration> configs = new ArrayList<>();
+ configs.add(new OutputConfiguration(flutterSurface));
+ for (Surface surface : remainingSurfaces) {
+ configs.add(new OutputConfiguration(surface));
+ }
+ createCaptureSessionWithSessionConfig(configs, callback);
+ } else {
+ // Collect all surfaces to render to.
+ List<Surface> surfaceList = new ArrayList<>();
+ surfaceList.add(flutterSurface);
+ surfaceList.addAll(remainingSurfaces);
+ createCaptureSession(surfaceList, callback);
+ }
+ }
+
+ @TargetApi(VERSION_CODES.P)
+ private void createCaptureSessionWithSessionConfig(
+ List<OutputConfiguration> outputConfigs, CameraCaptureSession.StateCallback callback)
+ throws CameraAccessException {
+ cameraDevice.createCaptureSession(
+ new SessionConfiguration(
+ SessionConfiguration.SESSION_REGULAR,
+ outputConfigs,
+ Executors.newSingleThreadExecutor(),
+ callback));
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ @SuppressWarnings("deprecation")
+ private void createCaptureSession(
+ List<Surface> surfaces, CameraCaptureSession.StateCallback callback)
+ throws CameraAccessException {
+ cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler);
+ }
+
+ // Send a repeating request to refresh capture session.
+ private void refreshPreviewCaptureSession(
+ @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) {
+ Log.i(TAG, "refreshPreviewCaptureSession");
+
+ if (captureSession == null) {
+ Log.i(
+ TAG,
+ "refreshPreviewCaptureSession: captureSession not yet initialized, "
+ + "skipping preview capture session refresh.");
+ return;
+ }
+
+ try {
+ if (!pausedPreview) {
+ captureSession.setRepeatingRequest(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+ }
+
+ if (onSuccessCallback != null) {
+ onSuccessCallback.run();
+ }
+
+ } catch (IllegalStateException e) {
+ onErrorCallback.onError("cameraAccess", "Camera is closed: " + e.getMessage());
+ } catch (CameraAccessException e) {
+ onErrorCallback.onError("cameraAccess", e.getMessage());
+ }
+ }
+
+ private void startCapture(boolean record, boolean stream) throws CameraAccessException {
+ List<Surface> surfaces = new ArrayList<>();
+ Runnable successCallback = null;
+ if (record) {
+ surfaces.add(mediaRecorder.getSurface());
+ successCallback = () -> mediaRecorder.start();
+ }
+ if (stream) {
+ surfaces.add(imageStreamReader.getSurface());
+ }
+
+ createCaptureSession(
+ CameraDevice.TEMPLATE_RECORD, successCallback, surfaces.toArray(new Surface[0]));
+ }
+
+ public void takePicture(@NonNull final Result result) {
+ // Only take one picture at a time.
+ if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) {
+ result.error("captureAlreadyActive", "Picture is currently already being captured", null);
+ return;
+ }
+
+ flutterResult = result;
+
+ // Create temporary file.
+ final File outputDir = applicationContext.getCacheDir();
+ try {
+ captureFile = File.createTempFile("CAP", ".jpg", outputDir);
+ captureTimeouts.reset();
+ } catch (IOException | SecurityException e) {
+ dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null);
+ return;
+ }
+
+ // Listen for picture being taken.
+ pictureImageReader.setOnImageAvailableListener(this, backgroundHandler);
+
+ final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus();
+ final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported();
+ if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) {
+ runPictureAutoFocus();
+ } else {
+ runPrecaptureSequence();
+ }
+ }
+
+ /**
+ * Run the precapture sequence for capturing a still image. This method should be called when a
+ * response is received in {@link #cameraCaptureCallback} from lockFocus().
+ */
+ private void runPrecaptureSequence() {
+ Log.i(TAG, "runPrecaptureSequence");
+ try {
+ // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START.
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
+ captureSession.capture(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+
+ // Repeating request to refresh preview session.
+ refreshPreviewCaptureSession(
+ null,
+ (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null));
+
+ // Start precapture.
+ cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START);
+
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+
+ // Trigger one capture to start AE sequence.
+ captureSession.capture(
+ previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+
+ } catch (CameraAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Capture a still picture. This method should be called when a response is received {@link
+ * #cameraCaptureCallback} from both lockFocus().
+ */
+ private void takePictureAfterPrecapture() {
+ Log.i(TAG, "captureStillPicture");
+ cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING);
+
+ if (cameraDevice == null) {
+ return;
+ }
+ // This is the CaptureRequest.Builder that is used to take a picture.
+ CaptureRequest.Builder stillBuilder;
+ try {
+ stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ } catch (CameraAccessException e) {
+ dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null);
+ return;
+ }
+ stillBuilder.addTarget(pictureImageReader.getSurface());
+
+ // Zoom.
+ stillBuilder.set(
+ CaptureRequest.SCALER_CROP_REGION,
+ previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION));
+
+ // Have all features update the builder.
+ updateBuilderSettings(stillBuilder);
+
+ // Orientation.
+ final PlatformChannel.DeviceOrientation lockedOrientation =
+ ((SensorOrientationFeature) cameraFeatures.getSensorOrientation())
+ .getLockedCaptureOrientation();
+ stillBuilder.set(
+ CaptureRequest.JPEG_ORIENTATION,
+ lockedOrientation == null
+ ? getDeviceOrientationManager().getPhotoOrientation()
+ : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation));
+
+ CameraCaptureSession.CaptureCallback captureCallback =
+ new CameraCaptureSession.CaptureCallback() {
+ @Override
+ public void onCaptureCompleted(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ unlockAutoFocus();
+ }
+ };
+
+ try {
+ captureSession.stopRepeating();
+ Log.i(TAG, "sending capture request");
+ captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler);
+ } catch (CameraAccessException e) {
+ dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private Display getDefaultDisplay() {
+ return activity.getWindowManager().getDefaultDisplay();
+ }
+
+ /** Starts a background thread and its {@link Handler}. */
+ public void startBackgroundThread() {
+ if (backgroundHandlerThread != null) {
+ return;
+ }
+
+ backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground");
+ try {
+ backgroundHandlerThread.start();
+ } catch (IllegalThreadStateException e) {
+ // Ignore exception in case the thread has already started.
+ }
+ backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper());
+ }
+
+ /** Stops the background thread and its {@link Handler}. */
+ public void stopBackgroundThread() {
+ if (backgroundHandlerThread != null) {
+ backgroundHandlerThread.quitSafely();
+ }
+ backgroundHandlerThread = null;
+ backgroundHandler = null;
+ }
+
+ /** Start capturing a picture, doing autofocus first. */
+ private void runPictureAutoFocus() {
+ Log.i(TAG, "runPictureAutoFocus");
+
+ cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS);
+ lockAutoFocus();
+ }
+
+ private void lockAutoFocus() {
+ Log.i(TAG, "lockAutoFocus");
+ if (captureSession == null) {
+ Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
+ return;
+ }
+
+ // Trigger AF to start.
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
+
+ try {
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ dartMessenger.sendCameraErrorEvent(e.getMessage());
+ }
+ }
+
+ /** Cancel and reset auto focus state and refresh the preview session. */
+ private void unlockAutoFocus() {
+ Log.i(TAG, "unlockAutoFocus");
+ if (captureSession == null) {
+ Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
+ return;
+ }
+ try {
+ // Cancel existing AF state.
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+
+ // Set AF state to idle again.
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
+
+ captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ dartMessenger.sendCameraErrorEvent(e.getMessage());
+ return;
+ }
+
+ refreshPreviewCaptureSession(
+ null,
+ (errorCode, errorMessage) ->
+ dartMessenger.error(flutterResult, errorCode, errorMessage, null));
+ }
+
+ public void startVideoRecording(
+ @NonNull Result result, @Nullable EventChannel imageStreamChannel) {
+ prepareRecording(result);
+
+ if (imageStreamChannel != null) {
+ setStreamHandler(imageStreamChannel);
+ }
+
+ recordingVideo = true;
+ try {
+ startCapture(true, imageStreamChannel != null);
+ result.success(null);
+ } catch (CameraAccessException e) {
+ recordingVideo = false;
+ captureFile = null;
+ result.error("videoRecordingFailed", e.getMessage(), null);
+ }
+ }
+
+ public void stopVideoRecording(@NonNull final Result result) {
+ if (!recordingVideo) {
+ result.success(null);
+ return;
+ }
+ // Re-create autofocus feature so it's using continuous capture focus mode now.
+ cameraFeatures.setAutoFocus(
+ cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false));
+ recordingVideo = false;
+ try {
+ captureSession.abortCaptures();
+ mediaRecorder.stop();
+ } catch (CameraAccessException | IllegalStateException e) {
+ // Ignore exceptions and try to continue (changes are camera session already aborted capture).
+ }
+ mediaRecorder.reset();
+ try {
+ startPreview();
+ } catch (CameraAccessException | IllegalStateException e) {
+ result.error("videoRecordingFailed", e.getMessage(), null);
+ return;
+ }
+ result.success(captureFile.getAbsolutePath());
+ captureFile = null;
+ }
+
+ public void pauseVideoRecording(@NonNull final Result result) {
+ if (!recordingVideo) {
+ result.success(null);
+ return;
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mediaRecorder.pause();
+ } else {
+ result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null);
+ return;
+ }
+ } catch (IllegalStateException e) {
+ result.error("videoRecordingFailed", e.getMessage(), null);
+ return;
+ }
+
+ result.success(null);
+ }
+
+ public void resumeVideoRecording(@NonNull final Result result) {
+ if (!recordingVideo) {
+ result.success(null);
+ return;
+ }
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mediaRecorder.resume();
+ } else {
+ result.error(
+ "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null);
+ return;
+ }
+ } catch (IllegalStateException e) {
+ result.error("videoRecordingFailed", e.getMessage(), null);
+ return;
+ }
+
+ result.success(null);
+ }
+
+ /**
+ * Method handler for setting new flash modes.
+ *
+ * @param result Flutter result.
+ * @param newMode new mode.
+ */
+ public void setFlashMode(@NonNull final Result result, @NonNull FlashMode newMode) {
+ // Save the new flash mode setting.
+ final FlashFeature flashFeature = cameraFeatures.getFlash();
+ flashFeature.setValue(newMode);
+ flashFeature.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null));
+ }
+
+ /**
+ * Method handler for setting new exposure modes.
+ *
+ * @param result Flutter result.
+ * @param newMode new mode.
+ */
+ public void setExposureMode(@NonNull final Result result, @NonNull ExposureMode newMode) {
+ final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock();
+ exposureLockFeature.setValue(newMode);
+ exposureLockFeature.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) ->
+ result.error("setExposureModeFailed", "Could not set exposure mode.", null));
+ }
+
+ /**
+ * Sets new exposure point from dart.
+ *
+ * @param result Flutter result.
+ * @param point The exposure point.
+ */
+ public void setExposurePoint(@NonNull final Result result, @Nullable Point point) {
+ final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint();
+ exposurePointFeature.setValue(point);
+ exposurePointFeature.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) ->
+ result.error("setExposurePointFailed", "Could not set exposure point.", null));
+ }
+
+ /** Return the max exposure offset value supported by the camera to dart. */
+ public double getMaxExposureOffset() {
+ return cameraFeatures.getExposureOffset().getMaxExposureOffset();
+ }
+
+ /** Return the min exposure offset value supported by the camera to dart. */
+ public double getMinExposureOffset() {
+ return cameraFeatures.getExposureOffset().getMinExposureOffset();
+ }
+
+ /** Return the exposure offset step size to dart. */
+ public double getExposureOffsetStepSize() {
+ return cameraFeatures.getExposureOffset().getExposureOffsetStepSize();
+ }
+
+ /**
+ * Sets new focus mode from dart.
+ *
+ * @param result Flutter result.
+ * @param newMode New mode.
+ */
+ public void setFocusMode(final Result result, @NonNull FocusMode newMode) {
+ final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus();
+ autoFocusFeature.setValue(newMode);
+ autoFocusFeature.updateBuilder(previewRequestBuilder);
+
+ /*
+ * For focus mode an extra step of actually locking/unlocking the
+ * focus has to be done, in order to ensure it goes into the correct state.
+ */
+ if (!pausedPreview) {
+ switch (newMode) {
+ case locked:
+ // Perform a single focus trigger.
+ if (captureSession == null) {
+ Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
+ return;
+ }
+ lockAutoFocus();
+
+ // Set AF state to idle again.
+ previewRequestBuilder.set(
+ CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
+
+ try {
+ captureSession.setRepeatingRequest(
+ previewRequestBuilder.build(), null, backgroundHandler);
+ } catch (CameraAccessException e) {
+ if (result != null) {
+ result.error(
+ "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
+ }
+ return;
+ }
+ break;
+ case auto:
+ // Cancel current AF trigger and set AF to idle again.
+ unlockAutoFocus();
+ break;
+ }
+ }
+
+ if (result != null) {
+ result.success(null);
+ }
+ }
+
+ /**
+ * Sets new focus point from dart.
+ *
+ * @param result Flutter result.
+ * @param point the new coordinates.
+ */
+ public void setFocusPoint(@NonNull final Result result, @Nullable Point point) {
+ final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint();
+ focusPointFeature.setValue(point);
+ focusPointFeature.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null));
+
+ this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue());
+ }
+
+ /**
+ * Sets a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or
+ * -1.3.
+ *
+ * @param result flutter result.
+ * @param offset new value.
+ */
+ public void setExposureOffset(@NonNull final Result result, double offset) {
+ final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset();
+ exposureOffsetFeature.setValue(offset);
+ exposureOffsetFeature.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(exposureOffsetFeature.getValue()),
+ (code, message) ->
+ result.error("setExposureOffsetFailed", "Could not set exposure offset.", null));
+ }
+
+ public float getMaxZoomLevel() {
+ return cameraFeatures.getZoomLevel().getMaximumZoomLevel();
+ }
+
+ public float getMinZoomLevel() {
+ return cameraFeatures.getZoomLevel().getMinimumZoomLevel();
+ }
+
+ /** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */
+ CamcorderProfile getRecordingProfileLegacy() {
+ return cameraFeatures.getResolution().getRecordingProfileLegacy();
+ }
+
+ EncoderProfiles getRecordingProfile() {
+ return cameraFeatures.getResolution().getRecordingProfile();
+ }
+
+ /** Shortut to get deviceOrientationListener. */
+ DeviceOrientationManager getDeviceOrientationManager() {
+ return cameraFeatures.getSensorOrientation().getDeviceOrientationManager();
+ }
+
+ /**
+ * Sets zoom level from dart.
+ *
+ * @param result Flutter result.
+ * @param zoom new value.
+ */
+ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException {
+ final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel();
+ float maxZoom = zoomLevel.getMaximumZoomLevel();
+ float minZoom = zoomLevel.getMinimumZoomLevel();
+
+ if (zoom > maxZoom || zoom < minZoom) {
+ String errorMessage =
+ String.format(
+ Locale.ENGLISH,
+ "Zoom level out of bounds (zoom level should be between %f and %f).",
+ minZoom,
+ maxZoom);
+ result.error("ZOOM_ERROR", errorMessage, null);
+ return;
+ }
+
+ zoomLevel.setValue(zoom);
+ zoomLevel.updateBuilder(previewRequestBuilder);
+
+ refreshPreviewCaptureSession(
+ () -> result.success(null),
+ (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null));
+ }
+
+ /**
+ * Lock capture orientation from dart.
+ *
+ * @param orientation new orientation.
+ */
+ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) {
+ cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation);
+ }
+
+ /** Unlock capture orientation from dart. */
+ public void unlockCaptureOrientation() {
+ cameraFeatures.getSensorOrientation().unlockCaptureOrientation();
+ }
+
+ /** Pause the preview from dart. */
+ public void pausePreview() throws CameraAccessException {
+ this.pausedPreview = true;
+ this.captureSession.stopRepeating();
+ }
+
+ /** Resume the preview from dart. */
+ public void resumePreview() {
+ this.pausedPreview = false;
+ this.refreshPreviewCaptureSession(
+ null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
+ }
+
+ public void startPreview() throws CameraAccessException {
+ if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
+ Log.i(TAG, "startPreview");
+
+ createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface());
+ }
+
+ public void startPreviewWithImageStream(EventChannel imageStreamChannel)
+ throws CameraAccessException {
+ setStreamHandler(imageStreamChannel);
+
+ startCapture(false, true);
+ Log.i(TAG, "startPreviewWithImageStream");
+ }
+
+ /**
+ * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a
+ * still image is ready to be saved.
+ */
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ Log.i(TAG, "onImageAvailable");
+
+ backgroundHandler.post(
+ new ImageSaver(
+ // Use acquireNextImage since image reader is only for one image.
+ reader.acquireNextImage(),
+ captureFile,
+ new ImageSaver.Callback() {
+ @Override
+ public void onComplete(String absolutePath) {
+ dartMessenger.finish(flutterResult, absolutePath);
+ }
+
+ @Override
+ public void onError(String errorCode, String errorMessage) {
+ dartMessenger.error(flutterResult, errorCode, errorMessage, null);
+ }
+ }));
+ cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW);
+ }
+
+ private void prepareRecording(@NonNull Result result) {
+ final File outputDir = applicationContext.getCacheDir();
+ try {
+ captureFile = File.createTempFile("REC", ".mp4", outputDir);
+ } catch (IOException | SecurityException e) {
+ result.error("cannotCreateFile", e.getMessage(), null);
+ return;
+ }
+ try {
+ prepareMediaRecorder(captureFile.getAbsolutePath());
+ } catch (IOException e) {
+ recordingVideo = false;
+ captureFile = null;
+ result.error("videoRecordingFailed", e.getMessage(), null);
+ return;
+ }
+ // Re-create autofocus feature so it's using video focus mode now.
+ cameraFeatures.setAutoFocus(
+ cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true));
+ }
+
+ private void setStreamHandler(EventChannel imageStreamChannel) {
+ imageStreamChannel.setStreamHandler(
+ new EventChannel.StreamHandler() {
+ @Override
+ public void onListen(Object o, EventChannel.EventSink imageStreamSink) {
+ setImageStreamImageAvailableListener(imageStreamSink);
+ }
+
+ @Override
+ public void onCancel(Object o) {
+ imageStreamReader.setOnImageAvailableListener(null, backgroundHandler);
+ }
+ });
+ }
+
+ private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
+ imageStreamReader.setOnImageAvailableListener(
+ reader -> {
+ Image img = reader.acquireNextImage();
+ // Use acquireNextImage since image reader is only for one image.
+ if (img == null) return;
+
+ List<Map<String, Object>> planes = new ArrayList<>();
+ for (Image.Plane plane : img.getPlanes()) {
+ ByteBuffer buffer = plane.getBuffer();
+
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes, 0, bytes.length);
+
+ Map<String, Object> planeBuffer = new HashMap<>();
+ planeBuffer.put("bytesPerRow", plane.getRowStride());
+ planeBuffer.put("bytesPerPixel", plane.getPixelStride());
+ planeBuffer.put("bytes", bytes);
+
+ planes.add(planeBuffer);
+ }
+
+ Map<String, Object> imageBuffer = new HashMap<>();
+ imageBuffer.put("width", img.getWidth());
+ imageBuffer.put("height", img.getHeight());
+ imageBuffer.put("format", img.getFormat());
+ imageBuffer.put("planes", planes);
+ imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture());
+ imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime());
+ Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity();
+ imageBuffer.put(
+ "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity);
+
+ final Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(() -> imageStreamSink.success(imageBuffer));
+ img.close();
+ },
+ backgroundHandler);
+ }
+
+ private void closeCaptureSession() {
+ if (captureSession != null) {
+ Log.i(TAG, "closeCaptureSession");
+
+ captureSession.close();
+ captureSession = null;
+ }
+ }
+
+ public void close() {
+ Log.i(TAG, "close");
+
+ if (cameraDevice != null) {
+ cameraDevice.close();
+ cameraDevice = null;
+
+ // Closing the CameraDevice without closing the CameraCaptureSession is recommended
+ // for quickly closing the camera:
+ // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close()
+ captureSession = null;
+ } else {
+ closeCaptureSession();
+ }
+
+ if (pictureImageReader != null) {
+ pictureImageReader.close();
+ pictureImageReader = null;
+ }
+ if (imageStreamReader != null) {
+ imageStreamReader.close();
+ imageStreamReader = null;
+ }
+ if (mediaRecorder != null) {
+ mediaRecorder.reset();
+ mediaRecorder.release();
+ mediaRecorder = null;
+ }
+
+ stopBackgroundThread();
+ }
+
+ public void dispose() {
+ Log.i(TAG, "dispose");
+
+ close();
+ flutterTexture.release();
+ getDeviceOrientationManager().stop();
+ }
+
+ /** Factory class that assists in creating a {@link HandlerThread} instance. */
+ static class HandlerThreadFactory {
+ /**
+ * Creates a new instance of the {@link HandlerThread} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param name to give to the HandlerThread.
+ * @return new instance of the {@link HandlerThread} class.
+ */
+ @VisibleForTesting
+ public static HandlerThread create(String name) {
+ return new HandlerThread(name);
+ }
+ }
+
+ /** Factory class that assists in creating a {@link Handler} instance. */
+ static class HandlerFactory {
+ /**
+ * Creates a new instance of the {@link Handler} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param looper to give to the Handler.
+ * @return new instance of the {@link Handler} class.
+ */
+ @VisibleForTesting
+ public static Handler create(Looper looper) {
+ return new Handler(looper);
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java
new file mode 100644
index 0000000..805f182
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java
@@ -0,0 +1,183 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.types.CameraCaptureProperties;
+import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
+
+/**
+ * A callback object for tracking the progress of a {@link android.hardware.camera2.CaptureRequest}
+ * submitted to the camera device.
+ */
+class CameraCaptureCallback extends CaptureCallback {
+ private static final String TAG = "CameraCaptureCallback";
+ private final CameraCaptureStateListener cameraStateListener;
+ private CameraState cameraState;
+ private final CaptureTimeoutsWrapper captureTimeouts;
+ private final CameraCaptureProperties captureProps;
+
+ private CameraCaptureCallback(
+ @NonNull CameraCaptureStateListener cameraStateListener,
+ @NonNull CaptureTimeoutsWrapper captureTimeouts,
+ @NonNull CameraCaptureProperties captureProps) {
+ cameraState = CameraState.STATE_PREVIEW;
+ this.cameraStateListener = cameraStateListener;
+ this.captureTimeouts = captureTimeouts;
+ this.captureProps = captureProps;
+ }
+
+ /**
+ * Creates a new instance of the {@link CameraCaptureCallback} class.
+ *
+ * @param cameraStateListener instance which will be called when the camera state changes.
+ * @param captureTimeouts specifying the different timeout counters that should be taken into
+ * account.
+ * @return a configured instance of the {@link CameraCaptureCallback} class.
+ */
+ public static CameraCaptureCallback create(
+ @NonNull CameraCaptureStateListener cameraStateListener,
+ @NonNull CaptureTimeoutsWrapper captureTimeouts,
+ @NonNull CameraCaptureProperties captureProps) {
+ return new CameraCaptureCallback(cameraStateListener, captureTimeouts, captureProps);
+ }
+
+ /**
+ * Gets the current {@link CameraState}.
+ *
+ * @return the current {@link CameraState}.
+ */
+ public CameraState getCameraState() {
+ return cameraState;
+ }
+
+ /**
+ * Sets the {@link CameraState}.
+ *
+ * @param state the camera is currently in.
+ */
+ public void setCameraState(@NonNull CameraState state) {
+ cameraState = state;
+ }
+
+ private void process(CaptureResult result) {
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+
+ // Update capture properties
+ if (result instanceof TotalCaptureResult) {
+ Float lensAperture = result.get(CaptureResult.LENS_APERTURE);
+ Long sensorExposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);
+ Integer sensorSensitivity = result.get(CaptureResult.SENSOR_SENSITIVITY);
+ this.captureProps.setLastLensAperture(lensAperture);
+ this.captureProps.setLastSensorExposureTime(sensorExposureTime);
+ this.captureProps.setLastSensorSensitivity(sensorSensitivity);
+ }
+
+ if (cameraState != CameraState.STATE_PREVIEW) {
+ Log.d(
+ TAG,
+ "CameraCaptureCallback | state: "
+ + cameraState
+ + " | afState: "
+ + afState
+ + " | aeState: "
+ + aeState);
+ }
+
+ switch (cameraState) {
+ case STATE_PREVIEW:
+ {
+ // We have nothing to do when the camera preview is working normally.
+ break;
+ }
+ case STATE_WAITING_FOCUS:
+ {
+ if (afState == null) {
+ return;
+ } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+ || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+ handleWaitingFocusState(aeState);
+ } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) {
+ Log.w(TAG, "Focus timeout, moving on with capture");
+ handleWaitingFocusState(aeState);
+ }
+
+ break;
+ }
+ case STATE_WAITING_PRECAPTURE_START:
+ {
+ // CONTROL_AE_STATE can be null on some devices
+ if (aeState == null
+ || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED
+ || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE
+ || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) {
+ setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE);
+ } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) {
+ Log.w(TAG, "Metering timeout waiting for pre-capture to start, moving on with capture");
+
+ setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE);
+ }
+ break;
+ }
+ case STATE_WAITING_PRECAPTURE_DONE:
+ {
+ // CONTROL_AE_STATE can be null on some devices
+ if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
+ cameraStateListener.onConverged();
+ } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) {
+ Log.w(
+ TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture");
+ cameraStateListener.onConverged();
+ }
+
+ break;
+ }
+ }
+ }
+
+ private void handleWaitingFocusState(Integer aeState) {
+ // CONTROL_AE_STATE can be null on some devices
+ if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) {
+ cameraStateListener.onConverged();
+ } else {
+ cameraStateListener.onPrecapture();
+ }
+ }
+
+ @Override
+ public void onCaptureProgressed(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureResult partialResult) {
+ process(partialResult);
+ }
+
+ @Override
+ public void onCaptureCompleted(
+ @NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ process(result);
+ }
+
+ /** An interface that describes the different state changes implementers can be informed about. */
+ interface CameraCaptureStateListener {
+
+ /** Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. */
+ void onConverged();
+
+ /**
+ * Called when the {@link android.hardware.camera2.CaptureRequest} enters the pre-capture state.
+ */
+ void onPrecapture();
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java
new file mode 100644
index 0000000..ee8fa5a
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java
@@ -0,0 +1,120 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+final class CameraPermissions {
+ interface PermissionsRegistry {
+ @SuppressWarnings("deprecation")
+ void addListener(
+ io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler);
+ }
+
+ interface ResultCallback {
+ void onResult(String errorCode, String errorDescription);
+ }
+
+ /**
+ * Camera access permission errors handled when camera is created. See {@code MethodChannelCamera}
+ * in {@code camera/camera_platform_interface} for details.
+ */
+ private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING =
+ "CameraPermissionsRequestOngoing";
+
+ private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE =
+ "Another request is ongoing and multiple requests cannot be handled at once.";
+ private static final String CAMERA_ACCESS_DENIED = "CameraAccessDenied";
+ private static final String CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied.";
+ private static final String AUDIO_ACCESS_DENIED = "AudioAccessDenied";
+ private static final String AUDIO_ACCESS_DENIED_MESSAGE = "Audio access permission was denied.";
+
+ private static final int CAMERA_REQUEST_ID = 9796;
+ @VisibleForTesting boolean ongoing = false;
+
+ void requestPermissions(
+ Activity activity,
+ PermissionsRegistry permissionsRegistry,
+ boolean enableAudio,
+ ResultCallback callback) {
+ if (ongoing) {
+ callback.onResult(
+ CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE);
+ return;
+ }
+ if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) {
+ permissionsRegistry.addListener(
+ new CameraRequestPermissionsListener(
+ (String errorCode, String errorDescription) -> {
+ ongoing = false;
+ callback.onResult(errorCode, errorDescription);
+ }));
+ ongoing = true;
+ ActivityCompat.requestPermissions(
+ activity,
+ enableAudio
+ ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}
+ : new String[] {Manifest.permission.CAMERA},
+ CAMERA_REQUEST_ID);
+ } else {
+ // Permissions already exist. Call the callback with success.
+ callback.onResult(null, null);
+ }
+ }
+
+ private boolean hasCameraPermission(Activity activity) {
+ return ContextCompat.checkSelfPermission(activity, permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private boolean hasAudioPermission(Activity activity) {
+ return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ @VisibleForTesting
+ @SuppressWarnings("deprecation")
+ static final class CameraRequestPermissionsListener
+ implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener {
+
+ // There's no way to unregister permission listeners in the v1 embedding, so we'll be called
+ // duplicate times in cases where the user denies and then grants a permission. Keep track of if
+ // we've responded before and bail out of handling the callback manually if this is a repeat
+ // call.
+ boolean alreadyCalled = false;
+
+ final ResultCallback callback;
+
+ @VisibleForTesting
+ CameraRequestPermissionsListener(ResultCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) {
+ if (alreadyCalled || id != CAMERA_REQUEST_ID) {
+ return false;
+ }
+
+ alreadyCalled = true;
+ // grantResults could be empty if the permissions request with the user is interrupted
+ // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
+ if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE);
+ } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) {
+ callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE);
+ } else {
+ callback.onResult(null, null);
+ }
+ return true;
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java
new file mode 100644
index 0000000..067ed02
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java
@@ -0,0 +1,109 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.app.Activity;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
+import io.flutter.view.TextureRegistry;
+
+/**
+ * Platform implementation of the camera_plugin.
+ *
+ * <p>Instantiate this in an add to app scenario to gracefully handle activity and context changes.
+ * See {@code io.flutter.plugins.camera.MainActivity} for an example.
+ *
+ * <p>Call {@link #registerWith(io.flutter.plugin.common.PluginRegistry.Registrar)} to register an
+ * implementation of this that uses the stable {@code io.flutter.plugin.common} package.
+ */
+public final class CameraPlugin implements FlutterPlugin, ActivityAware {
+
+ private static final String TAG = "CameraPlugin";
+ private @Nullable FlutterPluginBinding flutterPluginBinding;
+ private @Nullable MethodCallHandlerImpl methodCallHandler;
+
+ /**
+ * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment.
+ *
+ * <p>See {@code io.flutter.plugins.camera.MainActivity} for an example.
+ */
+ public CameraPlugin() {}
+
+ /**
+ * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
+ * package.
+ *
+ * <p>Calling this automatically initializes the plugin. However plugins initialized this way
+ * won't react to changes in activity or context, unlike {@link CameraPlugin}.
+ */
+ @SuppressWarnings("deprecation")
+ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
+ CameraPlugin plugin = new CameraPlugin();
+ plugin.maybeStartListening(
+ registrar.activity(),
+ registrar.messenger(),
+ registrar::addRequestPermissionsResultListener,
+ registrar.view());
+ }
+
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
+ this.flutterPluginBinding = binding;
+ }
+
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ this.flutterPluginBinding = null;
+ }
+
+ @Override
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+ maybeStartListening(
+ binding.getActivity(),
+ flutterPluginBinding.getBinaryMessenger(),
+ binding::addRequestPermissionsResultListener,
+ flutterPluginBinding.getTextureRegistry());
+ }
+
+ @Override
+ public void onDetachedFromActivity() {
+ // Could be on too low of an SDK to have started listening originally.
+ if (methodCallHandler != null) {
+ methodCallHandler.stopListening();
+ methodCallHandler = null;
+ }
+ }
+
+ @Override
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
+ onAttachedToActivity(binding);
+ }
+
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
+ }
+
+ private void maybeStartListening(
+ Activity activity,
+ BinaryMessenger messenger,
+ PermissionsRegistry permissionsRegistry,
+ TextureRegistry textureRegistry) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
+ return;
+ }
+
+ methodCallHandler =
+ new MethodCallHandlerImpl(
+ activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java
new file mode 100644
index 0000000..a69bae4
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java
@@ -0,0 +1,386 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Build.VERSION_CODES;
+import android.util.Range;
+import android.util.Rational;
+import android.util.Size;
+import androidx.annotation.RequiresApi;
+
+/** An interface allowing access to the different characteristics of the device's camera. */
+public interface CameraProperties {
+
+ /**
+ * Returns the name (or identifier) of the camera device.
+ *
+ * @return String The name of the camera device.
+ */
+ String getCameraName();
+
+ /**
+ * Returns the list of frame rate ranges for @see android.control.aeTargetFpsRange supported by
+ * this camera device.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_TARGET_FPS_RANGE key.
+ *
+ * @return android.util.Range<Integer>[] List of frame rate ranges supported by this camera
+ * device.
+ */
+ Range<Integer>[] getControlAutoExposureAvailableTargetFpsRanges();
+
+ /**
+ * Returns the maximum and minimum exposure compensation values for @see
+ * android.control.aeExposureCompensation, in counts of @see android.control.aeCompensationStep,
+ * that are supported by this camera device.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_RANGE key.
+ *
+ * @return android.util.Range<Integer> Maximum and minimum exposure compensation supported by this
+ * camera device.
+ */
+ Range<Integer> getControlAutoExposureCompensationRange();
+
+ /**
+ * Returns the smallest step by which the exposure compensation can be changed.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP key.
+ *
+ * @return double Smallest step by which the exposure compensation can be changed.
+ */
+ double getControlAutoExposureCompensationStep();
+
+ /**
+ * Returns a list of auto-focus modes for @see android.control.afMode that are supported by this
+ * camera device.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_AF_AVAILABLE_MODES key.
+ *
+ * @return int[] List of auto-focus modes supported by this camera device.
+ */
+ int[] getControlAutoFocusAvailableModes();
+
+ /**
+ * Returns the maximum number of metering regions that can be used by the auto-exposure routine.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AE key.
+ *
+ * @return Integer Maximum number of metering regions that can be used by the auto-exposure
+ * routine.
+ */
+ Integer getControlMaxRegionsAutoExposure();
+
+ /**
+ * Returns the maximum number of metering regions that can be used by the auto-focus routine.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AF key.
+ *
+ * @return Integer Maximum number of metering regions that can be used by the auto-focus routine.
+ */
+ Integer getControlMaxRegionsAutoFocus();
+
+ /**
+ * Returns a list of distortion correction modes for @see android.distortionCorrection.mode that
+ * are supported by this camera device.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#DISTORTION_CORRECTION_AVAILABLE_MODES key.
+ *
+ * @return int[] List of distortion correction modes supported by this camera device.
+ */
+ @RequiresApi(api = VERSION_CODES.P)
+ int[] getDistortionCorrectionAvailableModes();
+
+ /**
+ * Returns whether this camera device has a flash unit.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#FLASH_INFO_AVAILABLE key.
+ *
+ * @return Boolean Whether this camera device has a flash unit.
+ */
+ Boolean getFlashInfoAvailable();
+
+ /**
+ * Returns the direction the camera faces relative to device screen.
+ *
+ * <p><string>Possible values:</string>
+ *
+ * <ul>
+ * <li>@see android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT
+ * <li>@see android.hardware.camera2.CameraMetadata.LENS_FACING_BACK
+ * <li>@see android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL
+ * </ul>
+ *
+ * <p>By default maps to the @see android.hardware.camera2.CameraCharacteristics.LENS_FACING key.
+ *
+ * @return int Direction the camera faces relative to device screen.
+ */
+ int getLensFacing();
+
+ /**
+ * Returns the shortest distance from front most surface of the lens that can be brought into
+ * sharp focus.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE key.
+ *
+ * @return Float Shortest distance from front most surface of the lens that can be brought into
+ * sharp focus.
+ */
+ Float getLensInfoMinimumFocusDistance();
+
+ /**
+ * Returns the maximum ratio between both active area width and crop region width, and active area
+ * height and crop region height, for @see android.scaler.cropRegion.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key.
+ *
+ * @return Float Maximum ratio between both active area width and crop region width, and active
+ * area height and crop region height.
+ */
+ Float getScalerAvailableMaxDigitalZoom();
+
+ /**
+ * Returns the minimum ratio between the default camera zoom setting and all of the available
+ * zoom.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE key's lower value.
+ *
+ * @return Float Minimum ratio between the default zoom ratio and the minimum possible zoom.
+ */
+ @RequiresApi(api = VERSION_CODES.R)
+ Float getScalerMinZoomRatio();
+
+ /**
+ * Returns the maximum ratio between the default camera zoom setting and all of the available
+ * zoom.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE key's upper value.
+ *
+ * @return Float Maximum ratio between the default zoom ratio and the maximum possible zoom.
+ */
+ @RequiresApi(api = VERSION_CODES.R)
+ Float getScalerMaxZoomRatio();
+
+ /**
+ * Returns the area of the image sensor which corresponds to active pixels after any geometric
+ * distortion correction has been applied.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE key.
+ *
+ * @return android.graphics.Rect area of the image sensor which corresponds to active pixels after
+ * any geometric distortion correction has been applied.
+ */
+ Rect getSensorInfoActiveArraySize();
+
+ /**
+ * Returns the dimensions of the full pixel array, possibly including black calibration pixels.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE key.
+ *
+ * @return android.util.Size Dimensions of the full pixel array, possibly including black
+ * calibration pixels.
+ */
+ Size getSensorInfoPixelArraySize();
+
+ /**
+ * Returns the area of the image sensor which corresponds to active pixels prior to the
+ * application of any geometric distortion correction.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
+ * key.
+ *
+ * @return android.graphics.Rect Area of the image sensor which corresponds to active pixels prior
+ * to the application of any geometric distortion correction.
+ */
+ @RequiresApi(api = VERSION_CODES.M)
+ Rect getSensorInfoPreCorrectionActiveArraySize();
+
+ /**
+ * Returns the clockwise angle through which the output image needs to be rotated to be upright on
+ * the device screen in its native orientation.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#SENSOR_ORIENTATION key.
+ *
+ * @return int Clockwise angle through which the output image needs to be rotated to be upright on
+ * the device screen in its native orientation.
+ */
+ int getSensorOrientation();
+
+ /**
+ * Returns a level which generally classifies the overall set of the camera device functionality.
+ *
+ * <p><strong>Possible values:</strong>
+ *
+ * <ul>
+ * <li>@see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ * <li>@see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ * <li>@see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ * <li>@see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEVEL_3
+ * <li>@see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+ * </ul>
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL key.
+ *
+ * @return int Level which generally classifies the overall set of the camera device
+ * functionality.
+ */
+ int getHardwareLevel();
+
+ /**
+ * Returns a list of noise reduction modes for @see android.noiseReduction.mode that are supported
+ * by this camera device.
+ *
+ * <p>By default maps to the @see
+ * android.hardware.camera2.CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
+ * key.
+ *
+ * @return int[] List of noise reduction modes that are supported by this camera device.
+ */
+ int[] getAvailableNoiseReductionModes();
+}
+
+/**
+ * Implementation of the @see CameraProperties interface using the @see
+ * android.hardware.camera2.CameraCharacteristics class to access the different characteristics.
+ */
+class CameraPropertiesImpl implements CameraProperties {
+ private final CameraCharacteristics cameraCharacteristics;
+ private final String cameraName;
+
+ public CameraPropertiesImpl(String cameraName, CameraManager cameraManager)
+ throws CameraAccessException {
+ this.cameraName = cameraName;
+ this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName);
+ }
+
+ @Override
+ public String getCameraName() {
+ return cameraName;
+ }
+
+ @Override
+ public Range<Integer>[] getControlAutoExposureAvailableTargetFpsRanges() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
+ }
+
+ @Override
+ public Range<Integer> getControlAutoExposureCompensationRange() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
+ }
+
+ @Override
+ public double getControlAutoExposureCompensationStep() {
+ Rational rational =
+ cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
+
+ return rational == null ? 0.0 : rational.doubleValue();
+ }
+
+ @Override
+ public int[] getControlAutoFocusAvailableModes() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
+ }
+
+ @Override
+ public Integer getControlMaxRegionsAutoExposure() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE);
+ }
+
+ @Override
+ public Integer getControlMaxRegionsAutoFocus() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF);
+ }
+
+ @RequiresApi(api = VERSION_CODES.P)
+ @Override
+ public int[] getDistortionCorrectionAvailableModes() {
+ return cameraCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES);
+ }
+
+ @Override
+ public Boolean getFlashInfoAvailable() {
+ return cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ }
+
+ @Override
+ public int getLensFacing() {
+ return cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
+ }
+
+ @Override
+ public Float getLensInfoMinimumFocusDistance() {
+ return cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);
+ }
+
+ @Override
+ public Float getScalerAvailableMaxDigitalZoom() {
+ return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ @Override
+ public Float getScalerMaxZoomRatio() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getUpper();
+ }
+
+ @RequiresApi(api = VERSION_CODES.R)
+ @Override
+ public Float getScalerMinZoomRatio() {
+ return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getLower();
+ }
+
+ @Override
+ public Rect getSensorInfoActiveArraySize() {
+ return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+ }
+
+ @Override
+ public Size getSensorInfoPixelArraySize() {
+ return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
+ }
+
+ @RequiresApi(api = VERSION_CODES.M)
+ @Override
+ public Rect getSensorInfoPreCorrectionActiveArraySize() {
+ return cameraCharacteristics.get(
+ CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE);
+ }
+
+ @Override
+ public int getSensorOrientation() {
+ return cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ }
+
+ @Override
+ public int getHardwareLevel() {
+ return cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+ }
+
+ @Override
+ public int[] getAvailableNoiseReductionModes() {
+ return cameraCharacteristics.get(
+ CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java
new file mode 100644
index 0000000..951a279
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java
@@ -0,0 +1,182 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Build;
+import android.util.Size;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import java.util.Arrays;
+
+/**
+ * Utility class offering functions to calculate values regarding the camera boundaries.
+ *
+ * <p>The functions are used to calculate focus and exposure settings.
+ */
+public final class CameraRegionUtils {
+
+ /**
+ * Obtains the boundaries for the currently active camera, that can be used for calculating
+ * MeteringRectangle instances required for setting focus or exposure settings.
+ *
+ * @param cameraProperties - Collection of the characteristics for the current camera device.
+ * @param requestBuilder - The request builder for the current capture request.
+ * @return The boundaries for the current camera device.
+ */
+ public static Size getCameraBoundaries(
+ @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
+ && supportsDistortionCorrection(cameraProperties)) {
+ // Get the current distortion correction mode.
+ Integer distortionCorrectionMode =
+ requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE);
+
+ // Return the correct boundaries depending on the mode.
+ android.graphics.Rect rect;
+ if (distortionCorrectionMode == null
+ || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) {
+ rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize();
+ } else {
+ rect = cameraProperties.getSensorInfoActiveArraySize();
+ }
+
+ return SizeFactory.create(rect.width(), rect.height());
+ } else {
+ // No distortion correction support.
+ return cameraProperties.getSensorInfoPixelArraySize();
+ }
+ }
+
+ /**
+ * Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the center
+ * point.
+ *
+ * <p>Since the Camera API (due to cross-platform constraints) only accepts a point when
+ * configuring a specific focus or exposure area and Android requires a rectangle to configure
+ * these settings there is a need to convert the point into a rectangle. This method will create
+ * the required rectangle with an arbitrarily size that is a 10th of the current viewport and the
+ * coordinates as the center point.
+ *
+ * @param boundaries - The camera boundaries to calculate the metering rectangle for.
+ * @param x x - 1 >= coordinate >= 0.
+ * @param y y - 1 >= coordinate >= 0.
+ * @return The dimensions of the metering rectangle based on the supplied coordinates and
+ * boundaries.
+ */
+ public static MeteringRectangle convertPointToMeteringRectangle(
+ @NonNull Size boundaries,
+ double x,
+ double y,
+ @NonNull PlatformChannel.DeviceOrientation orientation) {
+ assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);
+ assert (x >= 0 && x <= 1);
+ assert (y >= 0 && y <= 1);
+ // Rotate the coordinates to match the device orientation.
+ double oldX = x, oldY = y;
+ switch (orientation) {
+ case PORTRAIT_UP: // 90 ccw.
+ y = 1 - oldX;
+ x = oldY;
+ break;
+ case PORTRAIT_DOWN: // 90 cw.
+ x = 1 - oldY;
+ y = oldX;
+ break;
+ case LANDSCAPE_LEFT:
+ // No rotation required.
+ break;
+ case LANDSCAPE_RIGHT: // 180.
+ x = 1 - x;
+ y = 1 - y;
+ break;
+ }
+ // Interpolate the target coordinate.
+ int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1)));
+ int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1)));
+ // Determine the dimensions of the metering rectangle (10th of the viewport).
+ int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d);
+ int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d);
+ // Adjust target coordinate to represent top-left corner of metering rectangle.
+ targetX -= targetWidth / 2;
+ targetY -= targetHeight / 2;
+ // Adjust target coordinate as to not fall out of bounds.
+ if (targetX < 0) {
+ targetX = 0;
+ }
+ if (targetY < 0) {
+ targetY = 0;
+ }
+ int maxTargetX = boundaries.getWidth() - 1 - targetWidth;
+ int maxTargetY = boundaries.getHeight() - 1 - targetHeight;
+ if (targetX > maxTargetX) {
+ targetX = maxTargetX;
+ }
+ if (targetY > maxTargetY) {
+ targetY = maxTargetY;
+ }
+ // Build the metering rectangle.
+ return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1);
+ }
+
+ @TargetApi(Build.VERSION_CODES.P)
+ private static boolean supportsDistortionCorrection(CameraProperties cameraProperties) {
+ int[] availableDistortionCorrectionModes =
+ cameraProperties.getDistortionCorrectionAvailableModes();
+ if (availableDistortionCorrectionModes == null) {
+ availableDistortionCorrectionModes = new int[0];
+ }
+ long nonOffModesSupported =
+ Arrays.stream(availableDistortionCorrectionModes)
+ .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF)
+ .count();
+ return nonOffModesSupported > 0;
+ }
+
+ /** Factory class that assists in creating a {@link MeteringRectangle} instance. */
+ static class MeteringRectangleFactory {
+ /**
+ * Creates a new instance of the {@link MeteringRectangle} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param x coordinate >= 0.
+ * @param y coordinate >= 0.
+ * @param width width >= 0.
+ * @param height height >= 0.
+ * @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and
+ * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively.
+ * @return new instance of the {@link MeteringRectangle} class.
+ * @throws IllegalArgumentException if any of the parameters were negative.
+ */
+ @VisibleForTesting
+ public static MeteringRectangle create(
+ int x, int y, int width, int height, int meteringWeight) {
+ return new MeteringRectangle(x, y, width, height, meteringWeight);
+ }
+ }
+
+ /** Factory class that assists in creating a {@link Size} instance. */
+ static class SizeFactory {
+ /**
+ * Creates a new instance of the {@link Size} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param width width >= 0.
+ * @param height height >= 0.
+ * @return new instance of the {@link Size} class.
+ */
+ @VisibleForTesting
+ public static Size create(int width, int height) {
+ return new Size(width, height);
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraState.java
new file mode 100644
index 0000000..ac48caf
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraState.java
@@ -0,0 +1,27 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+/**
+ * These are the states that the camera can be in. The camera can only take one photo at a time so
+ * this state describes the state of the camera itself. The camera works like a pipeline where we
+ * feed it requests through. It can only process one tasks at a time.
+ */
+public enum CameraState {
+ /** Idle, showing preview and not capturing anything. */
+ STATE_PREVIEW,
+
+ /** Starting and waiting for autofocus to complete. */
+ STATE_WAITING_FOCUS,
+
+ /** Start performing autoexposure. */
+ STATE_WAITING_PRECAPTURE_START,
+
+ /** waiting for autoexposure to complete. */
+ STATE_WAITING_PRECAPTURE_DONE,
+
+ /** Capturing an image. */
+ STATE_CAPTURING,
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java
new file mode 100644
index 0000000..11b6eea
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java
@@ -0,0 +1,132 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Provides various utilities for camera. */
+public final class CameraUtils {
+
+ private CameraUtils() {}
+
+ /**
+ * Gets the {@link CameraManager} singleton.
+ *
+ * @param context The context to get the {@link CameraManager} singleton from.
+ * @return The {@link CameraManager} singleton.
+ */
+ static CameraManager getCameraManager(Context context) {
+ return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ }
+
+ /**
+ * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value.
+ *
+ * @param orientation The orientation to serialize.
+ * @return The serialized orientation.
+ * @throws UnsupportedOperationException when the provided orientation not have a corresponding
+ * string value.
+ */
+ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) {
+ if (orientation == null)
+ throw new UnsupportedOperationException("Could not serialize null device orientation.");
+ switch (orientation) {
+ case PORTRAIT_UP:
+ return "portraitUp";
+ case PORTRAIT_DOWN:
+ return "portraitDown";
+ case LANDSCAPE_LEFT:
+ return "landscapeLeft";
+ case LANDSCAPE_RIGHT:
+ return "landscapeRight";
+ default:
+ throw new UnsupportedOperationException(
+ "Could not serialize device orientation: " + orientation.toString());
+ }
+ }
+
+ /**
+ * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation}
+ * value.
+ *
+ * @param orientation The string value to deserialize.
+ * @return The deserialized orientation.
+ * @throws UnsupportedOperationException when the provided string value does not have a
+ * corresponding {@link PlatformChannel.DeviceOrientation}.
+ */
+ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) {
+ if (orientation == null)
+ throw new UnsupportedOperationException("Could not deserialize null device orientation.");
+ switch (orientation) {
+ case "portraitUp":
+ return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
+ case "portraitDown":
+ return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
+ case "landscapeLeft":
+ return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
+ case "landscapeRight":
+ return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
+ default:
+ throw new UnsupportedOperationException(
+ "Could not deserialize device orientation: " + orientation);
+ }
+ }
+
+ /**
+ * Gets all the available cameras for the device.
+ *
+ * @param activity The current Android activity.
+ * @return A map of all the available cameras, with their name as their key.
+ * @throws CameraAccessException when the camera could not be accessed.
+ */
+ public static List<Map<String, Object>> getAvailableCameras(Activity activity)
+ throws CameraAccessException {
+ CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
+ String[] cameraNames = cameraManager.getCameraIdList();
+ List<Map<String, Object>> cameras = new ArrayList<>();
+ for (String cameraName : cameraNames) {
+ int cameraId;
+ try {
+ cameraId = Integer.parseInt(cameraName, 10);
+ } catch (NumberFormatException e) {
+ cameraId = -1;
+ }
+ if (cameraId < 0) {
+ continue;
+ }
+
+ HashMap<String, Object> details = new HashMap<>();
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName);
+ details.put("name", cameraName);
+ int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ details.put("sensorOrientation", sensorOrientation);
+
+ int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
+ switch (lensFacing) {
+ case CameraMetadata.LENS_FACING_FRONT:
+ details.put("lensFacing", "front");
+ break;
+ case CameraMetadata.LENS_FACING_BACK:
+ details.put("lensFacing", "back");
+ break;
+ case CameraMetadata.LENS_FACING_EXTERNAL:
+ details.put("lensFacing", "external");
+ break;
+ }
+ cameras.add(details);
+ }
+ return cameras;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java
new file mode 100644
index 0000000..e15078e
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java
@@ -0,0 +1,206 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.os.Handler;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugins.camera.features.autofocus.FocusMode;
+import io.flutter.plugins.camera.features.exposurelock.ExposureMode;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Utility class that facilitates communication to the Flutter client */
+public class DartMessenger {
+ @NonNull private final Handler handler;
+ @Nullable private MethodChannel cameraChannel;
+ @Nullable private MethodChannel deviceChannel;
+
+ /** Specifies the different device related message types. */
+ enum DeviceEventType {
+ /** Indicates the device's orientation has changed. */
+ ORIENTATION_CHANGED("orientation_changed");
+ private final String method;
+
+ DeviceEventType(String method) {
+ this.method = method;
+ }
+ }
+
+ /** Specifies the different camera related message types. */
+ enum CameraEventType {
+ /** Indicates that an error occurred while interacting with the camera. */
+ ERROR("error"),
+ /** Indicates that the camera is closing. */
+ CLOSING("camera_closing"),
+ /** Indicates that the camera is initialized. */
+ INITIALIZED("initialized");
+
+ private final String method;
+
+ /**
+ * Converts the supplied method name to the matching {@link CameraEventType}.
+ *
+ * @param method name to be converted into a {@link CameraEventType}.
+ */
+ CameraEventType(String method) {
+ this.method = method;
+ }
+ }
+
+ /**
+ * Creates a new instance of the {@link DartMessenger} class.
+ *
+ * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter.
+ * @param cameraId identifies the camera which is the source of the communication.
+ * @param handler the handler used to manage the thread's message queue. This should always be a
+ * handler managing the main thread since communication with Flutter should always happen on
+ * the main thread. The handler is mainly supplied so it will be easier test this class.
+ */
+ DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) {
+ cameraChannel =
+ new MethodChannel(messenger, "plugins.flutter.io/camera_android/camera" + cameraId);
+ deviceChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android/fromPlatform");
+ this.handler = handler;
+ }
+
+ /**
+ * Sends a message to the Flutter client informing the orientation of the device has been changed.
+ *
+ * @param orientation specifies the new orientation of the device.
+ */
+ public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) {
+ assert (orientation != null);
+ this.send(
+ DeviceEventType.ORIENTATION_CHANGED,
+ new HashMap<String, Object>() {
+ {
+ put("orientation", CameraUtils.serializeDeviceOrientation(orientation));
+ }
+ });
+ }
+
+ /**
+ * Sends a message to the Flutter client informing that the camera has been initialized.
+ *
+ * @param previewWidth describes the preview width that is supported by the camera.
+ * @param previewHeight describes the preview height that is supported by the camera.
+ * @param exposureMode describes the current exposure mode that is set on the camera.
+ * @param focusMode describes the current focus mode that is set on the camera.
+ * @param exposurePointSupported indicates if the camera supports setting an exposure point.
+ * @param focusPointSupported indicates if the camera supports setting a focus point.
+ */
+ void sendCameraInitializedEvent(
+ Integer previewWidth,
+ Integer previewHeight,
+ ExposureMode exposureMode,
+ FocusMode focusMode,
+ Boolean exposurePointSupported,
+ Boolean focusPointSupported) {
+ assert (previewWidth != null);
+ assert (previewHeight != null);
+ assert (exposureMode != null);
+ assert (focusMode != null);
+ assert (exposurePointSupported != null);
+ assert (focusPointSupported != null);
+ this.send(
+ CameraEventType.INITIALIZED,
+ new HashMap<String, Object>() {
+ {
+ put("previewWidth", previewWidth.doubleValue());
+ put("previewHeight", previewHeight.doubleValue());
+ put("exposureMode", exposureMode.toString());
+ put("focusMode", focusMode.toString());
+ put("exposurePointSupported", exposurePointSupported);
+ put("focusPointSupported", focusPointSupported);
+ }
+ });
+ }
+
+ /** Sends a message to the Flutter client informing that the camera is closing. */
+ void sendCameraClosingEvent() {
+ send(CameraEventType.CLOSING);
+ }
+
+ /**
+ * Sends a message to the Flutter client informing that an error occurred while interacting with
+ * the camera.
+ *
+ * @param description contains details regarding the error that occurred.
+ */
+ void sendCameraErrorEvent(@Nullable String description) {
+ this.send(
+ CameraEventType.ERROR,
+ new HashMap<String, Object>() {
+ {
+ if (!TextUtils.isEmpty(description)) put("description", description);
+ }
+ });
+ }
+
+ private void send(CameraEventType eventType) {
+ send(eventType, new HashMap<>());
+ }
+
+ private void send(CameraEventType eventType, Map<String, Object> args) {
+ if (cameraChannel == null) {
+ return;
+ }
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ cameraChannel.invokeMethod(eventType.method, args);
+ }
+ });
+ }
+
+ private void send(DeviceEventType eventType) {
+ send(eventType, new HashMap<>());
+ }
+
+ private void send(DeviceEventType eventType, Map<String, Object> args) {
+ if (deviceChannel == null) {
+ return;
+ }
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ deviceChannel.invokeMethod(eventType.method, args);
+ }
+ });
+ }
+
+ /**
+ * Send a success payload to a {@link MethodChannel.Result} on the main thread.
+ *
+ * @param payload The payload to send.
+ */
+ public void finish(MethodChannel.Result result, Object payload) {
+ handler.post(() -> result.success(payload));
+ }
+
+ /**
+ * Send an error payload to a {@link MethodChannel.Result} on the main thread.
+ *
+ * @param errorCode error code.
+ * @param errorMessage error message.
+ * @param errorDetails error details.
+ */
+ public void error(
+ MethodChannel.Result result,
+ String errorCode,
+ @Nullable String errorMessage,
+ @Nullable Object errorDetails) {
+ handler.post(() -> result.error(errorCode, errorMessage, errorDetails));
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java
new file mode 100644
index 0000000..821c9a5
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java
@@ -0,0 +1,105 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.media.Image;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/** Saves a JPEG {@link Image} into the specified {@link File}. */
+public class ImageSaver implements Runnable {
+
+ /** The JPEG image */
+ private final Image image;
+
+ /** The file we save the image into. */
+ private final File file;
+
+ /** Used to report the status of the save action. */
+ private final Callback callback;
+
+ /**
+ * Creates an instance of the ImageSaver runnable
+ *
+ * @param image - The image to save
+ * @param file - The file to save the image to
+ * @param callback - The callback that is run on completion, or when an error is encountered.
+ */
+ ImageSaver(@NonNull Image image, @NonNull File file, @NonNull Callback callback) {
+ this.image = image;
+ this.file = file;
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ ByteBuffer buffer = image.getPlanes()[0].getBuffer();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ FileOutputStream output = null;
+ try {
+ output = FileOutputStreamFactory.create(file);
+ output.write(bytes);
+
+ callback.onComplete(file.getAbsolutePath());
+
+ } catch (IOException e) {
+ callback.onError("IOError", "Failed saving image");
+ } finally {
+ image.close();
+ if (null != output) {
+ try {
+ output.close();
+ } catch (IOException e) {
+ callback.onError("cameraAccess", e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * The interface for the callback that is passed to ImageSaver, for detecting completion or
+ * failure of the image saving task.
+ */
+ public interface Callback {
+ /**
+ * Called when the image file has been saved successfully.
+ *
+ * @param absolutePath - The absolute path of the file that was saved.
+ */
+ void onComplete(String absolutePath);
+
+ /**
+ * Called when an error is encountered while saving the image file.
+ *
+ * @param errorCode - The error code.
+ * @param errorMessage - The human readable error message.
+ */
+ void onError(String errorCode, String errorMessage);
+ }
+
+ /** Factory class that assists in creating a {@link FileOutputStream} instance. */
+ static class FileOutputStreamFactory {
+ /**
+ * Creates a new instance of the {@link FileOutputStream} class.
+ *
+ * <p>This method is visible for testing purposes only and should never be used outside this *
+ * class.
+ *
+ * @param file - The file to create the output stream for
+ * @return new instance of the {@link FileOutputStream} class.
+ * @throws FileNotFoundException when the supplied file could not be found.
+ */
+ @VisibleForTesting
+ public static FileOutputStream create(File file) throws FileNotFoundException {
+ return new FileOutputStream(file);
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
new file mode 100644
index 0000000..432344a
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
@@ -0,0 +1,417 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera;
+
+import android.app.Activity;
+import android.hardware.camera2.CameraAccessException;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
+import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl;
+import io.flutter.plugins.camera.features.Point;
+import io.flutter.plugins.camera.features.autofocus.FocusMode;
+import io.flutter.plugins.camera.features.exposurelock.ExposureMode;
+import io.flutter.plugins.camera.features.flash.FlashMode;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.view.TextureRegistry;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
+ private final Activity activity;
+ private final BinaryMessenger messenger;
+ private final CameraPermissions cameraPermissions;
+ private final PermissionsRegistry permissionsRegistry;
+ private final TextureRegistry textureRegistry;
+ private final MethodChannel methodChannel;
+ private final EventChannel imageStreamChannel;
+ private @Nullable Camera camera;
+
+ MethodCallHandlerImpl(
+ Activity activity,
+ BinaryMessenger messenger,
+ CameraPermissions cameraPermissions,
+ PermissionsRegistry permissionsAdder,
+ TextureRegistry textureRegistry) {
+ this.activity = activity;
+ this.messenger = messenger;
+ this.cameraPermissions = cameraPermissions;
+ this.permissionsRegistry = permissionsAdder;
+ this.textureRegistry = textureRegistry;
+
+ methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android");
+ imageStreamChannel =
+ new EventChannel(messenger, "plugins.flutter.io/camera_android/imageStream");
+ methodChannel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) {
+ switch (call.method) {
+ case "availableCameras":
+ try {
+ result.success(CameraUtils.getAvailableCameras(activity));
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ case "create":
+ {
+ if (camera != null) {
+ camera.close();
+ }
+
+ cameraPermissions.requestPermissions(
+ activity,
+ permissionsRegistry,
+ call.argument("enableAudio"),
+ (String errCode, String errDesc) -> {
+ if (errCode == null) {
+ try {
+ instantiateCamera(call, result);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ } else {
+ result.error(errCode, errDesc, null);
+ }
+ });
+ break;
+ }
+ case "initialize":
+ {
+ if (camera != null) {
+ try {
+ camera.open(call.argument("imageFormatGroup"));
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ } else {
+ result.error(
+ "cameraNotFound",
+ "Camera not found. Please call the 'create' method before calling 'initialize'.",
+ null);
+ }
+ break;
+ }
+ case "takePicture":
+ {
+ camera.takePicture(result);
+ break;
+ }
+ case "prepareForVideoRecording":
+ {
+ // This optimization is not required for Android.
+ result.success(null);
+ break;
+ }
+ case "startVideoRecording":
+ {
+ camera.startVideoRecording(
+ result,
+ Objects.equals(call.argument("enableStream"), true) ? imageStreamChannel : null);
+ break;
+ }
+ case "stopVideoRecording":
+ {
+ camera.stopVideoRecording(result);
+ break;
+ }
+ case "pauseVideoRecording":
+ {
+ camera.pauseVideoRecording(result);
+ break;
+ }
+ case "resumeVideoRecording":
+ {
+ camera.resumeVideoRecording(result);
+ break;
+ }
+ case "setFlashMode":
+ {
+ String modeStr = call.argument("mode");
+ FlashMode mode = FlashMode.getValueForString(modeStr);
+ if (mode == null) {
+ result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null);
+ return;
+ }
+ try {
+ camera.setFlashMode(result, mode);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setExposureMode":
+ {
+ String modeStr = call.argument("mode");
+ ExposureMode mode = ExposureMode.getValueForString(modeStr);
+ if (mode == null) {
+ result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null);
+ return;
+ }
+ try {
+ camera.setExposureMode(result, mode);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setExposurePoint":
+ {
+ Boolean reset = call.argument("reset");
+ Double x = null;
+ Double y = null;
+ if (reset == null || !reset) {
+ x = call.argument("x");
+ y = call.argument("y");
+ }
+ try {
+ camera.setExposurePoint(result, new Point(x, y));
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "getMinExposureOffset":
+ {
+ try {
+ result.success(camera.getMinExposureOffset());
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "getMaxExposureOffset":
+ {
+ try {
+ result.success(camera.getMaxExposureOffset());
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "getExposureOffsetStepSize":
+ {
+ try {
+ result.success(camera.getExposureOffsetStepSize());
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setExposureOffset":
+ {
+ try {
+ camera.setExposureOffset(result, call.argument("offset"));
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setFocusMode":
+ {
+ String modeStr = call.argument("mode");
+ FocusMode mode = FocusMode.getValueForString(modeStr);
+ if (mode == null) {
+ result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null);
+ return;
+ }
+ try {
+ camera.setFocusMode(result, mode);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setFocusPoint":
+ {
+ Boolean reset = call.argument("reset");
+ Double x = null;
+ Double y = null;
+ if (reset == null || !reset) {
+ x = call.argument("x");
+ y = call.argument("y");
+ }
+ try {
+ camera.setFocusPoint(result, new Point(x, y));
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "startImageStream":
+ {
+ try {
+ camera.startPreviewWithImageStream(imageStreamChannel);
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "stopImageStream":
+ {
+ try {
+ camera.startPreview();
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "getMaxZoomLevel":
+ {
+ assert camera != null;
+
+ try {
+ float maxZoomLevel = camera.getMaxZoomLevel();
+ result.success(maxZoomLevel);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "getMinZoomLevel":
+ {
+ assert camera != null;
+
+ try {
+ float minZoomLevel = camera.getMinZoomLevel();
+ result.success(minZoomLevel);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "setZoomLevel":
+ {
+ assert camera != null;
+
+ Double zoom = call.argument("zoom");
+
+ if (zoom == null) {
+ result.error(
+ "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null);
+ return;
+ }
+
+ try {
+ camera.setZoomLevel(result, zoom.floatValue());
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "lockCaptureOrientation":
+ {
+ PlatformChannel.DeviceOrientation orientation =
+ CameraUtils.deserializeDeviceOrientation(call.argument("orientation"));
+
+ try {
+ camera.lockCaptureOrientation(orientation);
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "unlockCaptureOrientation":
+ {
+ try {
+ camera.unlockCaptureOrientation();
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "pausePreview":
+ {
+ try {
+ camera.pausePreview();
+ result.success(null);
+ } catch (Exception e) {
+ handleException(e, result);
+ }
+ break;
+ }
+ case "resumePreview":
+ {
+ camera.resumePreview();
+ result.success(null);
+ break;
+ }
+ case "dispose":
+ {
+ if (camera != null) {
+ camera.dispose();
+ }
+ result.success(null);
+ break;
+ }
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
+
+ void stopListening() {
+ methodChannel.setMethodCallHandler(null);
+ }
+
+ private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException {
+ String cameraName = call.argument("cameraName");
+ String preset = call.argument("resolutionPreset");
+ boolean enableAudio = call.argument("enableAudio");
+
+ TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
+ textureRegistry.createSurfaceTexture();
+ DartMessenger dartMessenger =
+ new DartMessenger(
+ messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper()));
+ CameraProperties cameraProperties =
+ new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
+ ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);
+
+ camera =
+ new Camera(
+ activity,
+ flutterSurfaceTexture,
+ new CameraFeatureFactoryImpl(),
+ dartMessenger,
+ cameraProperties,
+ resolutionPreset,
+ enableAudio);
+
+ Map<String, Object> reply = new HashMap<>();
+ reply.put("cameraId", flutterSurfaceTexture.id());
+ result.success(reply);
+ }
+
+ // We move catching CameraAccessException out of onMethodCall because it causes a crash
+ // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to
+ // to be able to compile with <21 sdks for apps that want the camera and support earlier version.
+ @SuppressWarnings("ConstantConditions")
+ private void handleException(Exception exception, Result result) {
+ if (exception instanceof CameraAccessException) {
+ result.error("CameraAccess", exception.getMessage(), null);
+ return;
+ }
+
+ // CameraAccessException can not be cast to a RuntimeException.
+ throw (RuntimeException) exception;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java
new file mode 100644
index 0000000..92cfd54
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java
@@ -0,0 +1,60 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features;
+
+import android.hardware.camera2.CaptureRequest;
+import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.CameraProperties;
+
+/**
+ * An interface describing a feature in the camera. This holds a setting value of type T and must
+ * implement a means to check if this setting is supported by the current camera properties. It also
+ * must implement a builder update method which will update a given capture request builder for this
+ * feature's current setting value.
+ *
+ * @param <T>
+ */
+public abstract class CameraFeature<T> {
+
+ protected final CameraProperties cameraProperties;
+
+ protected CameraFeature(@NonNull CameraProperties cameraProperties) {
+ this.cameraProperties = cameraProperties;
+ }
+
+ /** Debug name for this feature. */
+ public abstract String getDebugName();
+
+ /**
+ * Gets the current value of this feature's setting.
+ *
+ * @return <T> Current value of this feature's setting.
+ */
+ public abstract T getValue();
+
+ /**
+ * Sets a new value for this feature's setting.
+ *
+ * @param value New value for this feature's setting.
+ */
+ public abstract void setValue(T value);
+
+ /**
+ * Returns whether or not this feature is supported.
+ *
+ * <p>When the feature is not supported any {@see #value} is simply ignored by the camera plugin.
+ *
+ * @return boolean Whether or not this feature is supported.
+ */
+ public abstract boolean checkIsSupported();
+
+ /**
+ * Updates the setting in a provided {@see android.hardware.camera2.CaptureRequest.Builder}.
+ *
+ * @param requestBuilder A {@see android.hardware.camera2.CaptureRequest.Builder} instance used to
+ * configure the settings and outputs needed to capture a single image from the camera device.
+ */
+ public abstract void updateBuilder(CaptureRequest.Builder requestBuilder);
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java
new file mode 100644
index 0000000..b91f9a1
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java
@@ -0,0 +1,149 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features;
+
+import android.app.Activity;
+import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.DartMessenger;
+import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
+import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature;
+import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature;
+import io.flutter.plugins.camera.features.flash.FlashFeature;
+import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature;
+import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature;
+import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
+
+/**
+ * Factory for creating the supported feature implementation controlling different aspects of the
+ * {@link android.hardware.camera2.CaptureRequest}.
+ */
+public interface CameraFeatureFactory {
+
+ /**
+ * Creates a new instance of the auto focus feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @param recordingVideo indicates if the camera is currently recording.
+ * @return newly created instance of the AutoFocusFeature class.
+ */
+ AutoFocusFeature createAutoFocusFeature(
+ @NonNull CameraProperties cameraProperties, boolean recordingVideo);
+
+ /**
+ * Creates a new instance of the exposure lock feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the ExposureLockFeature class.
+ */
+ ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties);
+
+ /**
+ * Creates a new instance of the exposure offset feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the ExposureOffsetFeature class.
+ */
+ ExposureOffsetFeature createExposureOffsetFeature(@NonNull CameraProperties cameraProperties);
+
+ /**
+ * Creates a new instance of the flash feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the FlashFeature class.
+ */
+ FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties);
+
+ /**
+ * Creates a new instance of the resolution feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @param initialSetting initial resolution preset.
+ * @param cameraName the name of the camera which can be used to identify the camera device.
+ * @return newly created instance of the ResolutionFeature class.
+ */
+ ResolutionFeature createResolutionFeature(
+ @NonNull CameraProperties cameraProperties,
+ ResolutionPreset initialSetting,
+ String cameraName);
+
+ /**
+ * Creates a new instance of the focus point feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing
+ * information about the sensor and device orientation.
+ * @return newly created instance of the FocusPointFeature class.
+ */
+ FocusPointFeature createFocusPointFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull SensorOrientationFeature sensorOrientationFeature);
+
+ /**
+ * Creates a new instance of the FPS range feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the FpsRangeFeature class.
+ */
+ FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties);
+
+ /**
+ * Creates a new instance of the sensor orientation feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @param activity current activity associated with the camera plugin.
+ * @param dartMessenger instance of the DartMessenger class, used to send state updates back to
+ * Dart.
+ * @return newly created instance of the SensorOrientationFeature class.
+ */
+ SensorOrientationFeature createSensorOrientationFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull Activity activity,
+ @NonNull DartMessenger dartMessenger);
+
+ /**
+ * Creates a new instance of the zoom level feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the ZoomLevelFeature class.
+ */
+ ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties);
+
+ /**
+ * Creates a new instance of the exposure point feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing
+ * information about the sensor and device orientation.
+ * @return newly created instance of the ExposurePointFeature class.
+ */
+ ExposurePointFeature createExposurePointFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull SensorOrientationFeature sensorOrientationFeature);
+
+ /**
+ * Creates a new instance of the noise reduction feature.
+ *
+ * @param cameraProperties instance of the CameraProperties class containing information about the
+ * cameras features.
+ * @return newly created instance of the NoiseReductionFeature class.
+ */
+ NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties);
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java
new file mode 100644
index 0000000..95a8c06
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java
@@ -0,0 +1,98 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features;
+
+import android.app.Activity;
+import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.DartMessenger;
+import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
+import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature;
+import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature;
+import io.flutter.plugins.camera.features.flash.FlashFeature;
+import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature;
+import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature;
+import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
+
+/**
+ * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature
+ * implementation controlling different aspects of the {@link
+ * android.hardware.camera2.CaptureRequest}.
+ */
+public class CameraFeatureFactoryImpl implements CameraFeatureFactory {
+
+ @Override
+ public AutoFocusFeature createAutoFocusFeature(
+ @NonNull CameraProperties cameraProperties, boolean recordingVideo) {
+ return new AutoFocusFeature(cameraProperties, recordingVideo);
+ }
+
+ @Override
+ public ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties) {
+ return new ExposureLockFeature(cameraProperties);
+ }
+
+ @Override
+ public ExposureOffsetFeature createExposureOffsetFeature(
+ @NonNull CameraProperties cameraProperties) {
+ return new ExposureOffsetFeature(cameraProperties);
+ }
+
+ @Override
+ public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) {
+ return new FlashFeature(cameraProperties);
+ }
+
+ @Override
+ public ResolutionFeature createResolutionFeature(
+ @NonNull CameraProperties cameraProperties,
+ ResolutionPreset initialSetting,
+ String cameraName) {
+ return new ResolutionFeature(cameraProperties, initialSetting, cameraName);
+ }
+
+ @Override
+ public FocusPointFeature createFocusPointFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull SensorOrientationFeature sensorOrientationFeature) {
+ return new FocusPointFeature(cameraProperties, sensorOrientationFeature);
+ }
+
+ @Override
+ public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) {
+ return new FpsRangeFeature(cameraProperties);
+ }
+
+ @Override
+ public SensorOrientationFeature createSensorOrientationFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull Activity activity,
+ @NonNull DartMessenger dartMessenger) {
+ return new SensorOrientationFeature(cameraProperties, activity, dartMessenger);
+ }
+
+ @Override
+ public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) {
+ return new ZoomLevelFeature(cameraProperties);
+ }
+
+ @Override
+ public ExposurePointFeature createExposurePointFeature(
+ @NonNull CameraProperties cameraProperties,
+ @NonNull SensorOrientationFeature sensorOrientationFeature) {
+ return new ExposurePointFeature(cameraProperties, sensorOrientationFeature);
+ }
+
+ @Override
+ public NoiseReductionFeature createNoiseReductionFeature(
+ @NonNull CameraProperties cameraProperties) {
+ return new NoiseReductionFeature(cameraProperties);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java
new file mode 100644
index 0000000..659fd15
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java
@@ -0,0 +1,285 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features;
+
+import android.app.Activity;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.DartMessenger;
+import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
+import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature;
+import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature;
+import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature;
+import io.flutter.plugins.camera.features.flash.FlashFeature;
+import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature;
+import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature;
+import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionFeature;
+import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * These are all of our available features in the camera. Used in the Camera to access all features
+ * in a simpler way.
+ */
+public class CameraFeatures {
+ private static final String AUTO_FOCUS = "AUTO_FOCUS";
+ private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK";
+ private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET";
+ private static final String EXPOSURE_POINT = "EXPOSURE_POINT";
+ private static final String FLASH = "FLASH";
+ private static final String FOCUS_POINT = "FOCUS_POINT";
+ private static final String FPS_RANGE = "FPS_RANGE";
+ private static final String NOISE_REDUCTION = "NOISE_REDUCTION";
+ private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES";
+ private static final String RESOLUTION = "RESOLUTION";
+ private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION";
+ private static final String ZOOM_LEVEL = "ZOOM_LEVEL";
+
+ public static CameraFeatures init(
+ CameraFeatureFactory cameraFeatureFactory,
+ CameraProperties cameraProperties,
+ Activity activity,
+ DartMessenger dartMessenger,
+ ResolutionPreset resolutionPreset) {
+ CameraFeatures cameraFeatures = new CameraFeatures();
+ cameraFeatures.setAutoFocus(
+ cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false));
+ cameraFeatures.setExposureLock(
+ cameraFeatureFactory.createExposureLockFeature(cameraProperties));
+ cameraFeatures.setExposureOffset(
+ cameraFeatureFactory.createExposureOffsetFeature(cameraProperties));
+ SensorOrientationFeature sensorOrientationFeature =
+ cameraFeatureFactory.createSensorOrientationFeature(
+ cameraProperties, activity, dartMessenger);
+ cameraFeatures.setSensorOrientation(sensorOrientationFeature);
+ cameraFeatures.setExposurePoint(
+ cameraFeatureFactory.createExposurePointFeature(
+ cameraProperties, sensorOrientationFeature));
+ cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties));
+ cameraFeatures.setFocusPoint(
+ cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature));
+ cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties));
+ cameraFeatures.setNoiseReduction(
+ cameraFeatureFactory.createNoiseReductionFeature(cameraProperties));
+ cameraFeatures.setResolution(
+ cameraFeatureFactory.createResolutionFeature(
+ cameraProperties, resolutionPreset, cameraProperties.getCameraName()));
+ cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties));
+ return cameraFeatures;
+ }
+
+ private Map<String, CameraFeature> featureMap = new HashMap<>();
+
+ /**
+ * Gets a collection of all features that have been set.
+ *
+ * @return A collection of all features that have been set.
+ */
+ public Collection<CameraFeature> getAllFeatures() {
+ return this.featureMap.values();
+ }
+
+ /**
+ * Gets the auto focus feature if it has been set.
+ *
+ * @return the auto focus feature.
+ */
+ public AutoFocusFeature getAutoFocus() {
+ return (AutoFocusFeature) featureMap.get(AUTO_FOCUS);
+ }
+
+ /**
+ * Sets the instance of the auto focus feature.
+ *
+ * @param autoFocus the {@link AutoFocusFeature} instance to set.
+ */
+ public void setAutoFocus(AutoFocusFeature autoFocus) {
+ this.featureMap.put(AUTO_FOCUS, autoFocus);
+ }
+
+ /**
+ * Gets the exposure lock feature if it has been set.
+ *
+ * @return the exposure lock feature.
+ */
+ public ExposureLockFeature getExposureLock() {
+ return (ExposureLockFeature) featureMap.get(EXPOSURE_LOCK);
+ }
+
+ /**
+ * Sets the instance of the exposure lock feature.
+ *
+ * @param exposureLock the {@link ExposureLockFeature} instance to set.
+ */
+ public void setExposureLock(ExposureLockFeature exposureLock) {
+ this.featureMap.put(EXPOSURE_LOCK, exposureLock);
+ }
+
+ /**
+ * Gets the exposure offset feature if it has been set.
+ *
+ * @return the exposure offset feature.
+ */
+ public ExposureOffsetFeature getExposureOffset() {
+ return (ExposureOffsetFeature) featureMap.get(EXPOSURE_OFFSET);
+ }
+
+ /**
+ * Sets the instance of the exposure offset feature.
+ *
+ * @param exposureOffset the {@link ExposureOffsetFeature} instance to set.
+ */
+ public void setExposureOffset(ExposureOffsetFeature exposureOffset) {
+ this.featureMap.put(EXPOSURE_OFFSET, exposureOffset);
+ }
+
+ /**
+ * Gets the exposure point feature if it has been set.
+ *
+ * @return the exposure point feature.
+ */
+ public ExposurePointFeature getExposurePoint() {
+ return (ExposurePointFeature) featureMap.get(EXPOSURE_POINT);
+ }
+
+ /**
+ * Sets the instance of the exposure point feature.
+ *
+ * @param exposurePoint the {@link ExposurePointFeature} instance to set.
+ */
+ public void setExposurePoint(ExposurePointFeature exposurePoint) {
+ this.featureMap.put(EXPOSURE_POINT, exposurePoint);
+ }
+
+ /**
+ * Gets the flash feature if it has been set.
+ *
+ * @return the flash feature.
+ */
+ public FlashFeature getFlash() {
+ return (FlashFeature) featureMap.get(FLASH);
+ }
+
+ /**
+ * Sets the instance of the flash feature.
+ *
+ * @param flash the {@link FlashFeature} instance to set.
+ */
+ public void setFlash(FlashFeature flash) {
+ this.featureMap.put(FLASH, flash);
+ }
+
+ /**
+ * Gets the focus point feature if it has been set.
+ *
+ * @return the focus point feature.
+ */
+ public FocusPointFeature getFocusPoint() {
+ return (FocusPointFeature) featureMap.get(FOCUS_POINT);
+ }
+
+ /**
+ * Sets the instance of the focus point feature.
+ *
+ * @param focusPoint the {@link FocusPointFeature} instance to set.
+ */
+ public void setFocusPoint(FocusPointFeature focusPoint) {
+ this.featureMap.put(FOCUS_POINT, focusPoint);
+ }
+
+ /**
+ * Gets the fps range feature if it has been set.
+ *
+ * @return the fps range feature.
+ */
+ public FpsRangeFeature getFpsRange() {
+ return (FpsRangeFeature) featureMap.get(FPS_RANGE);
+ }
+
+ /**
+ * Sets the instance of the fps range feature.
+ *
+ * @param fpsRange the {@link FpsRangeFeature} instance to set.
+ */
+ public void setFpsRange(FpsRangeFeature fpsRange) {
+ this.featureMap.put(FPS_RANGE, fpsRange);
+ }
+
+ /**
+ * Gets the noise reduction feature if it has been set.
+ *
+ * @return the noise reduction feature.
+ */
+ public NoiseReductionFeature getNoiseReduction() {
+ return (NoiseReductionFeature) featureMap.get(NOISE_REDUCTION);
+ }
+
+ /**
+ * Sets the instance of the noise reduction feature.
+ *
+ * @param noiseReduction the {@link NoiseReductionFeature} instance to set.
+ */
+ public void setNoiseReduction(NoiseReductionFeature noiseReduction) {
+ this.featureMap.put(NOISE_REDUCTION, noiseReduction);
+ }
+
+ /**
+ * Gets the resolution feature if it has been set.
+ *
+ * @return the resolution feature.
+ */
+ public ResolutionFeature getResolution() {
+ return (ResolutionFeature) featureMap.get(RESOLUTION);
+ }
+
+ /**
+ * Sets the instance of the resolution feature.
+ *
+ * @param resolution the {@link ResolutionFeature} instance to set.
+ */
+ public void setResolution(ResolutionFeature resolution) {
+ this.featureMap.put(RESOLUTION, resolution);
+ }
+
+ /**
+ * Gets the sensor orientation feature if it has been set.
+ *
+ * @return the sensor orientation feature.
+ */
+ public SensorOrientationFeature getSensorOrientation() {
+ return (SensorOrientationFeature) featureMap.get(SENSOR_ORIENTATION);
+ }
+
+ /**
+ * Sets the instance of the sensor orientation feature.
+ *
+ * @param sensorOrientation the {@link SensorOrientationFeature} instance to set.
+ */
+ public void setSensorOrientation(SensorOrientationFeature sensorOrientation) {
+ this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation);
+ }
+
+ /**
+ * Gets the zoom level feature if it has been set.
+ *
+ * @return the zoom level feature.
+ */
+ public ZoomLevelFeature getZoomLevel() {
+ return (ZoomLevelFeature) featureMap.get(ZOOM_LEVEL);
+ }
+
+ /**
+ * Sets the instance of the zoom level feature.
+ *
+ * @param zoomLevel the {@link ZoomLevelFeature} instance to set.
+ */
+ public void setZoomLevel(ZoomLevelFeature zoomLevel) {
+ this.featureMap.put(ZOOM_LEVEL, zoomLevel);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java
new file mode 100644
index 0000000..b6b64f9
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java
@@ -0,0 +1,16 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features;
+
+/** Represents a point on an x/y axis. */
+public class Point {
+ public final Double x;
+ public final Double y;
+
+ public Point(Double x, Double y) {
+ this.x = x;
+ this.y = y;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java
new file mode 100644
index 0000000..1789a96
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java
@@ -0,0 +1,83 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.autofocus;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+
+/** Controls the auto focus configuration on the {@see anddroid.hardware.camera2} API. */
+public class AutoFocusFeature extends CameraFeature<FocusMode> {
+ private FocusMode currentSetting = FocusMode.auto;
+
+ // When switching recording modes this feature is re-created with the appropriate setting here.
+ private final boolean recordingVideo;
+
+ /**
+ * Creates a new instance of the {@see AutoFocusFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ * @param recordingVideo Indicates whether the camera is currently recording video.
+ */
+ public AutoFocusFeature(CameraProperties cameraProperties, boolean recordingVideo) {
+ super(cameraProperties);
+ this.recordingVideo = recordingVideo;
+ }
+
+ @Override
+ public String getDebugName() {
+ return "AutoFocusFeature";
+ }
+
+ @Override
+ public FocusMode getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(FocusMode value) {
+ this.currentSetting = value;
+ }
+
+ @Override
+ public boolean checkIsSupported() {
+ int[] modes = cameraProperties.getControlAutoFocusAvailableModes();
+
+ final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance();
+
+ // Check if the focal length of the lens is fixed. If the minimum focus distance == 0, then the
+ // focal length is fixed. The minimum focus distance can be null on some devices: https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE
+ boolean isFixedLength = minFocus == null || minFocus == 0;
+
+ return !isFixedLength
+ && !(modes.length == 0
+ || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF));
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ switch (currentSetting) {
+ case locked:
+ // When locking the auto-focus the camera device should do a one-time focus and afterwards
+ // set the auto-focus to idle. This is accomplished by setting the CONTROL_AF_MODE to
+ // CONTROL_AF_MODE_AUTO.
+ requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+ break;
+ case auto:
+ requestBuilder.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ recordingVideo
+ ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
+ : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ default:
+ break;
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java
new file mode 100644
index 0000000..56331b4
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java
@@ -0,0 +1,31 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.autofocus;
+
+// Mirrors focus_mode.dart
+public enum FocusMode {
+ auto("auto"),
+ locked("locked");
+
+ private final String strValue;
+
+ FocusMode(String strValue) {
+ this.strValue = strValue;
+ }
+
+ public static FocusMode getValueForString(String modeStr) {
+ for (FocusMode value : values()) {
+ if (value.strValue.equals(modeStr)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return strValue;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java
new file mode 100644
index 0000000..df08cd9
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java
@@ -0,0 +1,54 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.exposurelock;
+
+import android.hardware.camera2.CaptureRequest;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+
+/** Controls whether or not the exposure mode is currently locked or automatically metering. */
+public class ExposureLockFeature extends CameraFeature<ExposureMode> {
+
+ private ExposureMode currentSetting = ExposureMode.auto;
+
+ /**
+ * Creates a new instance of the {@see ExposureLockFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ */
+ public ExposureLockFeature(CameraProperties cameraProperties) {
+ super(cameraProperties);
+ }
+
+ @Override
+ public String getDebugName() {
+ return "ExposureLockFeature";
+ }
+
+ @Override
+ public ExposureMode getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(ExposureMode value) {
+ this.currentSetting = value;
+ }
+
+ // Available on all devices.
+ @Override
+ public boolean checkIsSupported() {
+ return true;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, currentSetting == ExposureMode.locked);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java
new file mode 100644
index 0000000..2971fb2
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java
@@ -0,0 +1,40 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.exposurelock;
+
+// Mirrors exposure_mode.dart
+public enum ExposureMode {
+ auto("auto"),
+ locked("locked");
+
+ private final String strValue;
+
+ ExposureMode(String strValue) {
+ this.strValue = strValue;
+ }
+
+ /**
+ * Tries to convert the supplied string into an {@see ExposureMode} enum value.
+ *
+ * <p>When the supplied string doesn't match a valid {@see ExposureMode} enum value, null is
+ * returned.
+ *
+ * @param modeStr String value to convert into an {@see ExposureMode} enum value.
+ * @return Matching {@see ExposureMode} enum value, or null if no match is found.
+ */
+ public static ExposureMode getValueForString(String modeStr) {
+ for (ExposureMode value : values()) {
+ if (value.strValue.equals(modeStr)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return strValue;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java
new file mode 100644
index 0000000..d5a9fcd
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.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.camera.features.exposureoffset;
+
+import android.hardware.camera2.CaptureRequest;
+import android.util.Range;
+import androidx.annotation.NonNull;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+
+/** Controls the exposure offset making the resulting image brighter or darker. */
+public class ExposureOffsetFeature extends CameraFeature<Double> {
+
+ private double currentSetting = 0;
+
+ /**
+ * Creates a new instance of the {@link ExposureOffsetFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ */
+ public ExposureOffsetFeature(CameraProperties cameraProperties) {
+ super(cameraProperties);
+ }
+
+ @Override
+ public String getDebugName() {
+ return "ExposureOffsetFeature";
+ }
+
+ @Override
+ public Double getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(@NonNull Double value) {
+ double stepSize = getExposureOffsetStepSize();
+ this.currentSetting = value / stepSize;
+ }
+
+ // Available on all devices.
+ @Override
+ public boolean checkIsSupported() {
+ return true;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting);
+ }
+
+ /**
+ * Returns the minimum exposure offset.
+ *
+ * @return double Minimum exposure offset.
+ */
+ public double getMinExposureOffset() {
+ Range<Integer> range = cameraProperties.getControlAutoExposureCompensationRange();
+ double minStepped = range == null ? 0 : range.getLower();
+ double stepSize = getExposureOffsetStepSize();
+ return minStepped * stepSize;
+ }
+
+ /**
+ * Returns the maximum exposure offset.
+ *
+ * @return double Maximum exposure offset.
+ */
+ public double getMaxExposureOffset() {
+ Range<Integer> range = cameraProperties.getControlAutoExposureCompensationRange();
+ double maxStepped = range == null ? 0 : range.getUpper();
+ double stepSize = getExposureOffsetStepSize();
+ return maxStepped * stepSize;
+ }
+
+ /**
+ * Returns the smallest step by which the exposure compensation can be changed.
+ *
+ * <p>Example: if this has a value of 0.5, then an aeExposureCompensation setting of -2 means that
+ * the actual AE offset is -1. More details can be found in the official Android documentation:
+ * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics.html#CONTROL_AE_COMPENSATION_STEP
+ *
+ * @return double Smallest step by which the exposure compensation can be changed.
+ */
+ public double getExposureOffsetStepSize() {
+ return cameraProperties.getControlAutoExposureCompensationStep();
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java
new file mode 100644
index 0000000..336e756
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java
@@ -0,0 +1,99 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.exposurepoint;
+
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.util.Size;
+import androidx.annotation.NonNull;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.CameraRegionUtils;
+import io.flutter.plugins.camera.features.CameraFeature;
+import io.flutter.plugins.camera.features.Point;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+
+/** Exposure point controls where in the frame exposure metering will come from. */
+public class ExposurePointFeature extends CameraFeature<Point> {
+
+ private Size cameraBoundaries;
+ private Point exposurePoint;
+ private MeteringRectangle exposureRectangle;
+ private final SensorOrientationFeature sensorOrientationFeature;
+
+ /**
+ * Creates a new instance of the {@link ExposurePointFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ */
+ public ExposurePointFeature(
+ CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) {
+ super(cameraProperties);
+ this.sensorOrientationFeature = sensorOrientationFeature;
+ }
+
+ /**
+ * Sets the camera boundaries that are required for the exposure point feature to function.
+ *
+ * @param cameraBoundaries - The camera boundaries to set.
+ */
+ public void setCameraBoundaries(@NonNull Size cameraBoundaries) {
+ this.cameraBoundaries = cameraBoundaries;
+ this.buildExposureRectangle();
+ }
+
+ @Override
+ public String getDebugName() {
+ return "ExposurePointFeature";
+ }
+
+ @Override
+ public Point getValue() {
+ return exposurePoint;
+ }
+
+ @Override
+ public void setValue(Point value) {
+ this.exposurePoint = (value == null || value.x == null || value.y == null) ? null : value;
+ this.buildExposureRectangle();
+ }
+
+ // Whether or not this camera can set the exposure point.
+ @Override
+ public boolean checkIsSupported() {
+ Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure();
+ return supportedRegions != null && supportedRegions > 0;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+ requestBuilder.set(
+ CaptureRequest.CONTROL_AE_REGIONS,
+ exposureRectangle == null ? null : new MeteringRectangle[] {exposureRectangle});
+ }
+
+ private void buildExposureRectangle() {
+ if (this.cameraBoundaries == null) {
+ throw new AssertionError(
+ "The cameraBoundaries should be set (using `ExposurePointFeature.setCameraBoundaries(Size)`) before updating the exposure point.");
+ }
+ if (this.exposurePoint == null) {
+ this.exposureRectangle = null;
+ } else {
+ PlatformChannel.DeviceOrientation orientation =
+ this.sensorOrientationFeature.getLockedCaptureOrientation();
+ if (orientation == null) {
+ orientation =
+ this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation();
+ }
+ this.exposureRectangle =
+ CameraRegionUtils.convertPointToMeteringRectangle(
+ this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation);
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java
new file mode 100644
index 0000000..054c81f
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java
@@ -0,0 +1,75 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.flash;
+
+import android.hardware.camera2.CaptureRequest;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+
+/** Controls the flash configuration on the {@link android.hardware.camera2} API. */
+public class FlashFeature extends CameraFeature<FlashMode> {
+ private FlashMode currentSetting = FlashMode.auto;
+
+ /**
+ * Creates a new instance of the {@link FlashFeature}.
+ *
+ * @param cameraProperties Collection of characteristics for the current camera device.
+ */
+ public FlashFeature(CameraProperties cameraProperties) {
+ super(cameraProperties);
+ }
+
+ @Override
+ public String getDebugName() {
+ return "FlashFeature";
+ }
+
+ @Override
+ public FlashMode getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(FlashMode value) {
+ this.currentSetting = value;
+ }
+
+ @Override
+ public boolean checkIsSupported() {
+ Boolean available = cameraProperties.getFlashInfoAvailable();
+ return available != null && available;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ switch (currentSetting) {
+ case off:
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+ requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
+ break;
+
+ case always:
+ requestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+ requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
+ break;
+
+ case torch:
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+ requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
+ break;
+
+ case auto:
+ requestBuilder.set(
+ CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
+ break;
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java
new file mode 100644
index 0000000..788c768
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java
@@ -0,0 +1,40 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.flash;
+
+// Mirrors flash_mode.dart
+public enum FlashMode {
+ off("off"),
+ auto("auto"),
+ always("always"),
+ torch("torch");
+
+ private final String strValue;
+
+ FlashMode(String strValue) {
+ this.strValue = strValue;
+ }
+
+ /**
+ * Tries to convert the supplied string into a {@see FlashMode} enum value.
+ *
+ * <p>When the supplied string doesn't match a valid {@see FlashMode} enum value, null is
+ * returned.
+ *
+ * @param modeStr String value to convert into an {@see FlashMode} enum value.
+ * @return Matching {@see FlashMode} enum value, or null if no match is found.
+ */
+ public static FlashMode getValueForString(String modeStr) {
+ for (FlashMode value : values()) {
+ if (value.strValue.equals(modeStr)) return value;
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return strValue;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java
new file mode 100644
index 0000000..a3a0172
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java
@@ -0,0 +1,99 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.focuspoint;
+
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.util.Size;
+import androidx.annotation.NonNull;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.CameraRegionUtils;
+import io.flutter.plugins.camera.features.CameraFeature;
+import io.flutter.plugins.camera.features.Point;
+import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
+
+/** Focus point controls where in the frame focus will come from. */
+public class FocusPointFeature extends CameraFeature<Point> {
+
+ private Size cameraBoundaries;
+ private Point focusPoint;
+ private MeteringRectangle focusRectangle;
+ private final SensorOrientationFeature sensorOrientationFeature;
+
+ /**
+ * Creates a new instance of the {@link FocusPointFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ */
+ public FocusPointFeature(
+ CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) {
+ super(cameraProperties);
+ this.sensorOrientationFeature = sensorOrientationFeature;
+ }
+
+ /**
+ * Sets the camera boundaries that are required for the focus point feature to function.
+ *
+ * @param cameraBoundaries - The camera boundaries to set.
+ */
+ public void setCameraBoundaries(@NonNull Size cameraBoundaries) {
+ this.cameraBoundaries = cameraBoundaries;
+ this.buildFocusRectangle();
+ }
+
+ @Override
+ public String getDebugName() {
+ return "FocusPointFeature";
+ }
+
+ @Override
+ public Point getValue() {
+ return focusPoint;
+ }
+
+ @Override
+ public void setValue(Point value) {
+ this.focusPoint = value == null || value.x == null || value.y == null ? null : value;
+ this.buildFocusRectangle();
+ }
+
+ // Whether or not this camera can set the focus point.
+ @Override
+ public boolean checkIsSupported() {
+ Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus();
+ return supportedRegions != null && supportedRegions > 0;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+ requestBuilder.set(
+ CaptureRequest.CONTROL_AF_REGIONS,
+ focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle});
+ }
+
+ private void buildFocusRectangle() {
+ if (this.cameraBoundaries == null) {
+ throw new AssertionError(
+ "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point.");
+ }
+ if (this.focusPoint == null) {
+ this.focusRectangle = null;
+ } else {
+ PlatformChannel.DeviceOrientation orientation =
+ this.sensorOrientationFeature.getLockedCaptureOrientation();
+ if (orientation == null) {
+ orientation =
+ this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation();
+ }
+ this.focusRectangle =
+ CameraRegionUtils.convertPointToMeteringRectangle(
+ this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation);
+ }
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java
new file mode 100644
index 0000000..500f2aa
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java
@@ -0,0 +1,87 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.fpsrange;
+
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+import android.util.Range;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+
+/**
+ * Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2}
+ * API.
+ */
+public class FpsRangeFeature extends CameraFeature<Range<Integer>> {
+ private static final Range<Integer> MAX_PIXEL4A_RANGE = new Range<>(30, 30);
+ private Range<Integer> currentSetting;
+
+ /**
+ * Creates a new instance of the {@link FpsRangeFeature}.
+ *
+ * @param cameraProperties Collection of characteristics for the current camera device.
+ */
+ public FpsRangeFeature(CameraProperties cameraProperties) {
+ super(cameraProperties);
+
+ if (isPixel4A()) {
+ // HACK: There is a bug in the Pixel 4A where it cannot support 60fps modes
+ // even though they are reported as supported by
+ // `getControlAutoExposureAvailableTargetFpsRanges`.
+ // For max device compatibility we will keep FPS under 60 even if they report they are
+ // capable of achieving 60 fps. Highest working FPS is 30.
+ // https://issuetracker.google.com/issues/189237151
+ currentSetting = MAX_PIXEL4A_RANGE;
+ } else {
+ Range<Integer>[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges();
+
+ if (ranges != null) {
+ for (Range<Integer> range : ranges) {
+ int upper = range.getUpper();
+
+ if (upper >= 10) {
+ if (currentSetting == null || upper > currentSetting.getUpper()) {
+ currentSetting = range;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isPixel4A() {
+ return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a");
+ }
+
+ @Override
+ public String getDebugName() {
+ return "FpsRangeFeature";
+ }
+
+ @Override
+ public Range<Integer> getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(Range<Integer> value) {
+ this.currentSetting = value;
+ }
+
+ // Always supported
+ @Override
+ public boolean checkIsSupported() {
+ return true;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java
new file mode 100644
index 0000000..408575b
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java
@@ -0,0 +1,91 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.noisereduction;
+
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+import java.util.HashMap;
+
+/**
+ * This can either be enabled or disabled. Only full capability devices can set this to off. Legacy
+ * and full support the fast mode.
+ * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES
+ */
+public class NoiseReductionFeature extends CameraFeature<NoiseReductionMode> {
+ private NoiseReductionMode currentSetting = NoiseReductionMode.fast;
+
+ private final HashMap<NoiseReductionMode, Integer> NOISE_REDUCTION_MODES = new HashMap<>();
+
+ /**
+ * Creates a new instance of the {@link NoiseReductionFeature}.
+ *
+ * @param cameraProperties Collection of the characteristics for the current camera device.
+ */
+ public NoiseReductionFeature(CameraProperties cameraProperties) {
+ super(cameraProperties);
+ NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF);
+ NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST);
+ NOISE_REDUCTION_MODES.put(
+ NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY);
+ if (VERSION.SDK_INT >= VERSION_CODES.M) {
+ NOISE_REDUCTION_MODES.put(
+ NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL);
+ NOISE_REDUCTION_MODES.put(
+ NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG);
+ }
+ }
+
+ @Override
+ public String getDebugName() {
+ return "NoiseReductionFeature";
+ }
+
+ @Override
+ public NoiseReductionMode getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(NoiseReductionMode value) {
+ this.currentSetting = value;
+ }
+
+ @Override
+ public boolean checkIsSupported() {
+ /*
+ * Available settings: public static final int NOISE_REDUCTION_MODE_FAST = 1; public static
+ * final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; public static final int
+ * NOISE_REDUCTION_MODE_MINIMAL = 3; public static final int NOISE_REDUCTION_MODE_OFF = 0;
+ * public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4;
+ *
+ * <p>Full-capability camera devices will always support OFF and FAST. Camera devices that
+ * support YUV_REPROCESSING or PRIVATE_REPROCESSING will support ZERO_SHUTTER_LAG.
+ * Legacy-capability camera devices will only support FAST mode.
+ */
+
+ // Can be null on some devices.
+ int[] modes = cameraProperties.getAvailableNoiseReductionModes();
+
+ /// If there's at least one mode available then we are supported.
+ return modes != null && modes.length > 0;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ if (!checkIsSupported()) {
+ return;
+ }
+
+ Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting);
+
+ // Always use fast mode.
+ requestBuilder.set(
+ CaptureRequest.NOISE_REDUCTION_MODE, NOISE_REDUCTION_MODES.get(currentSetting));
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java
new file mode 100644
index 0000000..425a458
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java
@@ -0,0 +1,41 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.noisereduction;
+
+/** Only supports fast mode for now. */
+public enum NoiseReductionMode {
+ off("off"),
+ fast("fast"),
+ highQuality("highQuality"),
+ minimal("minimal"),
+ zeroShutterLag("zeroShutterLag");
+
+ private final String strValue;
+
+ NoiseReductionMode(String strValue) {
+ this.strValue = strValue;
+ }
+
+ /**
+ * Tries to convert the supplied string into a {@see NoiseReductionMode} enum value.
+ *
+ * <p>When the supplied string doesn't match a valid {@see NoiseReductionMode} enum value, null is
+ * returned.
+ *
+ * @param modeStr String value to convert into an {@see NoiseReductionMode} enum value.
+ * @return Matching {@see NoiseReductionMode} enum value, or null if no match is found.
+ */
+ public static NoiseReductionMode getValueForString(String modeStr) {
+ for (NoiseReductionMode value : values()) {
+ if (value.strValue.equals(modeStr)) return value;
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return strValue;
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java
new file mode 100644
index 0000000..0ec2fbe
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java
@@ -0,0 +1,269 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.resolution;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CaptureRequest;
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+import android.util.Size;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.plugins.camera.CameraProperties;
+import io.flutter.plugins.camera.features.CameraFeature;
+import java.util.List;
+
+/**
+ * Controls the resolutions configuration on the {@link android.hardware.camera2} API.
+ *
+ * <p>The {@link ResolutionFeature} is responsible for converting the platform independent {@link
+ * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties
+ * required to configure the resolution using the {@link android.hardware.camera2} API.
+ */
+public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
+ private Size captureSize;
+ private Size previewSize;
+ private CamcorderProfile recordingProfileLegacy;
+ private EncoderProfiles recordingProfile;
+ private ResolutionPreset currentSetting;
+ private int cameraId;
+
+ /**
+ * Creates a new instance of the {@link ResolutionFeature}.
+ *
+ * @param cameraProperties Collection of characteristics for the current camera device.
+ * @param resolutionPreset Platform agnostic enum containing resolution information.
+ * @param cameraName Camera identifier of the camera for which to configure the resolution.
+ */
+ public ResolutionFeature(
+ CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) {
+ super(cameraProperties);
+ this.currentSetting = resolutionPreset;
+ try {
+ this.cameraId = Integer.parseInt(cameraName, 10);
+ } catch (NumberFormatException e) {
+ this.cameraId = -1;
+ return;
+ }
+ configureResolution(resolutionPreset, cameraId);
+ }
+
+ /**
+ * Gets the {@link android.media.CamcorderProfile} containing the information to configure the
+ * resolution using the {@link android.hardware.camera2} API.
+ *
+ * @return Resolution information to configure the {@link android.hardware.camera2} API.
+ */
+ public CamcorderProfile getRecordingProfileLegacy() {
+ return this.recordingProfileLegacy;
+ }
+
+ public EncoderProfiles getRecordingProfile() {
+ return this.recordingProfile;
+ }
+
+ /**
+ * Gets the optimal preview size based on the configured resolution.
+ *
+ * @return The optimal preview size.
+ */
+ public Size getPreviewSize() {
+ return this.previewSize;
+ }
+
+ /**
+ * Gets the optimal capture size based on the configured resolution.
+ *
+ * @return The optimal capture size.
+ */
+ public Size getCaptureSize() {
+ return this.captureSize;
+ }
+
+ @Override
+ public String getDebugName() {
+ return "ResolutionFeature";
+ }
+
+ @Override
+ public ResolutionPreset getValue() {
+ return currentSetting;
+ }
+
+ @Override
+ public void setValue(ResolutionPreset value) {
+ this.currentSetting = value;
+ configureResolution(currentSetting, cameraId);
+ }
+
+ @Override
+ public boolean checkIsSupported() {
+ return cameraId >= 0;
+ }
+
+ @Override
+ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
+ // No-op: when setting a resolution there is no need to update the request builder.
+ }
+
+ @VisibleForTesting
+ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset)
+ throws IndexOutOfBoundsException {
+ if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
+ preset = ResolutionPreset.high;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ EncoderProfiles profile =
+ getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
+ List<EncoderProfiles.VideoProfile> videoProfiles = profile.getVideoProfiles();
+ EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);
+
+ if (defaultVideoProfile != null) {
+ return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ // TODO(camsim99): Suppression is currently safe because legacy code is used as a fallback for SDK >= S.
+ // This should be removed when reverting that fallback behavior: https://github.com/flutter/flutter/issues/119668.
+ CamcorderProfile profile =
+ getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset);
+ return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
+ }
+
+ /**
+ * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link
+ * ResolutionPreset}. Supports SDK < 31.
+ *
+ * @param cameraId Camera identifier which indicates the device's camera for which to select a
+ * {@link android.media.CamcorderProfile}.
+ * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link
+ * android.media.CamcorderProfile}.
+ * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied
+ * {@link ResolutionPreset}.
+ */
+ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy(
+ int cameraId, ResolutionPreset preset) {
+ if (cameraId < 0) {
+ throw new AssertionError(
+ "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers.");
+ }
+
+ switch (preset) {
+ // All of these cases deliberately fall through to get the best available profile.
+ case max:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
+ }
+ case ultraHigh:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P);
+ }
+ case veryHigh:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P);
+ }
+ case high:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P);
+ }
+ case medium:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P);
+ }
+ case low:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA);
+ }
+ default:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
+ return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
+ } else {
+ throw new IllegalArgumentException(
+ "No capture session available for current capture session.");
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset(
+ int cameraId, ResolutionPreset preset) {
+ if (cameraId < 0) {
+ throw new AssertionError(
+ "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers.");
+ }
+
+ String cameraIdString = Integer.toString(cameraId);
+
+ switch (preset) {
+ // All of these cases deliberately fall through to get the best available profile.
+ case max:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH);
+ }
+ case ultraHigh:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P);
+ }
+ case veryHigh:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P);
+ }
+ case high:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P);
+ }
+ case medium:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P);
+ }
+ case low:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA);
+ }
+ default:
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
+ return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW);
+ }
+
+ throw new IllegalArgumentException(
+ "No capture session available for current capture session.");
+ }
+ }
+
+ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId)
+ throws IndexOutOfBoundsException {
+ if (!checkIsSupported()) {
+ return;
+ }
+ boolean captureSizeCalculated = false;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ recordingProfileLegacy = null;
+ recordingProfile =
+ getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
+ List<EncoderProfiles.VideoProfile> videoProfiles = recordingProfile.getVideoProfiles();
+
+ EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);
+
+ if (defaultVideoProfile != null) {
+ captureSizeCalculated = true;
+ captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
+ }
+ }
+
+ if (!captureSizeCalculated) {
+ recordingProfile = null;
+ @SuppressWarnings("deprecation")
+ CamcorderProfile camcorderProfile =
+ getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset);
+ recordingProfileLegacy = camcorderProfile;
+ captureSize =
+ new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight);
+ }
+
+ previewSize = computeBestPreviewSize(cameraId, resolutionPreset);
+ }
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java
new file mode 100644
index 0000000..3593003
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java
@@ -0,0 +1,15 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.resolution;
+
+// Mirrors camera.dart
+public enum ResolutionPreset {
+ low,
+ medium,
+ high,
+ veryHigh,
+ ultraHigh,
+ max,
+}
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java
new file mode 100644
index 0000000..ec6fa13
--- /dev/null
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java
@@ -0,0 +1,335 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.camera.features.sensororientation;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
+import io.flutter.plugins.camera.DartMessenger;
+
+/**
+ * Support class to help to determine the media orientation based on the orientation of the device.
+ */
+public class DeviceOrientationManager {
+
+ private static final IntentFilter orientationIntentFilter =
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
+
+ private final Activity activity;
+ private final DartMessenger messenger;
+ private final boolean isFrontFacing;
+ private final int sensorOrientation;
+ private PlatformChannel.DeviceOrientation lastOrientation;
+ private BroadcastReceiver broadcastReceiver;
+
+ /** Factory method to create a device orientation manager. */
+ public static DeviceOrientationManager create(
+ @NonNull Activity activity,
+ @NonNull DartMessenger messenger,
+ boolean isFrontFacing,
+ int sensorOrientation) {
+ return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation);
+ }
+
+ private DeviceOrientationManager(
+ @NonNull Activity activity,
+ @NonNull DartMessenger messenger,
+ boolean isFrontFacing,
+ int sensorOrientation) {
+ this.activity = activity;
+ this.messenger = messenger;
+ this.isFrontFacing = isFrontFacing;
+ this.sensorOrientation = sensorOrientation;
+ }
+
+ /**
+ * Starts listening to the device's sensors or UI for orientation updates.
+ *
+ * <p>When orientation information is updated the new orientation is send to the client using the
+ * {@link DartMessenger}. This latest value can also be retrieved through the {@link
+ * #getVideoOrientation()} accessor.
+ *
+ * <p>If the device's ACCELEROMETER_ROTATION setting is enabled the {@link
+ * DeviceOrientationManager} will report orientation updates based on the sensor information. If
+ * the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to
+ * the deliver orientation updates based on the UI orientation.
+ */
+ public void start() {
+ if (broadcastReceiver != null) {
+ return;
+ }
+ broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ handleUIOrientationChange();
+ }
+ };
+ activity.registerReceiver(broadcastReceiver, orientationIntentFilter);
+ broadcastReceiver.onReceive(activity, null);
+ }
+
+ /** Stops listening for orientation updates. */
+ public void stop() {
+ if (broadcastReceiver == null) {
+ return;
+ }
+ activity.unregisterReceiver(broadcastReceiver);
+ broadcastReceiver = null;
+ }
+
+ /**
+ * Returns the device's photo orientation in degrees based on the sensor orientation and the last
+ * known UI orientation.
+ *
+ * <p>Returns one of 0, 90, 180 or 270.
+ *
+ * @return The device's photo orientation in degrees.
+ */
+ public int getPhotoOrientation() {
+ return this.getPhotoOrientation(this.las