diff --git a/.ci.yaml b/.ci.yaml index e44c1479c26b..275a8f1a179e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -12,6 +12,27 @@ platform_properties: linux: properties: os: Linux + linux_desktop: + properties: + os: Ubuntu + cores: "8" + device_type: none + dependencies: >- + [ + {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "curl", "version": "version:7.64.0"} + ] + linux_web: + properties: + os: Ubuntu + cores: "8" + device_type: none + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:114.0"} + ] windows: properties: dependencies: > @@ -48,7 +69,7 @@ platform_properties: } targets: - ### Linux tasks ### + ### Linux-host general tasks ### - name: Linux repo_tools_tests recipe: packages/packages timeout: 30 @@ -58,6 +79,78 @@ targets: channel: master version_file: flutter_master.version + - name: Linux analyze master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze.yaml + channel: master + version_file: flutter_master.version + + - name: Linux analyze stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze.yaml + channel: stable + version_file: flutter_stable.version + + - name: Linux analyze_downgraded master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze_downgraded.yaml + channel: master + version_file: flutter_master.version + + - name: Linux analyze_downgraded stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze_downgraded.yaml + channel: stable + version_file: flutter_stable.version + + ### Web tasks ### + - name: Linux_web web_build_all_packages master + recipe: packages/packages + timeout: 30 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: web_build_all_packages.yaml + channel: master + + - name: Linux_web web_build_all_packages stable + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_stable.version + target_file: web_build_all_packages.yaml + channel: stable + + ### Linux desktop tasks + - name: Linux_desktop build_all_packages master + recipe: packages/packages + timeout: 30 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: linux_build_all_packages.yaml + channel: master + + - name: Linux_desktop build_all_packages stable + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_stable.version + target_file: linux_build_all_packages.yaml + channel: stable + ### iOS+macOS tasks ### # TODO(stuartmorgan): Move this to ARM once google_maps_flutter has ARM # support. `pod lint` makes a synthetic target that doesn't respect the @@ -144,7 +237,6 @@ targets: target_file: ios_build_all_packages.yaml - name: Mac_x64 ios_build_all_packages stable - bringup: true # New target recipe: packages/packages timeout: 30 properties: diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 9ddb35ef55a1..2c19000f05f9 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -8a5c22e282db5f45ffdef24752520894f18227b9 +fc8856eb80d306ac40563582a1212a07141d9001 diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index ec36783b4ac1..f99b2a7b9d5e 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -682aa387cfe4fbd71ccd5418b2c2a075729a1c66 +796c8ef79279f9c774545b3771238c3098dbefab diff --git a/.ci/scripts/analyze_repo_tools.sh b/.ci/scripts/analyze_repo_tools.sh new file mode 100755 index 000000000000..df2a87c04a98 --- /dev/null +++ b/.ci/scripts/analyze_repo_tools.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# 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. +set -e + +cd script/tool +dart analyze --fatal-infos diff --git a/.ci/scripts/dart_unit_tests_win32.sh b/.ci/scripts/dart_unit_tests_win32.sh index 5fbe4764f6b3..2cd451c45caa 100755 --- a/.ci/scripts/dart_unit_tests_win32.sh +++ b/.ci/scripts/dart_unit_tests_win32.sh @@ -4,6 +4,6 @@ # found in the LICENSE file. set -e -dart ./script/tool/bin/flutter_plugin_tools.dart test \ +dart ./script/tool/bin/flutter_plugin_tools.dart dart-test \ --exclude=script/configs/windows_unit_tests_exceptions.yaml \ --packages-for-branch --log-timing diff --git a/.ci/scripts/pathified_analyze.sh b/.ci/scripts/pathified_analyze.sh new file mode 100755 index 000000000000..2942cf7d57b8 --- /dev/null +++ b/.ci/scripts/pathified_analyze.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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. +set -e + +# Pathify the dependencies on changed packages (excluding major version +# changes, which won't affect clients). +./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates +# This uses --run-on-dirty-packages rather than --packages-for-branch +# since only the packages changed by 'make-deps-path-based' need to be +# re-checked. +dart ./script/tool/bin/flutter_plugin_tools.dart analyze --run-on-dirty-packages \ + --log-timing --custom-analysis=script/configs/custom_analysis.yaml +# Restore the tree to a clean state, to avoid accidental issues if +# other script steps are added to the enclosing task. +git checkout . diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml new file mode 100644 index 000000000000..25791d3c898c --- /dev/null +++ b/.ci/targets/analyze.yaml @@ -0,0 +1,15 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: analyze repo tools + script: .ci/scripts/analyze_repo_tools.sh + - name: analyze + script: script/tool_runner.sh + # DO NOT change the custom-analysis argument here without changing the Dart repo. + # See the comment in script/configs/custom_analysis.yaml for details. + args: ["analyze", "--custom-analysis=script/configs/custom_analysis.yaml"] + # Re-run analysis with path-based dependencies to ensure that publishing + # the changes won't break analysis of other packages in the respository + # that depend on it. + - name: analyze - pathified + script: .ci/scripts/pathified_analyze.sh diff --git a/.ci/targets/analyze_downgraded.yaml b/.ci/targets/analyze_downgraded.yaml new file mode 100644 index 000000000000..46bc111b30c5 --- /dev/null +++ b/.ci/targets/analyze_downgraded.yaml @@ -0,0 +1,10 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + # Does a sanity check that packages pass analysis with the lowest possible + # versions of all dependencies. This is to catch cases where we add use of + # new APIs but forget to update minimum versions of dependencies to where + # those APIs are introduced. + - name: analyze - downgraded + script: script/tool_runner.sh + args: ["analyze", "--downgrade", "--custom-analysis=script/configs/custom_analysis.yaml"] diff --git a/.ci/targets/linux_build_all_packages.yaml b/.ci/targets/linux_build_all_packages.yaml new file mode 100644 index 000000000000..b54f7b1e56c2 --- /dev/null +++ b/.ci/targets/linux_build_all_packages.yaml @@ -0,0 +1,11 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_packages app + script: .ci/scripts/create_all_packages_app.sh + - name: build all_packages for Linux debug + script: .ci/scripts/build_all_packages_app.sh + args: ["linux", "debug"] + - name: build all_packages for Linux release + script: .ci/scripts/build_all_packages_app.sh + args: ["linux", "release"] diff --git a/.ci/targets/web_build_all_packages.yaml b/.ci/targets/web_build_all_packages.yaml new file mode 100644 index 000000000000..d3b7ae010725 --- /dev/null +++ b/.ci/targets/web_build_all_packages.yaml @@ -0,0 +1,10 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_packages app + script: .ci/scripts/create_all_packages_app.sh + # No debug version, unlike the other platforms, since web does not support + # debug builds. + - name: build all_packages app for Web release + script: .ci/scripts/build_all_packages_app.sh + args: ["web", "release"] diff --git a/.cirrus.yml b/.cirrus.yml index c8a7af48cce9..0808a1cabb52 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -22,13 +22,6 @@ tool_setup_template: &TOOL_SETUP_TEMPLATE tool_setup_script: - .ci/scripts/prepare_tool.sh -macos_template: &MACOS_TEMPLATE - # Only one macOS task can run in parallel without credits, so use them for - # PRs on macOS. - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:14.3 - flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE upgrade_flutter_script: # Channels that are part of our normal test matrix use a pinned, @@ -183,22 +176,7 @@ task: env: CIRRUS_CLONE_SUBMODULES: true script: ./script/tool_runner.sh update-excerpts --fail-on-change - ### Web tasks ### - - name: web-build_all_packages - env: - BUILD_ALL_ARGS: "web" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Linux desktop tasks ### - - name: linux-build_all_packages - env: - BUILD_ALL_ARGS: "linux" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PACKAGES_APP_TEMPLATE - name: linux-platform_tests # Don't run full platform tests on both channels in pre-submit. skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' @@ -238,13 +216,13 @@ task: CHANNEL: "master" CHANNEL: "stable" unit_test_script: - - ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml + - ./script/tool_runner.sh dart-test --exclude=script/configs/dart_unit_tests_exceptions.yaml pathified_unit_test_script: # Run tests with path-based dependencies to ensure that publishing # the changes won't break tests of other packages in the repository # that depend on it. - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates - - $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml + - $PLUGIN_TOOL_COMMAND dart-test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml - name: linux-custom_package_tests env: PATH: $PATH:/usr/local/bin @@ -335,24 +313,3 @@ task: - cd ../.. - flutter packages get - dart testing/web_benchmarks_test.dart - -# macOS tasks. -task: - << : *FLUTTER_UPGRADE_TEMPLATE - << : *MACOS_TEMPLATE - matrix: - - name: macos-custom_package_tests - env: - PATH: $PATH:/usr/local/bin - matrix: - CHANNEL: "master" - CHANNEL: "stable" - # Create an iPhone 13, to match what is available on LUCI, since Pigeon tests - # currently have a hard-coded device. - create_simulator_script: - - xcrun simctl list - - xcrun simctl create "iPhone 13" com.apple.CoreSimulator.SimDeviceType.iPhone-13 com.apple.CoreSimulator.SimRuntime.iOS-16-4 - local_tests_script: - # script/configs/linux_only_custom_test.yaml - # Custom tests need Chrome for these packages. (They run in linux-custom_package_tests) - - ./script/tool_runner.sh custom-test --exclude=script/configs/linux_only_custom_test.yaml diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index e48c050fb88d..a474d2c4b5e4 100644 --- a/.github/workflows/pull_request_label.yml +++ b/.github/workflows/pull_request_label.yml @@ -21,7 +21,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@ba790c862c380240c6d5e7427be5ace9a05c754b + - uses: actions/labeler@9fcb2c2f5584144ca754f8bfe8c6f81e77753375 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/CODEOWNERS b/CODEOWNERS index e81c2fb6ae84..dfc8a29c749d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,12 +83,14 @@ packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz # - Linux packages/file_selector/file_selector_linux/** @cbracken +packages/image_picker/image_picker_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/image_picker/image_picker_macos/** @cbracken packages/url_launcher/url_launcher_macos/** @cbracken # - Windows diff --git a/packages/animations/example/ios/Runner/Info.plist b/packages/animations/example/ios/Runner/Info.plist index a060db61e461..1251b459385b 100644 --- a/packages/animations/example/ios/Runner/Info.plist +++ b/packages/animations/example/ios/Runner/Info.plist @@ -39,7 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist index bacd9e54f1ea..13e41200aca1 100644 --- a/packages/camera/camera/example/ios/Runner/Info.plist +++ b/packages/camera/camera/example/ios/Runner/Info.plist @@ -50,8 +50,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 8bfdd7f0b1fe..c853b8a4b1be 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Fixes unawaited_futures violations. +* Removes duplicate line in `MediaRecorderBuilder.java`. ## 0.10.8+2 diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index ef8d9e297674..24a736e0e908 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -51,6 +51,7 @@ android { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true unitTests.all { + jvmArgs "-Xmx1g" testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" outputs.upToDateWhen {false} @@ -65,5 +66,5 @@ dependencies { 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' + testImplementation 'org.robolectric:robolectric:4.10.3' } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index f55552edf898..966019bb1431 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -92,7 +92,6 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate()); mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); - mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); } else if (camcorderProfile != null) { mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); if (enableAudio) { diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index dbc352d697a4..fe4dcd795fed 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -91,8 +91,9 @@ public void beforeLegacy() { public void before() { mockProfileLow = mock(EncoderProfiles.class); EncoderProfiles mockProfile = mock(EncoderProfiles.class); - EncoderProfiles.VideoProfile mockVideoProfile = mock(EncoderProfiles.VideoProfile.class); - List mockVideoProfilesList = List.of(mockVideoProfile); + List mockVideoProfilesList = + new ArrayList(); + mockVideoProfilesList.add(null); mockedStaticProfile .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_HIGH)) @@ -117,8 +118,6 @@ public void before() { .thenReturn(mockProfileLow); when(mockProfile.getVideoProfiles()).thenReturn(mockVideoProfilesList); - when(mockVideoProfile.getHeight()).thenReturn(100); - when(mockVideoProfile.getWidth()).thenReturn(100); } @After @@ -386,7 +385,7 @@ public void computeBestPreviewSize_shouldUseLegacyBehaviorWhenEncoderProfilesNul @Config(minSdk = 31) @Test public void resolutionFeatureShouldUseLegacyBehaviorWhenEncoderProfilesNull() { - beforeLegacy(); + before(); try (MockedStatic mockedResolutionFeature = mockStatic(ResolutionFeature.class)) { mockedResolutionFeature diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 6cc58ee823d9..f37de01f5e7c 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -78,9 +78,9 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabledLegacy() throw public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { EncoderProfiles recorderProfile = mock(EncoderProfiles.class); List mockVideoProfiles = - List.of(mock(EncoderProfiles.VideoProfile.class)); + List.of(getEmptyEncoderProfilesVideoProfile()); List mockAudioProfiles = - List.of(mock(EncoderProfiles.AudioProfile.class)); + List.of(getEmptyEncoderProfilesAudioProfile()); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); @@ -172,9 +172,9 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabledLegacy() throws public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { EncoderProfiles recorderProfile = mock(EncoderProfiles.class); List mockVideoProfiles = - List.of(mock(EncoderProfiles.VideoProfile.class)); + List.of(getEmptyEncoderProfilesVideoProfile()); List mockAudioProfiles = - List.of(mock(EncoderProfiles.AudioProfile.class)); + List.of(getEmptyEncoderProfilesAudioProfile()); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); @@ -224,4 +224,32 @@ private CamcorderProfile getEmptyCamcorderProfile() { return null; } + + private EncoderProfiles.VideoProfile getEmptyEncoderProfilesVideoProfile() { + try { + Constructor constructor = + EncoderProfiles.VideoProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } + + private EncoderProfiles.AudioProfile getEmptyEncoderProfilesAudioProfile() { + try { + Constructor constructor = + EncoderProfiles.AudioProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } } diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 5912f9834548..2980f18c19bf 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+7 + +* Updates Guava version to 32.0.1. + ## 0.5.0+6 * Updates Guava version to 32.0.0. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 83871babe062..f10f13336081 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-video:${camerax_version}" - implementation 'com.google.guava:guava:32.0.0-android' + implementation 'com.google.guava:guava:32.0.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 5e4ea92ba6c5..d0364186b281 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+6 +version: 0.5.0+7 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist index ca93baac7012..adb62fb7803d 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -52,8 +52,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/dynamic_layouts/example/ios/Runner/Info.plist b/packages/dynamic_layouts/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/dynamic_layouts/example/ios/Runner/Info.plist +++ b/packages/dynamic_layouts/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist index aa6d84f63af1..28ab78e39981 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/file_selector/file_selector/example/ios/Runner/Info.plist b/packages/file_selector/file_selector/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/file_selector/file_selector/example/ios/Runner/Info.plist +++ b/packages/file_selector/file_selector/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist index 2bf6e923d3b6..67d621a7fc80 100644 --- a/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 6ff69f593d36..857f82fa02ae 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.5 + +* Added support for Right-to-left (RTL) directionality. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 0.1.4 * Use Material 3 NavigationBar instead of BottomNavigationBar diff --git a/packages/flutter_adaptive_scaffold/README.md b/packages/flutter_adaptive_scaffold/README.md index 612e62c4bc92..e9324afbf1b1 100644 --- a/packages/flutter_adaptive_scaffold/README.md +++ b/packages/flutter_adaptive_scaffold/README.md @@ -137,110 +137,108 @@ displayed and the entrance animation and exit animation. ```dart - // AdaptiveLayout has a number of slots that take SlotLayouts and these - // SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs. - return AdaptiveLayout( - // Primary navigation config has nothing from 0 to 600 dp screen width, - // then an unextended NavigationRail with no labels and just icons then an - // extended NavigationRail with both icons and labels. - primaryNavigation: SlotLayout( - config: { - Breakpoints.medium: SlotLayout.from( - inAnimation: AdaptiveScaffold.leftOutIn, - key: const Key('Primary Navigation Medium'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - selectedIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - leading: const Icon(Icons.menu), - destinations: destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, - ), - ), - Breakpoints.large: SlotLayout.from( - key: const Key('Primary Navigation Large'), - inAnimation: AdaptiveScaffold.leftOutIn, - builder: (_) => AdaptiveScaffold.standardNavigationRail( - selectedIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - extended: true, - leading: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: const [ - Text( - 'REPLY', - style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), - ), - Icon(Icons.menu_open) - ], - ), - destinations: destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - trailing: trailingNavRail, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, - ), - ), - }, +// AdaptiveLayout has a number of slots that take SlotLayouts and these +// SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs. +return AdaptiveLayout( + // Primary navigation config has nothing from 0 to 600 dp screen width, + // then an unextended NavigationRail with no labels and just icons then an + // extended NavigationRail with both icons and labels. + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + inAnimation: AdaptiveScaffold.leftOutIn, + key: const Key('Primary Navigation Medium'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + leading: const Icon(Icons.menu), + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, + ), ), - // Body switches between a ListView and a GridView from small to medium - // breakpoints and onwards. - body: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('Body Small'), - builder: (_) => ListView.builder( - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - ), + Breakpoints.large: SlotLayout.from( + key: const Key('Primary Navigation Large'), + inAnimation: AdaptiveScaffold.leftOutIn, + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + extended: true, + leading: const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + 'REPLY', + style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), + ), + Icon(Icons.menu_open) + ], ), - Breakpoints.mediumAndUp: SlotLayout.from( - key: const Key('Body Medium'), - builder: (_) => - GridView.count(crossAxisCount: 2, children: children), - ) - }, + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + trailing: trailingNavRail, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, + ), ), - // BottomNavigation is only active in small views defined as under 600 dp - // width. - bottomNavigation: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('Bottom Navigation Small'), - inAnimation: AdaptiveScaffold.bottomToTop, - outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations, - currentIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - ), - ) - }, + }, + ), + // Body switches between a ListView and a GridView from small to medium + // breakpoints and onwards. + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('Body Small'), + builder: (_) => ListView.builder( + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ), ), - ); - } -} + Breakpoints.mediumAndUp: SlotLayout.from( + key: const Key('Body Medium'), + builder: (_) => + GridView.count(crossAxisCount: 2, children: children), + ) + }, + ), + // BottomNavigation is only active in small views defined as under 600 dp + // width. + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('Bottom Navigation Small'), + inAnimation: AdaptiveScaffold.bottomToTop, + outAnimation: AdaptiveScaffold.topToBottom, + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + currentIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + ), + ) + }, + ), +); ``` Both of the examples shown here produce the same output: diff --git a/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist b/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist +++ b/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart index eb9522041fce..77b5cd78d9fe 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(goderbauer): Remove this ignore when this package requires Flutter 3.8 or later. -// ignore_for_file: prefer_const_constructors - import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; @@ -58,8 +55,8 @@ class _MyHomePageState extends State { children: [ const Divider(color: Colors.black), const SizedBox(height: 10), - Row( - children: const [ + const Row( + children: [ SizedBox(width: 27), Text('Folders', style: TextStyle(fontSize: 16)), ], @@ -74,8 +71,8 @@ class _MyHomePageState extends State { iconSize: 21, ), const SizedBox(width: 21), - Flexible( - child: const Text( + const Flexible( + child: Text( 'Freelance', overflow: TextOverflow.ellipsis, ), @@ -92,8 +89,8 @@ class _MyHomePageState extends State { iconSize: 21, ), const SizedBox(width: 21), - Flexible( - child: const Text( + const Flexible( + child: Text( 'Mortgage', overflow: TextOverflow.ellipsis, ), @@ -198,9 +195,9 @@ class _MyHomePageState extends State { }); }, extended: true, - leading: Row( + leading: const Row( mainAxisAlignment: MainAxisAlignment.spaceAround, - children: const [ + children: [ Text( 'REPLY', style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), @@ -260,6 +257,6 @@ class _MyHomePageState extends State { }, ), ); - // #enddocregion + // #enddocregion Example } } diff --git a/packages/flutter_adaptive_scaffold/example/lib/main.dart b/packages/flutter_adaptive_scaffold/example/lib/main.dart index 994b9c8b835c..4aac90d8bc93 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/main.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/main.dart @@ -52,6 +52,9 @@ class _MyHomePageState extends State // the navigation elements. ValueNotifier showGridView = ValueNotifier(false); + // Override the application's directionality. + TextDirection directionalityOverride = TextDirection.ltr; + // The index of the selected mail card. int? selected; @@ -118,70 +121,96 @@ class _MyHomePageState extends State @override Widget build(BuildContext context) { - final Widget trailingNavRail = Column( - children: [ - const Divider(color: Colors.white, thickness: 1.5), - const SizedBox(height: 10), - Row(children: [ - const SizedBox(width: 22), - Text('Folders', - style: TextStyle(fontSize: 13, color: Colors.grey[700])) - ]), - const SizedBox(height: 22), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Text('Freelance', overflow: TextOverflow.ellipsis), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Text('Mortgage', overflow: TextOverflow.ellipsis), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Flexible( - child: Text('Taxes', overflow: TextOverflow.ellipsis)) - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, + final Widget trailingNavRail = Expanded( + child: Column( + children: [ + const Divider(color: Colors.white, thickness: 1.5), + const SizedBox(height: 10), + Row(children: [ + const SizedBox(width: 22), + Text('Folders', + style: TextStyle(fontSize: 13, color: Colors.grey[700])) + ]), + const SizedBox(height: 22), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Text('Freelance', overflow: TextOverflow.ellipsis), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Text('Mortgage', overflow: TextOverflow.ellipsis), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Flexible( + child: Text('Taxes', overflow: TextOverflow.ellipsis)) + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Flexible( + child: Text('Receipts', overflow: TextOverflow.ellipsis)) + ], + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: SwitchListTile.adaptive( + title: const Text( + 'Directionality', + style: TextStyle(fontSize: 12), + ), + subtitle: Text( + directionalityOverride == TextDirection.ltr ? 'LTR' : 'RTL', + ), + value: directionalityOverride == TextDirection.ltr, + onChanged: (bool value) { + setState(() { + if (value) { + directionalityOverride = TextDirection.ltr; + } else { + directionalityOverride = TextDirection.rtl; + } + }); + }, + ), ), - const SizedBox(width: 21), - const Flexible( - child: Text('Receipts', overflow: TextOverflow.ellipsis)) - ], - ), - ], + ), + ], + ), ); // These are the destinations used within the AdaptiveScaffold navigation @@ -208,134 +237,137 @@ class _MyHomePageState extends State // Updating the listener value. showGridView.value = Breakpoints.mediumAndUp.isActive(context); - return Scaffold( - backgroundColor: const Color.fromARGB(255, 234, 227, 241), - // Usage of AdaptiveLayout suite begins here. AdaptiveLayout takes - // LayoutSlots for its variety of screen slots. - body: AdaptiveLayout( - // Each SlotLayout has a config which maps Breakpoints to - // SlotLayoutConfigs. - primaryNavigation: SlotLayout( - config: { - // The breakpoint used here is from the Breakpoints class but custom - // Breakpoints can be defined by extending the Breakpoint class - Breakpoints.medium: SlotLayout.from( - // Every SlotLayoutConfig takes a key and a builder. The builder - // is to save memory that would be spent on initialization. - key: const Key('primaryNavigation'), - builder: (_) { - return AdaptiveScaffold.standardNavigationRail( - // Usually it would be easier to use a builder from - // AdaptiveScaffold for these types of navigation but this - // navigation has custom staggered item animations. + return Directionality( + textDirection: directionalityOverride, + child: Scaffold( + backgroundColor: const Color.fromARGB(255, 234, 227, 241), + // Usage of AdaptiveLayout suite begins here. AdaptiveLayout takes + // LayoutSlots for its variety of screen slots. + body: AdaptiveLayout( + // Each SlotLayout has a config which maps Breakpoints to + // SlotLayoutConfigs. + primaryNavigation: SlotLayout( + config: { + // The breakpoint used here is from the Breakpoints class but custom + // Breakpoints can be defined by extending the Breakpoint class + Breakpoints.medium: SlotLayout.from( + // Every SlotLayoutConfig takes a key and a builder. The builder + // is to save memory that would be spent on initialization. + key: const Key('primaryNavigation'), + builder: (_) { + return AdaptiveScaffold.standardNavigationRail( + // Usually it would be easier to use a builder from + // AdaptiveScaffold for these types of navigation but this + // navigation has custom staggered item animations. + onDestinationSelected: (int index) { + setState(() { + _navigationIndex = index; + }); + }, + selectedIndex: _navigationIndex, + leading: ScaleTransition( + scale: _articleIconSlideController, + child: const _MediumComposeIcon(), + ), + backgroundColor: const Color.fromARGB(0, 255, 255, 255), + destinations: [ + slideInNavigationItem( + begin: -1, + controller: _inboxIconSlideController, + icon: Icons.inbox, + label: 'Inbox', + ), + slideInNavigationItem( + begin: -2, + controller: _articleIconSlideController, + icon: Icons.article_outlined, + label: 'Articles', + ), + slideInNavigationItem( + begin: -3, + controller: _chatIconSlideController, + icon: Icons.chat_bubble_outline, + label: 'Chat', + ), + slideInNavigationItem( + begin: -4, + controller: _videoIconSlideController, + icon: Icons.video_call_outlined, + label: 'Video', + ) + ], + ); + }, + ), + Breakpoints.large: SlotLayout.from( + key: const Key('Large primaryNavigation'), + // The AdaptiveScaffold builder here greatly simplifies + // navigational elements. + builder: (_) => AdaptiveScaffold.standardNavigationRail( + leading: const _LargeComposeIcon(), onDestinationSelected: (int index) { setState(() { _navigationIndex = index; }); }, selectedIndex: _navigationIndex, - leading: ScaleTransition( - scale: _articleIconSlideController, - child: const _MediumComposeIcon(), - ), - backgroundColor: const Color.fromARGB(0, 255, 255, 255), - destinations: [ - slideInNavigationItem( - begin: -1, - controller: _inboxIconSlideController, - icon: Icons.inbox, - label: 'Inbox', - ), - slideInNavigationItem( - begin: -2, - controller: _articleIconSlideController, - icon: Icons.article_outlined, - label: 'Articles', - ), - slideInNavigationItem( - begin: -3, - controller: _chatIconSlideController, - icon: Icons.chat_bubble_outline, - label: 'Chat', - ), - slideInNavigationItem( - begin: -4, - controller: _videoIconSlideController, - icon: Icons.video_call_outlined, - label: 'Video', - ) - ], - ); - }, - ), - Breakpoints.large: SlotLayout.from( - key: const Key('Large primaryNavigation'), - // The AdaptiveScaffold builder here greatly simplifies - // navigational elements. - builder: (_) => AdaptiveScaffold.standardNavigationRail( - leading: const _LargeComposeIcon(), - onDestinationSelected: (int index) { - setState(() { - _navigationIndex = index; - }); - }, - selectedIndex: _navigationIndex, - trailing: trailingNavRail, - extended: true, - destinations: destinations.map((_) { - return AdaptiveScaffold.toRailDestination(_); - }).toList(), + trailing: trailingNavRail, + extended: true, + destinations: destinations.map((_) { + return AdaptiveScaffold.toRailDestination(_); + }).toList(), + ), ), - ), - }, - ), - body: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('body'), - // The conditional here is for navigation screens. The first - // screen shows the main screen and every other screen shows - // ExamplePage. - builder: (_) => (_navigationIndex == 0) - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), - child: _ItemList( - selected: selected, - items: _allItems, - selectCard: selectCard, + }, + ), + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + // The conditional here is for navigation screens. The first + // screen shows the main screen and every other screen shows + // ExamplePage. + builder: (_) => (_navigationIndex == 0) + ? Padding( + padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), + child: _ItemList( + selected: selected, + items: _allItems, + selectCard: selectCard, + ), + ) + : const _ExamplePage(), + ), + }, + ), + secondaryBody: _navigationIndex == 0 + ? SlotLayout( + config: { + Breakpoints.mediumAndUp: SlotLayout.from( + // This overrides the default behavior of the secondaryBody + // disappearing as it is animating out. + outAnimation: AdaptiveScaffold.stayOnScreen, + key: const Key('Secondary Body'), + builder: (_) => SafeArea( + child: _DetailTile(item: _allItems[selected ?? 0]), ), ) - : const _ExamplePage(), - ), - }, - ), - secondaryBody: _navigationIndex == 0 - ? SlotLayout( - config: { - Breakpoints.mediumAndUp: SlotLayout.from( - // This overrides the default behavior of the secondaryBody - // disappearing as it is animating out. - outAnimation: AdaptiveScaffold.stayOnScreen, - key: const Key('Secondary Body'), - builder: (_) => SafeArea( - child: _DetailTile(item: _allItems[selected ?? 0]), - ), - ) - }, + }, + ) + : null, + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottomNavigation'), + // You can define inAnimations or outAnimations to override the + // default offset transition. + outAnimation: AdaptiveScaffold.topToBottom, + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + ), ) - : null, - bottomNavigation: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('bottomNavigation'), - // You can define inAnimations or outAnimations to override the - // default offset transition. - outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations, - ), - ) - }, + }, + ), ), ), ); @@ -412,11 +444,9 @@ class _LargeComposeIcon extends StatelessWidget { child: Column(children: [ Container( padding: const EdgeInsets.fromLTRB(6, 0, 0, 0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ + children: [ Text( 'REPLY', style: TextStyle(color: Colors.deepPurple, fontSize: 15), @@ -444,14 +474,10 @@ class _LargeComposeIcon extends StatelessWidget { ), width: 200, height: 50, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0, 0, 0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + child: const Padding( + padding: EdgeInsets.fromLTRB(16.0, 0, 0, 0), child: Row( - children: const [ + children: [ Icon(Icons.edit_outlined), SizedBox(width: 20), Center(child: Text('Compose')), diff --git a/packages/flutter_adaptive_scaffold/example/pubspec.yaml b/packages/flutter_adaptive_scaffold/example/pubspec.yaml index 70ca3041abe6..54e91a84f662 100644 --- a/packages/flutter_adaptive_scaffold/example/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 0.0.1 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart index 028f47e7cad7..f3485ae370bf 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart @@ -488,165 +488,160 @@ class _AdaptiveScaffoldState extends State { final NavigationRailThemeData navRailTheme = Theme.of(context).navigationRailTheme; - return Directionality( - textDirection: TextDirection.ltr, - child: Scaffold( - appBar: widget.drawerBreakpoint.isActive(context) && widget.useDrawer - ? widget.appBar ?? AppBar() - : null, - drawer: widget.drawerBreakpoint.isActive(context) && widget.useDrawer - ? Drawer( - child: NavigationRail( - extended: true, - leading: widget.leadingExtendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - ), - ) - : null, - body: AdaptiveLayout( - bodyOrientation: widget.bodyOrientation, - bodyRatio: widget.bodyRatio, - internalAnimations: widget.internalAnimations, - primaryNavigation: SlotLayout( - config: { - widget.mediumBreakpoint: SlotLayout.from( - key: const Key('primaryNavigation'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - width: widget.navigationRailWidth, - leading: widget.leadingUnextendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: - navRailTheme.unselectedLabelTextStyle, - ), + return Scaffold( + appBar: widget.drawerBreakpoint.isActive(context) && widget.useDrawer + ? widget.appBar ?? AppBar() + : null, + drawer: widget.drawerBreakpoint.isActive(context) && widget.useDrawer + ? Drawer( + child: NavigationRail( + extended: true, + leading: widget.leadingExtendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, ), - widget.largeBreakpoint: SlotLayout.from( - key: const Key('primaryNavigation1'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - width: widget.extendedNavigationRailWidth, - extended: true, - leading: widget.leadingExtendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: - navRailTheme.unselectedLabelTextStyle, - ), + ) + : null, + body: AdaptiveLayout( + bodyOrientation: widget.bodyOrientation, + bodyRatio: widget.bodyRatio, + internalAnimations: widget.internalAnimations, + primaryNavigation: SlotLayout( + config: { + widget.mediumBreakpoint: SlotLayout.from( + key: const Key('primaryNavigation'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + width: widget.navigationRailWidth, + leading: widget.leadingUnextendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, ), - }, - ), - bottomNavigation: - !widget.drawerBreakpoint.isActive(context) || !widget.useDrawer - ? SlotLayout( - config: { - widget.smallBreakpoint: SlotLayout.from( - key: const Key('bottomNavigation'), - builder: (_) => - AdaptiveScaffold.standardBottomNavigationBar( - currentIndex: widget.selectedIndex, - destinations: widget.destinations, - onDestinationSelected: widget.onSelectedIndexChange, - ), - ), - }, - ) - : null, - body: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('body'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.body, - ), - if (widget.smallBody != null) - widget.smallBreakpoint: - (widget.smallBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('smallBody'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.smallBody, - ) - : null, - if (widget.body != null) - widget.mediumBreakpoint: - (widget.body != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('body'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.body, - ) - : null, - if (widget.largeBody != null) - widget.largeBreakpoint: - (widget.largeBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('largeBody'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.largeBody, - ) - : null, - }, - ), - secondaryBody: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('sBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.secondaryBody, + ), + widget.largeBreakpoint: SlotLayout.from( + key: const Key('primaryNavigation1'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + width: widget.extendedNavigationRailWidth, + extended: true, + leading: widget.leadingExtendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, ), - if (widget.smallSecondaryBody != null) - widget.smallBreakpoint: - (widget.smallSecondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('smallSBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.smallSecondaryBody, - ) - : null, - if (widget.secondaryBody != null) - widget.mediumBreakpoint: - (widget.secondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('sBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.secondaryBody, - ) - : null, - if (widget.largeSecondaryBody != null) - widget.largeBreakpoint: - (widget.largeSecondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('largeSBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.largeSecondaryBody, - ) - : null, - }, - ), + ), + }, + ), + bottomNavigation: + !widget.drawerBreakpoint.isActive(context) || !widget.useDrawer + ? SlotLayout( + config: { + widget.smallBreakpoint: SlotLayout.from( + key: const Key('bottomNavigation'), + builder: (_) => + AdaptiveScaffold.standardBottomNavigationBar( + currentIndex: widget.selectedIndex, + destinations: widget.destinations, + onDestinationSelected: widget.onSelectedIndexChange, + ), + ), + }, + ) + : null, + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.body, + ), + if (widget.smallBody != null) + widget.smallBreakpoint: + (widget.smallBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('smallBody'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.smallBody, + ) + : null, + if (widget.body != null) + widget.mediumBreakpoint: + (widget.body != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.body, + ) + : null, + if (widget.largeBody != null) + widget.largeBreakpoint: + (widget.largeBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('largeBody'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.largeBody, + ) + : null, + }, + ), + secondaryBody: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('sBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.secondaryBody, + ), + if (widget.smallSecondaryBody != null) + widget.smallBreakpoint: + (widget.smallSecondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('smallSBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.smallSecondaryBody, + ) + : null, + if (widget.secondaryBody != null) + widget.mediumBreakpoint: + (widget.secondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('sBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.secondaryBody, + ) + : null, + if (widget.largeSecondaryBody != null) + widget.largeBreakpoint: + (widget.largeSecondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('largeSBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.largeSecondaryBody, + ) + : null, + }, ), ), ); diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index d988420a2221..155e08665b41 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_adaptive_scaffold description: Widgets to easily build adaptive layouts, including navigation elements. -version: 0.1.4 +version: 0.1.5 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22 repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart index 90fb25bffdb0..5a84f4476e41 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart @@ -170,9 +170,7 @@ void main() { expect(begin, findsOneWidget); expect(end, findsOneWidget); } - // TODO(gspencergoog): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); testWidgets('slot layout can tolerate rapid changes in breakpoints', (WidgetTester tester) async { @@ -191,9 +189,7 @@ void main() { await tester.pumpAndSettle(); expect(begin, findsOneWidget); expect(end, findsNothing); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); // This test reflects the behavior of the internal animations of both the body // and secondary body and also the navigational items. This is reflected in @@ -248,9 +244,7 @@ void main() { expect(tester.getTopLeft(secondaryTestBreakpoint), const Offset(200, 10)); expect( tester.getBottomRight(secondaryTestBreakpoint), const Offset(390, 790)); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); testWidgets('adaptive layout does not animate when animations off', (WidgetTester tester) async { @@ -269,9 +263,7 @@ void main() { expect(tester.getTopLeft(secondaryTestBreakpoint), const Offset(200, 10)); expect( tester.getBottomRight(secondaryTestBreakpoint), const Offset(390, 790)); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); } class TestBreakpoint0 extends Breakpoint { diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart index 325628645522..463dd84a0b3b 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart @@ -543,6 +543,43 @@ void main() { tester.widget(find.byType(NavigationRail)); expect(rail.groupAlignment, equals(groupAlignment)); }); + + testWidgets( + "doesn't override Directionality", + (WidgetTester tester) async { + const List destinations = [ + NavigationDestination( + icon: Icon(Icons.home), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.account_circle), + label: 'Profile', + ), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Directionality( + textDirection: TextDirection.rtl, + child: AdaptiveScaffold( + destinations: destinations, + body: (BuildContext context) { + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + ); + + final Finder body = find.byKey(const Key('body')); + expect(body, findsOneWidget); + final TextDirection dir = Directionality.of(body.evaluate().first); + expect(dir, TextDirection.rtl); + }, + ); } /// An empty widget that implements [PreferredSizeWidget] to ensure that diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index 3e6a59396a2e..21a7e338e1ea 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 0.6.15 * Fixes unawaited_futures violations. diff --git a/packages/flutter_markdown/example/ios/Runner/Info.plist b/packages/flutter_markdown/example/ios/Runner/Info.plist index 6c83e4da6515..a5b939285a5a 100644 --- a/packages/flutter_markdown/example/ios/Runner/Info.plist +++ b/packages/flutter_markdown/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/flutter_markdown/lib/src/_functions_io.dart b/packages/flutter_markdown/lib/src/_functions_io.dart index 7c35ba485333..b3fa99ea63e6 100644 --- a/packages/flutter_markdown/lib/src/_functions_io.dart +++ b/packages/flutter_markdown/lib/src/_functions_io.dart @@ -66,7 +66,8 @@ final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?) } return result.copyWith( - textScaleFactor: MediaQuery.textScaleFactorOf(context), + textScaleFactor: + MediaQuery.textScaleFactorOf(context), // ignore: deprecated_member_use ); }; diff --git a/packages/flutter_markdown/lib/src/_functions_web.dart b/packages/flutter_markdown/lib/src/_functions_web.dart index a58a9cea37d6..828388613a53 100644 --- a/packages/flutter_markdown/lib/src/_functions_web.dart +++ b/packages/flutter_markdown/lib/src/_functions_web.dart @@ -68,7 +68,8 @@ final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?) } return result.copyWith( - textScaleFactor: MediaQuery.textScaleFactorOf(context), + textScaleFactor: + MediaQuery.textScaleFactorOf(context), // ignore: deprecated_member_use ); }; diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index 475c510be98f..f32ec0a19f03 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -829,6 +829,7 @@ class MarkdownBuilder implements md.NodeVisitor { if (selectable) { return SelectableText.rich( text!, + // ignore: deprecated_member_use textScaleFactor: styleSheet.textScaleFactor, textAlign: textAlign ?? TextAlign.start, onTap: onTapText, @@ -837,6 +838,7 @@ class MarkdownBuilder implements md.NodeVisitor { } else { return RichText( text: text!, + // ignore: deprecated_member_use textScaleFactor: styleSheet.textScaleFactor!, textAlign: textAlign ?? TextAlign.start, key: k, diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml index 74b886c1c75d..525222b9c282 100644 --- a/packages/flutter_markdown/pubspec.yaml +++ b/packages/flutter_markdown/pubspec.yaml @@ -7,8 +7,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.6.15 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_markdown/test/list_test.dart b/packages/flutter_markdown/test/list_test.dart index 0703dfd44483..4a587c50a93f 100644 --- a/packages/flutter_markdown/test/list_test.dart +++ b/packages/flutter_markdown/test/list_test.dart @@ -194,10 +194,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Column( - children: const [ + const Column( + children: [ MarkdownBody(fitContent: false, data: data), ], ), @@ -219,10 +217,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Column( - children: const [ + const Column( + children: [ MarkdownBody(data: data), ], ), diff --git a/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart b/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart index 9dc611be70a6..a5ae86953921 100644 --- a/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart +++ b/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart @@ -16,10 +16,8 @@ void defineTests() { 'Then it wraps its content', (WidgetTester tester) async { await tester.pumpWidget(boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Stack( - children: const [ + const Stack( + children: [ Text('shrinkWrap=true'), Align( alignment: Alignment.bottomCenter, @@ -48,10 +46,8 @@ void defineTests() { 'Then it expands to the maximum allowed height', (WidgetTester tester) async { await tester.pumpWidget(boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Stack( - children: const [ + const Stack( + children: [ Text('shrinkWrap=false test'), Align( alignment: Alignment.bottomCenter, diff --git a/packages/flutter_markdown/test/text_scale_factor_test.dart b/packages/flutter_markdown/test/text_scale_factor_test.dart index 3710b3e0a62e..2f3138a94aef 100644 --- a/packages/flutter_markdown/test/text_scale_factor_test.dart +++ b/packages/flutter_markdown/test/text_scale_factor_test.dart @@ -25,7 +25,7 @@ void defineTests() { ); final RichText richText = tester.widget(find.byType(RichText)); - expect(richText.textScaleFactor, 2.0); + expect(richText.textScaleFactor, 2.0); // ignore: deprecated_member_use }, ); @@ -36,6 +36,7 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( + // ignore: deprecated_member_use data: MediaQueryData(textScaleFactor: 2.0), child: MarkdownBody( data: data, @@ -45,7 +46,7 @@ void defineTests() { ); final RichText richText = tester.widget(find.byType(RichText)); - expect(richText.textScaleFactor, 2.0); + expect(richText.textScaleFactor, 2.0); // ignore: deprecated_member_use }, ); @@ -56,6 +57,7 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( + // ignore: deprecated_member_use data: MediaQueryData(textScaleFactor: 2.0), child: MarkdownBody( data: data, @@ -67,6 +69,7 @@ void defineTests() { final SelectableText selectableText = tester.widget(find.byType(SelectableText)); + // ignore: deprecated_member_use expect(selectableText.textScaleFactor, 2.0); }, ); diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 0bba2a21f2c7..b0df45c5fe69 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,24 @@ +## 8.0.5 + +- Fixes a bug that GoRouterState in top level redirect doesn't contain complete data. + +## 8.0.4 + +- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension. + +## 8.0.3 + +- Makes namedLocation and route name related APIs case sensitive. + +## 8.0.2 + +- Fixes a bug in `debugLogDiagnostics` to support StatefulShellRoute. + +## 8.0.1 + +- Fixes a link for an example in `path` documentation. + documentation. + ## 8.0.0 - **BREAKING CHANGE**: diff --git a/packages/go_router/example/ios/Runner/Info.plist b/packages/go_router/example/ios/Runner/Info.plist index 4f68a2cee180..677cf7bb8b0e 100644 --- a/packages/go_router/example/ios/Runner/Info.plist +++ b/packages/go_router/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index d68958cbaa79..c98f2f8bd123 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -145,8 +145,7 @@ class RouteBuilder { if (matchList.isError) { keyToPage = , List>>{ navigatorKey: >[ - _buildErrorPage( - context, _buildErrorState(matchList.error!, matchList.uri)), + _buildErrorPage(context, _buildErrorState(matchList)), ] }; } else { @@ -325,8 +324,7 @@ class RouteBuilder { if (match is ImperativeRouteMatch) { effectiveMatchList = match.matches; if (effectiveMatchList.isError) { - return _buildErrorState( - effectiveMatchList.error!, effectiveMatchList.uri); + return _buildErrorState(effectiveMatchList); } } else { effectiveMatchList = matchList; @@ -491,19 +489,18 @@ class RouteBuilder { child: child, ); - GoRouterState _buildErrorState( - Exception error, - Uri uri, - ) { - final String location = uri.toString(); + GoRouterState _buildErrorState(RouteMatchList matchList) { + final String location = matchList.uri.toString(); + assert(matchList.isError); return GoRouterState( configuration, location: location, - matchedLocation: uri.path, - name: null, - queryParameters: uri.queryParameters, - queryParametersAll: uri.queryParametersAll, - error: error, + matchedLocation: matchList.uri.path, + fullPath: matchList.fullPath, + pathParameters: matchList.pathParameters, + queryParameters: matchList.uri.queryParameters, + queryParametersAll: matchList.uri.queryParametersAll, + error: matchList.error, pageKey: ValueKey('$location(error)'), ); } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index f2e9d84617d8..865007d76597 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -221,9 +221,8 @@ class RouteConfiguration { '${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}'); return true; }()); - final String keyName = name.toLowerCase(); - assert(_nameToPath.containsKey(keyName), 'unknown route name: $name'); - final String path = _nameToPath[keyName]!; + assert(_nameToPath.containsKey(name), 'unknown route name: $name'); + final String path = _nameToPath[name]!; assert(() { // Check that all required params are present final List paramNames = []; @@ -415,9 +414,10 @@ class RouteConfiguration { GoRouterState( this, location: prevLocation, - name: null, // No name available at the top level trim the query params off the // sub-location to match route.redirect + fullPath: prevMatchList.fullPath, + pathParameters: prevMatchList.pathParameters, matchedLocation: prevMatchList.uri.path, queryParameters: prevMatchList.uri.queryParameters, queryParametersAll: prevMatchList.uri.queryParametersAll, @@ -552,7 +552,7 @@ class RouteConfiguration { final String fullPath = concatenatePaths(parentFullpath, route.path); sb.writeln(' => ${''.padLeft(depth * 2)}$fullPath'); _debugFullPathsFor(route.routes, fullPath, depth + 1, sb); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { _debugFullPathsFor(route.routes, parentFullpath, depth, sb); } } @@ -564,7 +564,7 @@ class RouteConfiguration { final String fullPath = concatenatePaths(parentFullPath, route.path); if (route.name != null) { - final String name = route.name!.toLowerCase(); + final String name = route.name!; assert( !_nameToPath.containsKey(name), 'duplication fullpaths for name ' diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c4ff4ad6a95a..58e3c3fea7c0 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -10,6 +10,8 @@ import '../router.dart'; /// context.go('/'); extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. + /// + /// This method can't be called during redirects. String namedLocation( String name, { Map pathParameters = const {}, diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 8983a7894e9c..ba656326e328 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -208,7 +208,7 @@ class GoRoute extends RouteBase { /// The query parameter are also capture during the route parsing and stored /// in [GoRouterState]. /// - /// See [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/sub_routes.dart) + /// See [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/path_and_query_parameters.dart) /// to learn more about parameters. final String path; diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index eac57c225581..15ceea634401 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -466,6 +466,8 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// Find the current GoRouter in the widget tree. + /// + /// This method throws when it is called during redirects. static GoRouter of(BuildContext context) { final InheritedGoRouter? inherited = context.dependOnInheritedWidgetOfExactType(); @@ -474,6 +476,8 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// The current GoRouter in the widget tree, if any. + /// + /// This method returns null when it is called during redirects. static GoRouter? maybeOf(BuildContext context) { final InheritedGoRouter? inherited = context.dependOnInheritedWidgetOfExactType(); diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 9c634eddbeea..13ee9add91e2 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -17,12 +17,12 @@ class GoRouterState { this._configuration, { required this.location, required this.matchedLocation, - required this.name, + this.name, this.path, - this.fullPath, - this.pathParameters = const {}, - this.queryParameters = const {}, - this.queryParametersAll = const >{}, + required this.fullPath, + required this.pathParameters, + required this.queryParameters, + required this.queryParametersAll, this.extra, this.error, required this.pageKey, @@ -42,16 +42,24 @@ class GoRouterState { /// matchedLocation = /family/f2 final String matchedLocation; - /// The optional name of the route. + /// The optional name of the route associated with this app. + /// + /// This can be null for GoRouterState pass into top level redirect. final String? name; - /// The path to this sub-route, e.g. family/:fid + /// The path of the route associated with this app. e.g. family/:fid + /// + /// This can be null for GoRouterState pass into top level redirect. final String? path; /// The full path to this sub-route, e.g. /family/:fid + /// + /// For top level redirect, this is the entire path that matches the location. + /// It can be empty if go router can't find a match. In that case, the [error] + /// contains more information. final String? fullPath; - /// The parameters for this sub-route, e.g. {'fid': 'f2'} + /// The parameters for this match, e.g. {'fid': 'f2'} final Map pathParameters; /// The query parameters for the location, e.g. {'from': '/family/f2'} @@ -64,7 +72,7 @@ class GoRouterState { /// An extra object to pass along with the navigation. final Object? extra; - /// The error associated with this sub-route. + /// The error associated with this match. final Exception? error; /// A unique string key for this sub-route. @@ -129,8 +137,6 @@ class GoRouterState { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. - // TODO(chunhtai): remove this method when go_router can provide a way to - // look up named location during redirect. String namedLocation( String name, { Map pathParameters = const {}, diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index b71ea714404c..db489edff28b 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.0 +version: 8.0.5 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index ac6ff0e398d8..ac4d1ed848fa 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -1037,6 +1037,33 @@ void main() { ), ], ), + GoRoute( + path: '/g', + builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute.indexedStack( + builder: _mockIndexedStackShellBuilder, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: 'h', + builder: _mockScreenBuilder, + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: 'i', + builder: _mockScreenBuilder, + ), + ], + ), + ], + ), + ], + ), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -1049,7 +1076,10 @@ void main() { ' => /a/c\n' ' => /d\n' ' => /d/e\n' - ' => /d/e/f\n', + ' => /d/e/f\n' + ' => /g\n' + ' => /g/h\n' + ' => /g/i\n', ); }, ); @@ -1069,3 +1099,7 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; + +Widget _mockIndexedStackShellBuilder(BuildContext context, GoRouterState state, + StatefulNavigationShell shell) => + shell; diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index b3b46660e64d..2ebb4afca774 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1493,8 +1493,7 @@ void main() { }, throwsA(isAssertionError)); }); - testWidgets('match case insensitive w/ params', - (WidgetTester tester) async { + testWidgets('cannot match case insensitive', (WidgetTester tester) async { final List routes = [ GoRoute( name: 'home', @@ -1524,8 +1523,15 @@ void main() { ]; final GoRouter router = await createRouter(routes, tester); - router.goNamed('person', - pathParameters: {'fid': 'f2', 'pid': 'p1'}); + expect( + () { + router.goNamed( + 'person', + pathParameters: {'fid': 'f2', 'pid': 'p1'}, + ); + }, + throwsAssertionError, + ); }); testWidgets('too few params', (WidgetTester tester) async { @@ -2016,7 +2022,7 @@ void main() { expect(Uri.parse(state.location).queryParameters, isNotEmpty); expect(Uri.parse(state.matchedLocation).queryParameters, isEmpty); expect(state.path, isNull); - expect(state.fullPath, isNull); + expect(state.fullPath, '/login'); expect(state.pathParameters.length, 0); expect(state.queryParameters.length, 1); expect(state.queryParameters['from'], '/'); @@ -2030,6 +2036,40 @@ void main() { expect(find.byType(LoginScreen), findsOneWidget); }); + testWidgets('top-level redirect state contains path parameters', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + routes: [ + GoRoute( + path: ':id', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ]), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/123', + redirect: (BuildContext context, GoRouterState state) { + expect(state.path, isNull); + expect(state.fullPath, '/:id'); + expect(state.pathParameters.length, 1); + expect(state.pathParameters['id'], '123'); + return null; + }, + ); + + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(2)); + }); + testWidgets('route-level redirect state', (WidgetTester tester) async { const String loc = '/book/0'; final List routes = [ diff --git a/packages/go_router/test/name_case_test.dart b/packages/go_router/test/name_case_test.dart new file mode 100644 index 000000000000..6e3f067197fe --- /dev/null +++ b/packages/go_router/test/name_case_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. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +void main() { + testWidgets( + 'Route names are case sensitive', + (WidgetTester tester) async { + // config router with 2 routes with the same name but different case (Name, name) + final GoRouter router = GoRouter( + routes: [ + GoRoute( + path: '/', + name: 'Name', + builder: (_, __) => const ScreenA(), + ), + GoRoute( + path: '/path', + name: 'name', + builder: (_, __) => const ScreenB(), + ), + ], + ); + + // run MaterialApp, initial screen path is '/' -> ScreenA + await tester.pumpWidget( + MaterialApp.router( + routerConfig: router, + title: 'GoRouter Testcase', + ), + ); + + // go to ScreenB + router.goNamed('name'); + await tester.pumpAndSettle(); + expect(find.byType(ScreenB), findsOneWidget); + + // go to ScreenA + router.goNamed('Name'); + await tester.pumpAndSettle(); + expect(find.byType(ScreenA), findsOneWidget); + }, + ); +} + +class ScreenA extends StatelessWidget { + const ScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class ScreenB extends StatelessWidget { + const ScreenB({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index ee4279b51576..253790f01163 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -118,11 +118,8 @@ void main() { ); expect(configuration.namedLocation('lowercase'), '/abc'); - expect(configuration.namedLocation('LOWERCASE'), '/abc'); expect(configuration.namedLocation('camelCase'), '/efg'); - expect(configuration.namedLocation('camelcase'), '/efg'); expect(configuration.namedLocation('snake_case'), '/hij'); - expect(configuration.namedLocation('SNAKE_CASE'), '/hij'); // With query parameters expect(configuration.namedLocation('lowercase'), '/abc'); diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 81a4e7db62f0..ce6210d043fd 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Fixes a bug that the required/positional parameters are not added to query parameters correctly. + ## 2.1.0 * Supports required/positional parameters that are not in the path. diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 3e53cb1c427e..693eb4a42cf3 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -441,7 +441,7 @@ GoRouteData.\$route( late final List _ctorParams = _ctor.parameters.where((ParameterElement element) { - if (element.isRequired && !element.isExtraField) { + if (_pathParams.contains(element.name)) { return true; } return false; @@ -449,7 +449,7 @@ GoRouteData.\$route( late final List _ctorQueryParams = _ctor.parameters .where((ParameterElement element) => - element.isOptional && !element.isExtraField) + !_pathParams.contains(element.name) && !element.isExtraField) .toList(); ConstructorElement get _ctor { diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 027b79ea2c6f..9f213c1508d2 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 2.1.0 +version: 2.1.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart index 016d2c86518a..b27b1ea91002 100644 --- a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart +++ b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart @@ -68,6 +68,9 @@ extension $NullableRequiredParamNotInPathExtension String get location => GoRouteData.$location( 'bob', + queryParams: { + if (id != null) 'id': id!.toString(), + }, ); void go(BuildContext context) => context.go(location); @@ -108,6 +111,9 @@ extension $NonNullableRequiredParamNotInPathExtension String get location => GoRouteData.$location( 'bob', + queryParams: { + 'id': id.toString(), + }, ); void go(BuildContext context) => context.go(location); diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index a0b461f10eb1..6edda3e08049 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.3.1 + +* Fixes a regression from 2.2.8 that could cause incorrect handling of a + rapid series of map object updates. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 2.3.0 * Endorses [`google_maps_flutter_web`](https://pub.dev/packages/google_maps_flutter_web) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 9c0ab999ddbc..6b3d16007c4b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the google_maps_flutter plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index e1b710c307a6..08c2286527fb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -360,41 +360,41 @@ class _GoogleMapState extends State { return; } final GoogleMapController controller = await _controller.future; - await controller._updateMapConfiguration(updates); + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } Future _updateMarkers() async { final GoogleMapController controller = await _controller.future; - await controller._updateMarkers( - MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + unawaited(controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); } Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; - await controller._updatePolygons( - PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + unawaited(controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); } Future _updatePolylines() async { final GoogleMapController controller = await _controller.future; - await controller._updatePolylines( - PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + unawaited(controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); } Future _updateCircles() async { final GoogleMapController controller = await _controller.future; - await controller._updateCircles( - CircleUpdates.from(_circles.values.toSet(), widget.circles)); + unawaited(controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); } Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; - await controller._updateTileOverlays(widget.tileOverlays); + unawaited(controller._updateTileOverlays(widget.tileOverlays)); } Future onPlatformViewCreated(int id) async { diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index d1e577027b37..e56ff513e7a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.3.0 +version: 2.3.1 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart index 459e16b60c42..f94caf1f5837 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithCircles(Set circles) { return Directionality( @@ -20,36 +20,24 @@ Widget _mapWithCircles(Set circles) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a circle', (WidgetTester tester) async { const Circle c1 = Circle(circleId: CircleId('circle_1')); await tester.pumpWidget(_mapWithCircles({c1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToAdd.length, 1); - final Circle initializedCircle = platformGoogleMap.circlesToAdd.first; + final Circle initializedCircle = map.circleUpdates.last.circlesToAdd.first; expect(initializedCircle, equals(c1)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); }); testWidgets('Adding a circle', (WidgetTester tester) async { @@ -59,16 +47,15 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c1, c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToAdd.length, 1); - final Circle addedCircle = platformGoogleMap.circlesToAdd.first; + final Circle addedCircle = map.circleUpdates.last.circlesToAdd.first; expect(addedCircle, equals(c2)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); }); testWidgets('Removing a circle', (WidgetTester tester) async { @@ -77,13 +64,12 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circleIdsToRemove.length, 1); - expect(platformGoogleMap.circleIdsToRemove.first, equals(c1.circleId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circleIdsToRemove.length, 1); + expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c1.circleId)); - expect(platformGoogleMap.circlesToChange.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Updating a circle', (WidgetTester tester) async { @@ -93,13 +79,12 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange.length, 1); - expect(platformGoogleMap.circlesToChange.first, equals(c2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToChange.length, 1); + expect(map.circleUpdates.last.circlesToChange.first, equals(c2)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Updating a circle', (WidgetTester tester) async { @@ -109,11 +94,10 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToChange.length, 1); - final Circle update = platformGoogleMap.circlesToChange.first; + final Circle update = map.circleUpdates.last.circlesToChange.first; expect(update, equals(c2)); expect(update.radius, 10); }); @@ -129,12 +113,11 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange, cur); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange, cur); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -150,16 +133,15 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange.length, 1); - expect(platformGoogleMap.circlesToAdd.length, 1); - expect(platformGoogleMap.circleIdsToRemove.length, 1); + expect(map.circleUpdates.last.circlesToChange.length, 1); + expect(map.circleUpdates.last.circlesToAdd.length, 1); + expect(map.circleUpdates.last.circleIdsToRemove.length, 1); - expect(platformGoogleMap.circlesToChange.first, equals(c2)); - expect(platformGoogleMap.circlesToAdd.first, equals(c1)); - expect(platformGoogleMap.circleIdsToRemove.first, equals(c3.circleId)); + expect(map.circleUpdates.last.circlesToChange.first, equals(c2)); + expect(map.circleUpdates.last.circlesToAdd.first, equals(c1)); + expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c3.circleId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -173,12 +155,11 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange, {c3}); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange, {c3}); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -190,17 +171,42 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange.isEmpty, true); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); + const Circle c3 = Circle(circleId: CircleId('circle_3'), radius: 1); + const Circle c3updated = Circle(circleId: CircleId('circle_3'), radius: 10); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithCircles({c1, c2})); + await tester.pumpWidget(_mapWithCircles({c1, c3})); + await tester.pumpWidget(_mapWithCircles({c1, c3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.circleUpdates.length, 3); + + expect(map.circleUpdates[0].circlesToChange.isEmpty, true); + expect(map.circleUpdates[0].circlesToAdd, {c1, c2}); + expect(map.circleUpdates[0].circleIdsToRemove.isEmpty, true); + + expect(map.circleUpdates[1].circlesToChange.isEmpty, true); + expect(map.circleUpdates[1].circlesToAdd, {c3}); + expect(map.circleUpdates[1].circleIdsToRemove, {c2.circleId}); + + expect(map.circleUpdates[2].circlesToChange, {c3updated}); + expect(map.circleUpdates[2].circlesToAdd.isEmpty, true); + expect(map.circleUpdates[2].circleIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart new file mode 100644 index 000000000000..22447ba5ecad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -0,0 +1,303 @@ +// 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:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +// A dummy implementation of the platform interface for tests. +class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + FakeGoogleMapsFlutterPlatform(); + + /// The IDs passed to each call to buildView, in call order. + List createdIds = []; + + /// A map of creation IDs to fake map instances. + Map mapInstances = + {}; + + PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!; + + /// Whether to add a small delay to async calls to simulate more realistic + /// async behavior (simulating the platform channel calls most + /// implementations will do). + /// + /// When true, requires tests to `pumpAndSettle` at the end of the test + /// to avoid exceptions. + bool simulatePlatformDelay = false; + + /// Whether `dispose` has been called. + bool disposed = false; + + /// Stream controller to inject events for testing. + final StreamController> mapEventStreamController = + StreamController>.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapConfiguration( + MapConfiguration update, { + required int mapId, + }) async { + mapInstances[mapId]?.mapConfiguration = update; + await _fakeDelay(); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.markerUpdates.add(markerUpdates); + await _fakeDelay(); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polygonUpdates.add(polygonUpdates); + await _fakeDelay(); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polylineUpdates.add(polylineUpdates); + await _fakeDelay(); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.circleUpdates.add(circleUpdates); + await _fakeDelay(); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async { + mapInstances[mapId]?.tileOverlaySets.add(newTileOverlays); + await _fakeDelay(); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return const ScreenCoordinate(x: 0, y: 0); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return const LatLng(0, 0); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), + }) { + final PlatformMapStateRecorder? instance = mapInstances[creationId]; + if (instance == null) { + createdIds.add(creationId); + mapInstances[creationId] = PlatformMapStateRecorder( + widgetConfiguration: widgetConfiguration, + mapConfiguration: mapConfiguration, + mapObjects: mapObjects); + onPlatformViewCreated(creationId); + } + return Container(); + } + + Future _fakeDelay() async { + if (!simulatePlatformDelay) { + return; + } + return Future.delayed(const Duration(microseconds: 1)); + } +} + +/// A fake implementation of a native map, which stores all the updates it is +/// sent for inspection in tests. +class PlatformMapStateRecorder { + PlatformMapStateRecorder({ + required this.widgetConfiguration, + this.mapObjects = const MapObjects(), + this.mapConfiguration = const MapConfiguration(), + }) { + markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); + polygonUpdates + .add(PolygonUpdates.from(const {}, mapObjects.polygons)); + polylineUpdates + .add(PolylineUpdates.from(const {}, mapObjects.polylines)); + circleUpdates.add(CircleUpdates.from(const {}, mapObjects.circles)); + tileOverlaySets.add(mapObjects.tileOverlays); + } + + MapWidgetConfiguration widgetConfiguration; + MapObjects mapObjects; + MapConfiguration mapConfiguration; + + final List markerUpdates = []; + final List polygonUpdates = []; + final List polylineUpdates = []; + final List circleUpdates = []; + final List> tileOverlaySets = >[]; +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart deleted file mode 100644 index c28ff1f4f55f..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ /dev/null @@ -1,485 +0,0 @@ -// 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:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -class FakePlatformGoogleMap { - FakePlatformGoogleMap(int id, Map params) - : cameraPosition = - CameraPosition.fromMap(params['initialCameraPosition']), - channel = MethodChannel('plugins.flutter.io/google_maps_$id') { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(channel, onMethodCall); - updateOptions(params['options'] as Map); - updateMarkers(params); - updatePolygons(params); - updatePolylines(params); - updateCircles(params); - updateTileOverlays(Map.castFrom(params)); - } - - MethodChannel channel; - - CameraPosition? cameraPosition; - - bool? compassEnabled; - - bool? mapToolbarEnabled; - - CameraTargetBounds? cameraTargetBounds; - - MapType? mapType; - - MinMaxZoomPreference? minMaxZoomPreference; - - bool? rotateGesturesEnabled; - - bool? scrollGesturesEnabled; - - bool? tiltGesturesEnabled; - - bool? zoomGesturesEnabled; - - bool? zoomControlsEnabled; - - bool? liteModeEnabled; - - bool? trackCameraPosition; - - bool? myLocationEnabled; - - bool? trafficEnabled; - - bool? buildingsEnabled; - - bool? myLocationButtonEnabled; - - List? padding; - - Set markerIdsToRemove = {}; - - Set markersToAdd = {}; - - Set markersToChange = {}; - - Set polygonIdsToRemove = {}; - - Set polygonsToAdd = {}; - - Set polygonsToChange = {}; - - Set polylineIdsToRemove = {}; - - Set polylinesToAdd = {}; - - Set polylinesToChange = {}; - - Set circleIdsToRemove = {}; - - Set circlesToAdd = {}; - - Set circlesToChange = {}; - - Set tileOverlayIdsToRemove = {}; - - Set tileOverlaysToAdd = {}; - - Set tileOverlaysToChange = {}; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'map#update': - final Map arguments = - (call.arguments as Map).cast(); - updateOptions(arguments['options']! as Map); - return Future.sync(() {}); - case 'markers#update': - updateMarkers(call.arguments as Map?); - return Future.sync(() {}); - case 'polygons#update': - updatePolygons(call.arguments as Map?); - return Future.sync(() {}); - case 'polylines#update': - updatePolylines(call.arguments as Map?); - return Future.sync(() {}); - case 'tileOverlays#update': - updateTileOverlays(Map.castFrom( - call.arguments as Map)); - return Future.sync(() {}); - case 'circles#update': - updateCircles(call.arguments as Map?); - return Future.sync(() {}); - default: - return Future.sync(() {}); - } - } - - void updateMarkers(Map? markerUpdates) { - if (markerUpdates == null) { - return; - } - markersToAdd = _deserializeMarkers(markerUpdates['markersToAdd']); - markerIdsToRemove = _deserializeMarkerIds( - markerUpdates['markerIdsToRemove'] as List?); - markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); - } - - Set _deserializeMarkerIds(List? markerIds) { - if (markerIds == null) { - return {}; - } - return markerIds - .map((dynamic markerId) => MarkerId(markerId as String)) - .toSet(); - } - - Set _deserializeMarkers(dynamic markers) { - if (markers == null) { - return {}; - } - final List markersData = markers as List; - final Set result = {}; - for (final Map markerData - in markersData.cast>()) { - final String markerId = markerData['markerId'] as String; - final double alpha = markerData['alpha'] as double; - final bool draggable = markerData['draggable'] as bool; - final bool visible = markerData['visible'] as bool; - - final dynamic infoWindowData = markerData['infoWindow']; - InfoWindow infoWindow = InfoWindow.noText; - if (infoWindowData != null) { - final Map infoWindowMap = - infoWindowData as Map; - infoWindow = InfoWindow( - title: infoWindowMap['title'] as String?, - snippet: infoWindowMap['snippet'] as String?, - ); - } - - result.add(Marker( - markerId: MarkerId(markerId), - draggable: draggable, - visible: visible, - infoWindow: infoWindow, - alpha: alpha, - )); - } - - return result; - } - - void updatePolygons(Map? polygonUpdates) { - if (polygonUpdates == null) { - return; - } - polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']); - polygonIdsToRemove = _deserializePolygonIds( - polygonUpdates['polygonIdsToRemove'] as List?); - polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); - } - - Set _deserializePolygonIds(List? polygonIds) { - if (polygonIds == null) { - return {}; - } - return polygonIds - .map((dynamic polygonId) => PolygonId(polygonId as String)) - .toSet(); - } - - Set _deserializePolygons(dynamic polygons) { - if (polygons == null) { - return {}; - } - final List polygonsData = polygons as List; - final Set result = {}; - for (final Map polygonData - in polygonsData.cast>()) { - final String polygonId = polygonData['polygonId'] as String; - final bool visible = polygonData['visible'] as bool; - final bool geodesic = polygonData['geodesic'] as bool; - final List points = - _deserializePoints(polygonData['points'] as List); - final List> holes = - _deserializeHoles(polygonData['holes'] as List); - - result.add(Polygon( - polygonId: PolygonId(polygonId), - visible: visible, - geodesic: geodesic, - points: points, - holes: holes, - )); - } - - return result; - } - - // Converts a list of points expressed as two-element lists of doubles into - // a list of `LatLng`s. All list items are assumed to be non-null. - List _deserializePoints(List points) { - return points.map((dynamic item) { - final List list = item as List; - return LatLng(list[0]! as double, list[1]! as double); - }).toList(); - } - - List> _deserializeHoles(List holes) { - return holes.map>((dynamic hole) { - return _deserializePoints(hole as List); - }).toList(); - } - - void updatePolylines(Map? polylineUpdates) { - if (polylineUpdates == null) { - return; - } - polylinesToAdd = _deserializePolylines(polylineUpdates['polylinesToAdd']); - polylineIdsToRemove = _deserializePolylineIds( - polylineUpdates['polylineIdsToRemove'] as List?); - polylinesToChange = - _deserializePolylines(polylineUpdates['polylinesToChange']); - } - - Set _deserializePolylineIds(List? polylineIds) { - if (polylineIds == null) { - return {}; - } - return polylineIds - .map((dynamic polylineId) => PolylineId(polylineId as String)) - .toSet(); - } - - Set _deserializePolylines(dynamic polylines) { - if (polylines == null) { - return {}; - } - final List polylinesData = polylines as List; - final Set result = {}; - for (final Map polylineData - in polylinesData.cast>()) { - final String polylineId = polylineData['polylineId'] as String; - final bool visible = polylineData['visible'] as bool; - final bool geodesic = polylineData['geodesic'] as bool; - final List points = - _deserializePoints(polylineData['points'] as List); - - result.add(Polyline( - polylineId: PolylineId(polylineId), - visible: visible, - geodesic: geodesic, - points: points, - )); - } - - return result; - } - - void updateCircles(Map? circleUpdates) { - if (circleUpdates == null) { - return; - } - circlesToAdd = _deserializeCircles(circleUpdates['circlesToAdd']); - circleIdsToRemove = _deserializeCircleIds( - circleUpdates['circleIdsToRemove'] as List?); - circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); - } - - void updateTileOverlays(Map updateTileOverlayUpdates) { - final List>? tileOverlaysToAddList = - updateTileOverlayUpdates['tileOverlaysToAdd'] != null - ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToAdd'] as List) - : null; - final List? tileOverlayIdsToRemoveList = - updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null - ? List.castFrom( - updateTileOverlayUpdates['tileOverlayIdsToRemove'] - as List) - : null; - final List>? tileOverlaysToChangeList = - updateTileOverlayUpdates['tileOverlaysToChange'] != null - ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToChange'] - as List) - : null; - tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); - tileOverlayIdsToRemove = - _deserializeTileOverlayIds(tileOverlayIdsToRemoveList); - tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList); - } - - Set _deserializeCircleIds(List? circleIds) { - if (circleIds == null) { - return {}; - } - return circleIds - .map((dynamic circleId) => CircleId(circleId as String)) - .toSet(); - } - - Set _deserializeCircles(dynamic circles) { - if (circles == null) { - return {}; - } - final List circlesData = circles as List; - final Set result = {}; - for (final Map circleData - in circlesData.cast>()) { - final String circleId = circleData['circleId'] as String; - final bool visible = circleData['visible'] as bool; - final double radius = circleData['radius'] as double; - - result.add(Circle( - circleId: CircleId(circleId), - visible: visible, - radius: radius, - )); - } - - return result; - } - - Set _deserializeTileOverlayIds(List? tileOverlayIds) { - if (tileOverlayIds == null || tileOverlayIds.isEmpty) { - return {}; - } - return tileOverlayIds - .map((String tileOverlayId) => TileOverlayId(tileOverlayId)) - .toSet(); - } - - Set _deserializeTileOverlays( - List>? tileOverlays) { - if (tileOverlays == null || tileOverlays.isEmpty) { - return {}; - } - final Set result = {}; - for (final Map tileOverlayData in tileOverlays) { - final String tileOverlayId = tileOverlayData['tileOverlayId'] as String; - final bool fadeIn = tileOverlayData['fadeIn'] as bool; - final double transparency = tileOverlayData['transparency'] as double; - final int zIndex = tileOverlayData['zIndex'] as int; - final bool visible = tileOverlayData['visible'] as bool; - - result.add(TileOverlay( - tileOverlayId: TileOverlayId(tileOverlayId), - fadeIn: fadeIn, - transparency: transparency, - zIndex: zIndex, - visible: visible, - )); - } - - return result; - } - - void updateOptions(Map options) { - if (options.containsKey('compassEnabled')) { - compassEnabled = options['compassEnabled'] as bool?; - } - if (options.containsKey('mapToolbarEnabled')) { - mapToolbarEnabled = options['mapToolbarEnabled'] as bool?; - } - if (options.containsKey('cameraTargetBounds')) { - final List boundsList = - options['cameraTargetBounds'] as List; - cameraTargetBounds = boundsList[0] == null - ? CameraTargetBounds.unbounded - : CameraTargetBounds(LatLngBounds.fromList(boundsList[0])); - } - if (options.containsKey('mapType')) { - mapType = MapType.values[options['mapType'] as int]; - } - if (options.containsKey('minMaxZoomPreference')) { - final List minMaxZoomList = - options['minMaxZoomPreference'] as List; - minMaxZoomPreference = MinMaxZoomPreference( - minMaxZoomList[0] as double?, minMaxZoomList[1] as double?); - } - if (options.containsKey('rotateGesturesEnabled')) { - rotateGesturesEnabled = options['rotateGesturesEnabled'] as bool?; - } - if (options.containsKey('scrollGesturesEnabled')) { - scrollGesturesEnabled = options['scrollGesturesEnabled'] as bool?; - } - if (options.containsKey('tiltGesturesEnabled')) { - tiltGesturesEnabled = options['tiltGesturesEnabled'] as bool?; - } - if (options.containsKey('trackCameraPosition')) { - trackCameraPosition = options['trackCameraPosition'] as bool?; - } - if (options.containsKey('zoomGesturesEnabled')) { - zoomGesturesEnabled = options['zoomGesturesEnabled'] as bool?; - } - if (options.containsKey('zoomControlsEnabled')) { - zoomControlsEnabled = options['zoomControlsEnabled'] as bool?; - } - if (options.containsKey('liteModeEnabled')) { - liteModeEnabled = options['liteModeEnabled'] as bool?; - } - if (options.containsKey('myLocationEnabled')) { - myLocationEnabled = options['myLocationEnabled'] as bool?; - } - if (options.containsKey('myLocationButtonEnabled')) { - myLocationButtonEnabled = options['myLocationButtonEnabled'] as bool?; - } - if (options.containsKey('trafficEnabled')) { - trafficEnabled = options['trafficEnabled'] as bool?; - } - if (options.containsKey('buildingsEnabled')) { - buildingsEnabled = options['buildingsEnabled'] as bool?; - } - if (options.containsKey('padding')) { - padding = options['padding'] as List?; - } - } -} - -class FakePlatformViewsController { - FakePlatformGoogleMap? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = - call.arguments as Map; - final Map params = - _decodeParams(args['params'] as Uint8List)!; - lastCreatedView = FakePlatformGoogleMap( - args['id'] as int, - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } - - void reset() { - lastCreatedView = null; - } -} - -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes) - as Map?; -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 99b12988f3b4..7005a8d3ab60 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -2,30 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initial camera position', (WidgetTester tester) async { @@ -38,10 +27,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.cameraPosition, + expect(map.widgetConfiguration.initialCameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); }); @@ -65,10 +53,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.cameraPosition, + expect(map.widgetConfiguration.initialCameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); }); @@ -83,10 +70,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.compassEnabled, false); + expect(map.mapConfiguration.compassEnabled, false); await tester.pumpWidget( const Directionality( @@ -97,7 +83,7 @@ void main() { ), ); - expect(platformGoogleMap.compassEnabled, true); + expect(map.mapConfiguration.compassEnabled, true); }); testWidgets('Can update mapToolbarEnabled', (WidgetTester tester) async { @@ -111,10 +97,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.mapToolbarEnabled, false); + expect(map.mapConfiguration.mapToolbarEnabled, false); await tester.pumpWidget( const Directionality( @@ -125,7 +110,7 @@ void main() { ), ); - expect(platformGoogleMap.mapToolbarEnabled, true); + expect(map.mapConfiguration.mapToolbarEnabled, true); }); testWidgets('Can update cameraTargetBounds', (WidgetTester tester) async { @@ -145,11 +130,10 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; expect( - platformGoogleMap.cameraTargetBounds, + map.mapConfiguration.cameraTargetBounds, CameraTargetBounds( LatLngBounds( southwest: const LatLng(10.0, 20.0), @@ -174,7 +158,7 @@ void main() { ); expect( - platformGoogleMap.cameraTargetBounds, + map.mapConfiguration.cameraTargetBounds, CameraTargetBounds( LatLngBounds( southwest: const LatLng(16.0, 20.0), @@ -194,10 +178,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.mapType, MapType.hybrid); + expect(map.mapConfiguration.mapType, MapType.hybrid); await tester.pumpWidget( const Directionality( @@ -209,7 +192,7 @@ void main() { ), ); - expect(platformGoogleMap.mapType, MapType.satellite); + expect(map.mapConfiguration.mapType, MapType.satellite); }); testWidgets('Can update minMaxZoom', (WidgetTester tester) async { @@ -223,10 +206,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.minMaxZoomPreference, + expect(map.mapConfiguration.minMaxZoomPreference, const MinMaxZoomPreference(1.0, 3.0)); await tester.pumpWidget( @@ -238,8 +220,8 @@ void main() { ), ); - expect( - platformGoogleMap.minMaxZoomPreference, MinMaxZoomPreference.unbounded); + expect(map.mapConfiguration.minMaxZoomPreference, + MinMaxZoomPreference.unbounded); }); testWidgets('Can update rotateGesturesEnabled', (WidgetTester tester) async { @@ -253,10 +235,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.rotateGesturesEnabled, false); + expect(map.mapConfiguration.rotateGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -267,7 +248,7 @@ void main() { ), ); - expect(platformGoogleMap.rotateGesturesEnabled, true); + expect(map.mapConfiguration.rotateGesturesEnabled, true); }); testWidgets('Can update scrollGesturesEnabled', (WidgetTester tester) async { @@ -281,10 +262,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.scrollGesturesEnabled, false); + expect(map.mapConfiguration.scrollGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -295,7 +275,7 @@ void main() { ), ); - expect(platformGoogleMap.scrollGesturesEnabled, true); + expect(map.mapConfiguration.scrollGesturesEnabled, true); }); testWidgets('Can update tiltGesturesEnabled', (WidgetTester tester) async { @@ -309,10 +289,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.tiltGesturesEnabled, false); + expect(map.mapConfiguration.tiltGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -323,7 +302,7 @@ void main() { ), ); - expect(platformGoogleMap.tiltGesturesEnabled, true); + expect(map.mapConfiguration.tiltGesturesEnabled, true); }); testWidgets('Can update trackCameraPosition', (WidgetTester tester) async { @@ -336,10 +315,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.trackCameraPosition, false); + expect(map.mapConfiguration.trackCameraPosition, false); await tester.pumpWidget( Directionality( @@ -352,7 +330,7 @@ void main() { ), ); - expect(platformGoogleMap.trackCameraPosition, true); + expect(map.mapConfiguration.trackCameraPosition, true); }); testWidgets('Can update zoomGesturesEnabled', (WidgetTester tester) async { @@ -366,10 +344,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.zoomGesturesEnabled, false); + expect(map.mapConfiguration.zoomGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -380,7 +357,7 @@ void main() { ), ); - expect(platformGoogleMap.zoomGesturesEnabled, true); + expect(map.mapConfiguration.zoomGesturesEnabled, true); }); testWidgets('Can update zoomControlsEnabled', (WidgetTester tester) async { @@ -394,10 +371,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.zoomControlsEnabled, false); + expect(map.mapConfiguration.zoomControlsEnabled, false); await tester.pumpWidget( const Directionality( @@ -408,7 +384,7 @@ void main() { ), ); - expect(platformGoogleMap.zoomControlsEnabled, true); + expect(map.mapConfiguration.zoomControlsEnabled, true); }); testWidgets('Can update myLocationEnabled', (WidgetTester tester) async { @@ -421,10 +397,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.myLocationEnabled, false); + expect(map.mapConfiguration.myLocationEnabled, false); await tester.pumpWidget( const Directionality( @@ -436,7 +411,7 @@ void main() { ), ); - expect(platformGoogleMap.myLocationEnabled, true); + expect(map.mapConfiguration.myLocationEnabled, true); }); testWidgets('Can update myLocationButtonEnabled', @@ -450,10 +425,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.myLocationButtonEnabled, true); + expect(map.mapConfiguration.myLocationButtonEnabled, true); await tester.pumpWidget( const Directionality( @@ -465,7 +439,7 @@ void main() { ), ); - expect(platformGoogleMap.myLocationButtonEnabled, false); + expect(map.mapConfiguration.myLocationButtonEnabled, false); }); testWidgets('Is default padding 0', (WidgetTester tester) async { @@ -478,10 +452,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.padding, [0, 0, 0, 0]); + expect(map.mapConfiguration.padding, EdgeInsets.zero); }); testWidgets('Can update padding', (WidgetTester tester) async { @@ -494,10 +467,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.padding, [0, 0, 0, 0]); + expect(map.mapConfiguration.padding, EdgeInsets.zero); await tester.pumpWidget( const Directionality( @@ -509,7 +481,8 @@ void main() { ), ); - expect(platformGoogleMap.padding, [20, 10, 40, 30]); + expect(map.mapConfiguration.padding, + const EdgeInsets.fromLTRB(10, 20, 30, 40)); await tester.pumpWidget( const Directionality( @@ -521,7 +494,8 @@ void main() { ), ); - expect(platformGoogleMap.padding, [60, 50, 80, 70]); + expect(map.mapConfiguration.padding, + const EdgeInsets.fromLTRB(50, 60, 70, 80)); }); testWidgets('Can update traffic', (WidgetTester tester) async { @@ -534,10 +508,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.trafficEnabled, false); + expect(map.mapConfiguration.trafficEnabled, false); await tester.pumpWidget( const Directionality( @@ -549,7 +522,7 @@ void main() { ), ); - expect(platformGoogleMap.trafficEnabled, true); + expect(map.mapConfiguration.trafficEnabled, true); }); testWidgets('Can update buildings', (WidgetTester tester) async { @@ -563,10 +536,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.buildingsEnabled, false); + expect(map.mapConfiguration.buildingsEnabled, false); await tester.pumpWidget( const Directionality( @@ -577,12 +549,6 @@ void main() { ), ); - expect(platformGoogleMap.buildingsEnabled, true); + expect(map.mapConfiguration.buildingsEnabled, true); }); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 10e118486a31..eb7e038c0439 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -2,22 +2,20 @@ // 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:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:stream_transform/stream_transform.dart'; + +import 'fake_google_maps_flutter_platform.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late TestGoogleMapsFlutterPlatform platform; + late FakeGoogleMapsFlutterPlatform platform; setUp(() { // Use a mock platform so we never need to hit the MethodChannel code. - platform = TestGoogleMapsFlutterPlatform(); + platform = FakeGoogleMapsFlutterPlatform(); GoogleMapsFlutterPlatform.instance = platform; }); @@ -26,14 +24,10 @@ void main() { ) async { // Inject two map widgets... await tester.pumpWidget( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Directionality( + const Directionality( textDirection: TextDirection.ltr, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Column( - children: const [ + children: [ GoogleMap( initialCameraPosition: CameraPosition( target: LatLng(43.362, -5.849), @@ -70,222 +64,3 @@ void main() { expect(platform.disposed, true); }); } - -// A dummy implementation of the platform interface for tests. -class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { - TestGoogleMapsFlutterPlatform(); - - // The IDs passed to each call to buildView, in call order. - List createdIds = []; - - // Whether `dispose` has been called. - bool disposed = false; - - // Stream controller to inject events for testing. - final StreamController> mapEventStreamController = - StreamController>.broadcast(); - - @override - Future init(int mapId) async {} - - @override - Future updateMapConfiguration( - MapConfiguration update, { - required int mapId, - }) async {} - - @override - Future updateMarkers( - MarkerUpdates markerUpdates, { - required int mapId, - }) async {} - - @override - Future updatePolygons( - PolygonUpdates polygonUpdates, { - required int mapId, - }) async {} - - @override - Future updatePolylines( - PolylineUpdates polylineUpdates, { - required int mapId, - }) async {} - - @override - Future updateCircles( - CircleUpdates circleUpdates, { - required int mapId, - }) async {} - - @override - Future updateTileOverlays({ - required Set newTileOverlays, - required int mapId, - }) async {} - - @override - Future clearTileCache( - TileOverlayId tileOverlayId, { - required int mapId, - }) async {} - - @override - Future animateCamera( - CameraUpdate cameraUpdate, { - required int mapId, - }) async {} - - @override - Future moveCamera( - CameraUpdate cameraUpdate, { - required int mapId, - }) async {} - - @override - Future setMapStyle( - String? mapStyle, { - required int mapId, - }) async {} - - @override - Future getVisibleRegion({ - required int mapId, - }) async { - return LatLngBounds( - southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); - } - - @override - Future getScreenCoordinate( - LatLng latLng, { - required int mapId, - }) async { - return const ScreenCoordinate(x: 0, y: 0); - } - - @override - Future getLatLng( - ScreenCoordinate screenCoordinate, { - required int mapId, - }) async { - return const LatLng(0, 0); - } - - @override - Future showMarkerInfoWindow( - MarkerId markerId, { - required int mapId, - }) async {} - - @override - Future hideMarkerInfoWindow( - MarkerId markerId, { - required int mapId, - }) async {} - - @override - Future isMarkerInfoWindowShown( - MarkerId markerId, { - required int mapId, - }) async { - return false; - } - - @override - Future getZoomLevel({ - required int mapId, - }) async { - return 0.0; - } - - @override - Future takeSnapshot({ - required int mapId, - }) async { - return null; - } - - @override - Stream onCameraMoveStarted({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCameraMove({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCameraIdle({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onInfoWindowTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDragStart({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDrag({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDragEnd({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onPolylineTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onPolygonTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCircleTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onLongPress({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - void dispose({required int mapId}) { - disposed = true; - } - - @override - Widget buildViewWithConfiguration( - int creationId, - PlatformViewCreatedCallback onPlatformViewCreated, { - required MapWidgetConfiguration widgetConfiguration, - MapObjects mapObjects = const MapObjects(), - MapConfiguration mapConfiguration = const MapConfiguration(), - }) { - onPlatformViewCreated(0); - createdIds.add(creationId); - return Container(); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart index 75a153e0eaa2..9f65f5d3bf5b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithMarkers(Set markers) { return Directionality( @@ -20,36 +20,24 @@ Widget _mapWithMarkers(Set markers) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a marker', (WidgetTester tester) async { const Marker m1 = Marker(markerId: MarkerId('marker_1')); await tester.pumpWidget(_mapWithMarkers({m1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToAdd.length, 1); - final Marker initializedMarker = platformGoogleMap.markersToAdd.first; + final Marker initializedMarker = map.markerUpdates.last.markersToAdd.first; expect(initializedMarker, equals(m1)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); }); testWidgets('Adding a marker', (WidgetTester tester) async { @@ -59,16 +47,15 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m1, m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToAdd.length, 1); - final Marker addedMarker = platformGoogleMap.markersToAdd.first; + final Marker addedMarker = map.markerUpdates.last.markersToAdd.first; expect(addedMarker, equals(m2)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); }); testWidgets('Removing a marker', (WidgetTester tester) async { @@ -77,13 +64,12 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markerIdsToRemove.length, 1); - expect(platformGoogleMap.markerIdsToRemove.first, equals(m1.markerId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markerIdsToRemove.length, 1); + expect(map.markerUpdates.last.markerIdsToRemove.first, equals(m1.markerId)); - expect(platformGoogleMap.markersToChange.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Updating a marker', (WidgetTester tester) async { @@ -93,13 +79,12 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange.length, 1); - expect(platformGoogleMap.markersToChange.first, equals(m2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToChange.length, 1); + expect(map.markerUpdates.last.markersToChange.first, equals(m2)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Updating a marker', (WidgetTester tester) async { @@ -112,11 +97,10 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToChange.length, 1); - final Marker update = platformGoogleMap.markersToChange.first; + final Marker update = map.markerUpdates.last.markersToChange.first; expect(update, equals(m2)); expect(update.infoWindow.snippet, 'changed'); }); @@ -132,12 +116,11 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange, cur); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange, cur); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -153,16 +136,15 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange.length, 1); - expect(platformGoogleMap.markersToAdd.length, 1); - expect(platformGoogleMap.markerIdsToRemove.length, 1); + expect(map.markerUpdates.last.markersToChange.length, 1); + expect(map.markerUpdates.last.markersToAdd.length, 1); + expect(map.markerUpdates.last.markerIdsToRemove.length, 1); - expect(platformGoogleMap.markersToChange.first, equals(m2)); - expect(platformGoogleMap.markersToAdd.first, equals(m1)); - expect(platformGoogleMap.markerIdsToRemove.first, equals(m3.markerId)); + expect(map.markerUpdates.last.markersToChange.first, equals(m2)); + expect(map.markerUpdates.last.markersToAdd.first, equals(m1)); + expect(map.markerUpdates.last.markerIdsToRemove.first, equals(m3.markerId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -176,12 +158,11 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange, {m3}); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange, {m3}); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -196,17 +177,43 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange.isEmpty, true); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); + const Marker m3 = Marker(markerId: MarkerId('marker_3')); + const Marker m3updated = + Marker(markerId: MarkerId('marker_3'), draggable: true); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithMarkers({m1, m2})); + await tester.pumpWidget(_mapWithMarkers({m1, m3})); + await tester.pumpWidget(_mapWithMarkers({m1, m3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.markerUpdates.length, 3); + + expect(map.markerUpdates[0].markersToChange.isEmpty, true); + expect(map.markerUpdates[0].markersToAdd, {m1, m2}); + expect(map.markerUpdates[0].markerIdsToRemove.isEmpty, true); + + expect(map.markerUpdates[1].markersToChange.isEmpty, true); + expect(map.markerUpdates[1].markersToAdd, {m3}); + expect(map.markerUpdates[1].markerIdsToRemove, {m2.markerId}); + + expect(map.markerUpdates[2].markersToChange, {m3updated}); + expect(map.markerUpdates[2].markersToAdd.isEmpty, true); + expect(map.markerUpdates[2].markerIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart index 152cbddfc34a..08910fa5ccbb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithPolygons(Set polygons) { return Directionality( @@ -43,36 +43,25 @@ Polygon _polygonWithPointsAndHole(PolygonId polygonId) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a polygon', (WidgetTester tester) async { const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon initializedPolygon = + map.polygonUpdates.last.polygonsToAdd.first; expect(initializedPolygon, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Adding a polygon', (WidgetTester tester) async { @@ -82,16 +71,15 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon addedPolygon = map.polygonUpdates.last.polygonsToAdd.first; expect(addedPolygon, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Removing a polygon', (WidgetTester tester) async { @@ -100,13 +88,13 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p1.polygonId)); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Updating a polygon', (WidgetTester tester) async { @@ -117,13 +105,12 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Mutate a polygon', (WidgetTester tester) async { @@ -137,13 +124,12 @@ void main() { p1.points.add(const LatLng(1.0, 1.0)); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -157,12 +143,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, cur); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, cur); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -178,16 +163,16 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToAdd.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); + expect(map.polygonUpdates.last.polygonsToAdd.first, equals(p1)); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p3.polygonId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -201,12 +186,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, {p3}); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, {p3}); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -218,12 +202,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Initializing a polygon with points and hole', @@ -231,14 +214,14 @@ void main() { final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon initializedPolygon = + map.polygonUpdates.last.polygonsToAdd.first; expect(initializedPolygon, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Adding a polygon with points and hole', @@ -249,16 +232,15 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon addedPolygon = map.polygonUpdates.last.polygonsToAdd.first; expect(addedPolygon, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Removing a polygon with points and hole', @@ -268,13 +250,13 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p1.polygonId)); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Updating a polygon by adding points and hole', @@ -285,13 +267,12 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Mutate a polygon with points and holes', @@ -311,13 +292,12 @@ void main() { ..addAll(>[_rectPoints(size: 1)]); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update polygons with points and hole', @@ -339,12 +319,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, cur); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, cur); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update polygons with points and hole', @@ -368,16 +347,16 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToAdd.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); + expect(map.polygonUpdates.last.polygonsToAdd.first, equals(p1)); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p3.polygonId)); }); testWidgets('Partial Update polygons with points and hole', @@ -399,17 +378,44 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, {p3}); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, {p3}); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); + const Polygon p3 = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 1); + const Polygon p3updated = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithPolygons({p1, p2})); + await tester.pumpWidget(_mapWithPolygons({p1, p3})); + await tester.pumpWidget(_mapWithPolygons({p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polygonUpdates.length, 3); + + expect(map.polygonUpdates[0].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[0].polygonsToAdd, {p1, p2}); + expect(map.polygonUpdates[0].polygonIdsToRemove.isEmpty, true); + + expect(map.polygonUpdates[1].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[1].polygonsToAdd, {p3}); + expect(map.polygonUpdates[1].polygonIdsToRemove, {p2.polygonId}); + + expect(map.polygonUpdates[2].polygonsToChange, {p3updated}); + expect(map.polygonUpdates[2].polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates[2].polygonIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart index 03b6c620190a..cac311f7f2ed 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithPolylines(Set polylines) { return Directionality( @@ -20,36 +20,25 @@ Widget _mapWithPolylines(Set polylines) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a polyline', (WidgetTester tester) async { const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); await tester.pumpWidget(_mapWithPolylines({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); - final Polyline initializedPolyline = platformGoogleMap.polylinesToAdd.first; + final Polyline initializedPolyline = + map.polylineUpdates.last.polylinesToAdd.first; expect(initializedPolyline, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); }); testWidgets('Adding a polyline', (WidgetTester tester) async { @@ -59,16 +48,16 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); - final Polyline addedPolyline = platformGoogleMap.polylinesToAdd.first; + final Polyline addedPolyline = + map.polylineUpdates.last.polylinesToAdd.first; expect(addedPolyline, equals(p2)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); }); testWidgets('Removing a polyline', (WidgetTester tester) async { @@ -77,13 +66,13 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylineIdsToRemove.length, 1); - expect(platformGoogleMap.polylineIdsToRemove.first, equals(p1.polylineId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylineIdsToRemove.length, 1); + expect(map.polylineUpdates.last.polylineIdsToRemove.first, + equals(p1.polylineId)); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Updating a polyline', (WidgetTester tester) async { @@ -94,13 +83,12 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p2)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Updating a polyline', (WidgetTester tester) async { @@ -111,11 +99,10 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); - final Polyline update = platformGoogleMap.polylinesToChange.first; + final Polyline update = map.polylineUpdates.last.polylinesToChange.first; expect(update, equals(p2)); expect(update.geodesic, true); }); @@ -131,13 +118,12 @@ void main() { p1.points.add(const LatLng(1.0, 1.0)); await tester.pumpWidget(_mapWithPolylines({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -151,12 +137,11 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange, cur); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange, cur); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -172,16 +157,16 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToAdd.length, 1); - expect(platformGoogleMap.polylineIdsToRemove.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); + expect(map.polylineUpdates.last.polylineIdsToRemove.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p2)); - expect(platformGoogleMap.polylinesToAdd.first, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.first, equals(p3.polylineId)); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p2)); + expect(map.polylineUpdates.last.polylinesToAdd.first, equals(p1)); + expect(map.polylineUpdates.last.polylineIdsToRemove.first, + equals(p3.polylineId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -195,12 +180,11 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange, {p3}); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange, {p3}); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -212,17 +196,45 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange.isEmpty, true); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); + const Polyline p3 = + Polyline(polylineId: PolylineId('polyline_3'), width: 1); + const Polyline p3updated = + Polyline(polylineId: PolylineId('polyline_3'), width: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithPolylines({p1, p2})); + await tester.pumpWidget(_mapWithPolylines({p1, p3})); + await tester.pumpWidget(_mapWithPolylines({p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polylineUpdates.length, 3); + + expect(map.polylineUpdates[0].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[0].polylinesToAdd, {p1, p2}); + expect(map.polylineUpdates[0].polylineIdsToRemove.isEmpty, true); + + expect(map.polylineUpdates[1].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[1].polylinesToAdd, {p3}); + expect(map.polylineUpdates[1].polylineIdsToRemove, + {p2.polylineId}); + + expect(map.polylineUpdates[2].polylinesToChange, {p3updated}); + expect(map.polylineUpdates[2].polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates[2].polylineIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart index e4e4514dd501..c2faca593bc6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithTileOverlays(Set tileOverlays) { return Directionality( @@ -20,20 +20,11 @@ Widget _mapWithTileOverlays(Set tileOverlays) { } void main() { - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a tile overlay', (WidgetTester tester) async { @@ -41,15 +32,8 @@ void main() { TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); await tester.pumpWidget(_mapWithTileOverlays({t1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - - final TileOverlay initializedTileOverlay = - platformGoogleMap.tileOverlaysToAdd.first; - expect(initializedTileOverlay, equals(t1)); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t1})); }); testWidgets('Adding a tile overlay', (WidgetTester tester) async { @@ -61,16 +45,8 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t1, t2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - - final TileOverlay addedTileOverlay = - platformGoogleMap.tileOverlaysToAdd.first; - expect(addedTileOverlay, equals(t2)); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t1, t2})); }); testWidgets('Removing a tile overlay', (WidgetTester tester) async { @@ -80,32 +56,8 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); - expect(platformGoogleMap.tileOverlayIdsToRemove.first, - equals(t1.tileOverlayId)); - - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); - }); - - testWidgets('Updating a tile overlay', (WidgetTester tester) async { - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - const TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1'), zIndex: 10); - - await tester.pumpWidget(_mapWithTileOverlays({t1})); - await tester.pumpWidget(_mapWithTileOverlays({t2})); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); - - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({})); }); testWidgets('Updating a tile overlay', (WidgetTester tester) async { @@ -117,94 +69,7 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - - final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first; - expect(update, equals(t2)); - expect(update.zIndex, 10); - }); - - testWidgets('Multi Update', (WidgetTester tester) async { - TileOverlay t1 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - TileOverlay t2 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - final Set prev = {t1, t2}; - t1 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), visible: false); - t2 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); - final Set cur = {t1, t2}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange, cur); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); - }); - - testWidgets('Multi Update', (WidgetTester tester) async { - TileOverlay t2 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - const TileOverlay t3 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); - final Set prev = {t2, t3}; - - // t1 is added, t2 is updated, t3 is removed. - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - t2 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); - final Set cur = {t1, t2}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); - - expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); - expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1)); - expect(platformGoogleMap.tileOverlayIdsToRemove.first, - equals(t3.tileOverlayId)); - }); - - testWidgets('Partial Update', (WidgetTester tester) async { - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - const TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - TileOverlay t3 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); - final Set prev = {t1, t2, t3}; - t3 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_3'), zIndex: 10); - final Set cur = {t1, t2, t3}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange, {t3}); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t2})); }); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 525576f013cd..dab6b69b98ba 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -25,3 +25,10 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.1 + +dependency_overrides: + # Override the google_maps_flutter dependency on google_maps_flutter_web. + # TODO(ditman): Unwind the circular dependency. This will create problems + # if we need to make a breaking change to google_maps_flutter_web. + google_maps_flutter_web: + path: ../ diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist index 6c749634f53d..08fef9a9fe42 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist @@ -56,8 +56,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index e31a3c1d99c4..2d9aae4ff784 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.16 + +* Updates Guava to version 32.0.1. + ## 6.1.15 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index 8c2566852df5..388821458158 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -60,7 +60,7 @@ android { dependencies { implementation 'com.google.android.gms:play-services-auth:20.5.0' - implementation 'com.google.guava:guava:32.0.0-android' + implementation 'com.google.guava:guava:32.0.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' } diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index db3fd5f8d716..046baa1e8c6f 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.15 +version: 6.1.16 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist index 6c749634f53d..08fef9a9fe42 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist @@ -56,8 +56,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index f1e09f50b51b..d0a667bf018f 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.8.9 + +* Adds `getMedia` and `getMultipleMedia` methods. + +## 0.8.8 + +* Adds initial support for Windows, macOS, and Linux. + * See README for current desktop limitations. +* Adds `supportsImageSource` to allow runtime checks for whether a given source + is supported by the current platform's implementation. + ## 0.8.7+5 * Fixes `BuildContext` handling in example. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index b566116c8595..33ecc2edee81 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -6,9 +6,9 @@ A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. -| | Android | iOS | Web | -|-------------|---------|---------|---------------------------------| -| **Support** | SDK 21+ | iOS 11+ | [See `image_picker_for_web`][1] | +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|---------|-------|--------|---------------------------------|-------------| +| **Support** | SDK 21+ | iOS 11+ | Any | 10.14+ | [See `image_picker_for_web`][1] | Windows 10+ | ## Installation @@ -59,7 +59,7 @@ When under high memory pressure the Android system may kill the MainActivity of the application using the image_picker. On Android the image_picker makes use of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` intents. This means that while the intent is executing the source application -is moved to the background and becomes eligable for cleanup when the system is +is moved to the background and becomes eligible for cleanup when the system is low on memory. When the intent finishes executing, Android will restart the application. Since the data is never returned to the original call use the `ImagePicker.retrieveLostData()` method to retrieve the lost data. For example: @@ -109,6 +109,61 @@ As activities cannot communicate between tasks, the image picker activity cannot send back its eventual result to the calling activity. To work around this problem, consider using `launchMode: singleTask` instead. +### Windows, macOS, and Linux + +This plugin currently has limited support for the three desktop platforms, +serving as a wrapper around the [`file_selector`](https://pub.dev/packages/file_selector) +plugin with appropriate file type filters set. Selection modification options, +such as max width and height, are not yet supported. + +By default, `ImageSource.camera` is not supported, since unlike on Android and +iOS there is no system-provided UI for taking photos. However, the desktop +implementations allow delegating to a camera handler by setting a +`cameraDelegate` before using `image_picker`, such as in `main()`: + + +``` dart +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// ··· +class MyCameraDelegate extends ImagePickerCameraDelegate { + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAPhoto(options.preferredCameraDevice); + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAVideo(options.preferredCameraDevice); + } +} +// ··· +void setUpCameraDelegate() { + final ImagePickerPlatform instance = ImagePickerPlatform.instance; + if (instance is CameraDelegatingImagePickerPlatform) { + instance.cameraDelegate = MyCameraDelegate(); + } +} +``` + +Once you have set a `cameraDelegate`, `image_picker` calls with +`ImageSource.camera` will work as normal, calling your provided delegate. We +encourage the community to build packages that implement +`ImagePickerCameraDelegate`, to provide options for desktop camera UI. + +#### macOS installation + +Since the macOS implementation uses `file_selector`, you will need to +add a filesystem access +[entitlement][https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox]: +```xml + com.apple.security.files.user-selected.read-only + +``` + ### Example @@ -125,6 +180,10 @@ final XFile? galleryVideo = final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); +// Pick singe image or video. +final XFile? media = await picker.pickMedia(); +// Pick multiple images and videos. +final List medias = await picker.pickMultipleMedia(); ``` ## Migrating to 0.8.2+ diff --git a/packages/image_picker/image_picker/example/ios/Runner/Info.plist b/packages/image_picker/image_picker/example/ios/Runner/Info.plist index 423e21fd1672..90eb79e9ad18 100755 --- a/packages/image_picker/image_picker/example/ios/Runner/Info.plist +++ b/packages/image_picker/image_picker/example/ios/Runner/Info.plist @@ -53,8 +53,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 4a96ce194ef9..b1431c5c33bb 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,10 +39,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -80,8 +81,12 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -94,14 +99,42 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = await _picker.pickMultiImage( + final List pickedFileList = isMedia + ? await _picker.pickMultipleMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ) + : await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = await _picker.pickMedia( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _imageFileList = pickedFileList; - }); + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() { _pickImageError = e; @@ -179,28 +212,34 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + // Why network for web? // See https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform return Semantics( label: 'image_picker_example_picked_image', child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file( - File(_imageFileList![index].path), - errorBuilder: (BuildContext context, Object error, - StackTrace? stackTrace) => - const Center( - child: Text('This image type is not supported')), - ), + ? Image.network(_mediaFileList![index].path) + : (mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index)), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -216,6 +255,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (isVideo) { return _previewVideo(); @@ -239,7 +289,7 @@ class _MyHomePageState extends State { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -309,10 +359,11 @@ class _MyHomePageState extends State { ImageSource.gallery, context: context, isMultiImage: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', child: const Icon(Icons.photo_library), ), ), @@ -321,39 +372,73 @@ class _MyHomePageState extends State { child: FloatingActionButton( onPressed: () { isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); }, - heroTag: 'image2', - tooltip: 'Take a Photo', - child: const Icon(Icons.camera_alt), + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), ), ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( - backgroundColor: Colors.red, onPressed: () { - isVideo = true; - _onImageButtonPressed(ImageSource.gallery, context: context); + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - child: const Icon(Icons.video_library), + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), ), ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - child: const Icon(Icons.videocam), + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), ), ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), ], ), ); diff --git a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart index 3f0f0788ae3c..15c8185ecf6e 100644 --- a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart +++ b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart @@ -4,6 +4,28 @@ import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +// #docregion CameraDelegate +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// #enddocregion CameraDelegate + +/// Example of a camera delegate +// #docregion CameraDelegate +class MyCameraDelegate extends ImagePickerCameraDelegate { + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAPhoto(options.preferredCameraDevice); + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAVideo(options.preferredCameraDevice); + } +} +// #enddocregion CameraDelegate /// Example function for README demonstration of various pick* calls. Future> readmePickExample() async { @@ -20,6 +42,10 @@ Future> readmePickExample() async { final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); + // Pick singe image or video. + final XFile? media = await picker.pickMedia(); + // Pick multiple images and videos. + final List medias = await picker.pickMultipleMedia(); // #enddocregion Pick // Return everything for the sanity check test. @@ -28,7 +54,9 @@ Future> readmePickExample() async { photo, galleryVideo, cameraVideo, - if (images.isEmpty) null else images.first + if (images.isEmpty) null else images.first, + media, + if (medias.isEmpty) null else medias.first, ]; } @@ -49,6 +77,20 @@ Future getLostData() async { } // #enddocregion LostData +/// Example of camera delegate setup. +// #docregion CameraDelegate +void setUpCameraDelegate() { + final ImagePickerPlatform instance = ImagePickerPlatform.instance; + if (instance is CameraDelegatingImagePickerPlatform) { + instance.cameraDelegate = MyCameraDelegate(); + } +} +// #enddocregion CameraDelegate + // Stubs for the getLostData function. void _handleLostFiles(List file) {} void _handleError(PlatformException? exception) {} + +// Stubs for MyCameraDelegate. +Future _takeAPhoto(CameraDevice device) async => null; +Future _takeAVideo(CameraDevice device) async => null; diff --git a/packages/image_picker/image_picker/example/linux/.gitignore b/packages/image_picker/image_picker/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/image_picker/image_picker/example/linux/CMakeLists.txt b/packages/image_picker/image_picker/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..f30eeac66816 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "image_picker_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flutter.plugins.image_picker_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt b/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake b/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker/example/linux/main.cc b/packages/image_picker/image_picker/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/main.cc @@ -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. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/image_picker/image_picker/example/linux/my_application.cc b/packages/image_picker/image_picker/example/linux/my_application.cc new file mode 100644 index 000000000000..08e3dc4c1603 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/my_application.cc @@ -0,0 +1,111 @@ +// 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 "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "image_picker_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "image_picker_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/image_picker/image_picker/example/linux/my_application.h b/packages/image_picker/image_picker/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/my_application.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/image_picker/image_picker/example/macos/.gitignore b/packages/image_picker/image_picker/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Podfile b/packages/image_picker/image_picker/example/macos/Podfile new file mode 100644 index 000000000000..049abe295427 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..31d4a2f7511c --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,573 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* image_picker_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "image_picker_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* image_picker_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* image_picker_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..94322033850a --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift b/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// 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 Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..82b6f9d9a33e Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..13b35eba55c6 Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..0a3f5fa40fb3 Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bdb57226d5f2 Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..f083318e09ca Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..326c0e72c9d8 Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..2f1632cfddf3 Binary files /dev/null and b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/image_picker/image_picker/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/image_picker/image_picker/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..d2192aa2260c --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = image_picker_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 dev.flutter.plugins. All rights reserved. diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements b/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/Info.plist b/packages/image_picker/image_picker/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift b/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift @@ -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 Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements b/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 6b38b56bbed8..4fbeb73be3f6 100644 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # 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: ../ - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart index 771d5d419de1..512438ce2b5c 100644 --- a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart +++ b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart @@ -50,6 +50,13 @@ class FakeImagePicker extends ImagePickerPlatform { return [XFile('multiImage')]; } + @override + Future> getMedia({required MediaOptions options}) async { + return options.allowMultiple + ? [XFile('medias'), XFile('medias')] + : [XFile('media')]; + } + @override Future getVideo( {required ImageSource source, diff --git a/packages/image_picker/image_picker/example/windows/.gitignore b/packages/image_picker/image_picker/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/image_picker/image_picker/example/windows/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..948306ac83f7 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(image_picker_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "image_picker_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..930d2071a324 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake b/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..a423a02476a2 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..394917c053a0 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/image_picker/image_picker/example/windows/runner/Runner.rc b/packages/image_picker/image_picker/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..c9d919d0ddc3 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flutter.plugins" "\0" + VALUE "FileDescription", "image_picker_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "image_picker_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 dev.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "image_picker_example.exe" "\0" + VALUE "ProductName", "image_picker_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp b/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..7be2fe290105 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp @@ -0,0 +1,68 @@ +// 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 "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/image_picker/image_picker/example/windows/runner/flutter_window.h b/packages/image_picker/image_picker/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/image_picker/image_picker/example/windows/runner/main.cpp b/packages/image_picker/image_picker/example/windows/runner/main.cpp new file mode 100644 index 000000000000..0428c41b79eb --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// 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 +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"image_picker_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/image_picker/image_picker/example/windows/runner/resource.h b/packages/image_picker/image_picker/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico b/packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest b/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..a42ea7687cb6 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/windows/runner/utils.cpp b/packages/image_picker/image_picker/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..cb6aebe4e593 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/utils.cpp @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/image_picker/image_picker/example/windows/runner/utils.h b/packages/image_picker/image_picker/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/utils.h @@ -0,0 +1,23 @@ +// 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. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp b/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..daefc277e4c7 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp @@ -0,0 +1,284 @@ +// 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 "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: +/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = + L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = + RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, + &light_mode, &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/image_picker/image_picker/example/windows/runner/win32_window.h b/packages/image_picker/image_picker/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..33d491ead922 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/win32_window.h @@ -0,0 +1,104 @@ +// 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. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index ce16763ec6af..a558dbd7d55c 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -27,7 +27,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the image that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -78,7 +78,7 @@ class ImagePicker { /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -115,7 +115,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the video that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -151,7 +151,7 @@ class ImagePicker { /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a /// successful image/video selection, or a failure. @@ -168,7 +168,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the image that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -217,32 +217,24 @@ class ImagePicker { CameraDevice preferredCameraDevice = CameraDevice.rear, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImagePickerOptions imagePickerOptions = + ImagePickerOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ); return platform.getImageFromSource( source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - requestFullMetadata: requestFullMetadata, - ), + options: imagePickerOptions, ); } /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -277,22 +269,121 @@ class ImagePicker { int? imageQuality, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImageOptions imageOptions = ImageOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); return platform.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( + imageOptions: imageOptions, + ), + ); + } + + /// Returns an [XFile] of the image or video that was picked. + /// The image or videos can only come from the gallery. + /// + /// The returned [XFile] is intended to be used within a single app session. + /// Do not save the file path and use it across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no image or video was picked, the return value is null. + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) async { + final List listMedia = await platform.getMedia( + options: MediaOptions( + imageOptions: ImageOptions.createAndValidate( + maxHeight: maxHeight, maxWidth: maxWidth, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ), + allowMultiple: false, + ), + ); + + return listMedia.isNotEmpty ? listMedia.first : null; + } + + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the gallery. + /// + /// The returned [List] is intended to be used within a single app session. + /// Do not save the file paths and use them across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at their + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> pickMultipleMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) { + return platform.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( maxHeight: maxHeight, + maxWidth: maxWidth, imageQuality: imageQuality, requestFullMetadata: requestFullMetadata, ), @@ -302,7 +393,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the video that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -338,7 +429,7 @@ class ImagePicker { /// is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostDataResponse] object if successfully retrieved the lost data. The [LostDataResponse] object can \ /// represent either a successful image/video selection, or a failure. @@ -351,4 +442,12 @@ class ImagePicker { Future retrieveLostData() { return platform.getLostData(); } + + /// Returns true if the current platform implementation supports [source]. + /// + /// Calling a `pick*` method with a source for which this method + /// returns `false` will throw an error. + bool supportsImageSource(ImageSource source) { + return platform.supportsImageSource(source); + } } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index f927befc90f1..69e255c65bab 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+5 +version: 0.8.9 environment: sdk: ">=2.18.0 <4.0.0" @@ -16,8 +16,14 @@ flutter: default_package: image_picker_android ios: default_package: image_picker_ios + linux: + default_package: image_picker_linux + macos: + default_package: image_picker_macos web: default_package: image_picker_for_web + windows: + default_package: image_picker_windows dependencies: flutter: @@ -25,7 +31,10 @@ dependencies: image_picker_android: ^0.8.4+11 image_picker_for_web: ^2.1.0 image_picker_ios: ^0.8.6+1 - image_picker_platform_interface: ^2.6.1 + image_picker_linux: ^0.2.0 + image_picker_macos: ^0.2.0 + image_picker_platform_interface: ^2.8.0 + image_picker_windows: ^0.2.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 637ecf9c6e7a..4ff5b4e025d0 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -587,5 +587,370 @@ void main() { }); }); }); + + group('#Media', () { + setUp(() { + when( + mockPlatform.getMedia( + options: anyNamed('options'), + ), + ).thenAnswer((Invocation _) async => []); + }); + + group('#pickMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + await picker.pickMedia( + maxWidth: 10.0, + ); + await picker.pickMedia( + maxHeight: 10.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); + + expect(await picker.pickMedia(), isNull); + expect(await picker.pickMedia(), isNull); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + + group('#pickMultipleMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + await picker.pickMultipleMedia( + maxWidth: 10.0, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMultipleMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultipleMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); + + expect(await picker.pickMultipleMedia(), isEmpty); + expect(await picker.pickMultipleMedia(), isEmpty); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + test('supportsImageSource calls through to platform', () async { + final ImagePicker picker = ImagePicker(); + when(mockPlatform.supportsImageSource(any)).thenReturn(true); + + final bool supported = picker.supportsImageSource(ImageSource.camera); + + expect(supported, true); + verify(mockPlatform.supportsImageSource(ImageSource.camera)); + }); + }); }); } diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index d1e19830e919..85c9df08c790 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -165,6 +165,16 @@ class MockImagePickerPlatform extends _i1.Mock returnValue: _i4.Future?>.value(), ) as _i4.Future?>); @override + _i4.Future> getMedia({required _i2.MediaOptions? options}) => + (super.noSuchMethod( + Invocation.method( + #getMedia, + [], + {#options: options}, + ), + returnValue: _i4.Future>.value(<_i5.XFile>[]), + ) as _i4.Future>); + @override _i4.Future<_i5.XFile?> getVideo({ required _i2.ImageSource? source, _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear, @@ -225,4 +235,12 @@ class MockImagePickerPlatform extends _i1.Mock ), returnValue: _i4.Future>.value(<_i5.XFile>[]), ) as _i4.Future>); + @override + bool supportsImageSource(_i2.ImageSource? source) => (super.noSuchMethod( + Invocation.method( + #supportsImageSource, + [source], + ), + returnValue: false, + ) as bool); } diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index cfb5ae4cb54a..971cfe08f334 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.8.7 + +* Adds `getMedia` method. + +## 0.8.6+20 + +* Bumps androidx.activity:activity from 1.7.0 to 1.7.1. + ## 0.8.6+19 * Bumps androidx.core:core from 1.9.0 to 1.10.1. diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index 6cced2f1edf9..b64b437bda4b 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -41,7 +41,7 @@ android { implementation 'androidx.core:core:1.10.1' implementation 'androidx.annotation:annotation:1.3.0' implementation 'androidx.exifinterface:exifinterface:1.3.6' - implementation 'androidx.activity:activity:1.7.0' + implementation 'androidx.activity:activity:1.7.1' // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.21")) diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index d216309d5888..685534ec6ab8 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -79,6 +79,7 @@ public class ImagePickerDelegate @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343; @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346; + @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; @@ -279,6 +280,52 @@ Messages.CacheRetrievalResult retrieveLostImage() { return result.build(); } + public void chooseMediaFromGallery( + @NonNull Messages.MediaSelectionOptions options, + @NonNull Messages.GeneralOptions generalOptions, + @NonNull Messages.Result> result) { + if (!setPendingOptionsAndResult(options.getImageSelectionOptions(), null, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchPickMediaFromGalleryIntent(generalOptions); + } + + private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) { + Intent pickMediaIntent; + if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (generalOptions.getAllowMultiple()) { + pickMediaIntent = + new ActivityResultContracts.PickMultipleVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } else { + pickMediaIntent = + new ActivityResultContracts.PickVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } + } else { + pickMediaIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickMediaIntent.setType("*/*"); + String[] mimeTypes = {"video/*", "image/*"}; + pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple()); + } + } + activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); + } + public void chooseVideoFromGallery( @NonNull VideoSelectionOptions options, boolean usePhotoPicker, @@ -291,9 +338,9 @@ public void chooseVideoFromGallery( launchPickVideoFromGalleryIntent(usePhotoPicker); } - private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) { Intent pickVideoIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickVideoIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -389,9 +436,9 @@ public void chooseMultiImageFromGallery( launchMultiPickImageFromGalleryIntent(usePhotoPicker); } - private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickImageIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -406,9 +453,9 @@ private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } - private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickMultiImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickMultiImageIntent = new ActivityResultContracts.PickMultipleVisualMedia() .createIntent( @@ -563,6 +610,9 @@ public boolean onActivityResult( case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handlerRunnable = () -> handleCaptureImageResult(resultCode); break; + case REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY: + handlerRunnable = () -> handleChooseMediaResult(resultCode, data); + break; case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY: handlerRunnable = () -> handleChooseVideoResult(resultCode, data); break; @@ -589,17 +639,59 @@ private void handleChooseImageResult(int resultCode, Intent data) { finishWithSuccess(null); } + public class MediaPath { + public MediaPath(@NonNull String path, @Nullable String mimeType) { + this.path = path; + this.mimeType = mimeType; + } + + final String path; + final String mimeType; + + public @NonNull String getPath() { + return path; + } + + public @Nullable String getMimeType() { + return mimeType; + } + } + + private void handleChooseMediaResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = new ArrayList<>(); + if (intent.getClipData() != null) { + for (int i = 0; i < intent.getClipData().getItemCount(); i++) { + Uri uri = intent.getClipData().getItemAt(i).getUri(); + String path = fileUtils.getPathFromUri(activity, uri); + String mimeType = activity.getContentResolver().getType(uri); + paths.add(new MediaPath(path, mimeType)); + } + } else { + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); + } + handleMediaResult(paths); + return; + } + + // User cancelled choosing a picture. + finishWithSuccess(null); + } + private void handleChooseMultiImageResult(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_OK && intent != null) { - ArrayList paths = new ArrayList<>(); + ArrayList paths = new ArrayList<>(); if (intent.getClipData() != null) { for (int i = 0; i < intent.getClipData().getItemCount(); i++) { - paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri())); + paths.add( + new MediaPath( + fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()), + null)); } } else { - paths.add(fileUtils.getPathFromUri(activity, intent.getData())); + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); } - handleMultiImageResult(paths); + handleMediaResult(paths); return; } @@ -649,26 +741,6 @@ private void handleCaptureVideoResult(int resultCode) { finishWithSuccess(null); } - private void handleMultiImageResult(ArrayList paths) { - ImageSelectionOptions localImageOptions = null; - synchronized (pendingCallStateLock) { - if (pendingCallState != null) { - localImageOptions = pendingCallState.imageOptions; - } - } - - if (localImageOptions != null) { - ArrayList finalPath = new ArrayList<>(); - for (int i = 0; i < paths.size(); i++) { - String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions); - finalPath.add(i, finalImagePath); - } - finishWithListSuccess(finalPath); - } else { - finishWithListSuccess(paths); - } - } - void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { ImageSelectionOptions localImageOptions = null; synchronized (pendingCallStateLock) { @@ -679,7 +751,7 @@ void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { if (localImageOptions != null) { String finalImagePath = getResizedImagePath(path, localImageOptions); - //delete original file if scaled + // Delete original file if scaled. if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); } @@ -697,7 +769,34 @@ private String getResizedImagePath(String path, @NonNull ImageSelectionOptions o outputOptions.getQuality().intValue()); } - void handleVideoResult(String path) { + private void handleMediaResult(@NonNull ArrayList paths) { + ImageSelectionOptions localImageOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localImageOptions = pendingCallState.imageOptions; + } + } + + ArrayList finalPaths = new ArrayList<>(); + if (localImageOptions != null) { + for (int i = 0; i < paths.size(); i++) { + MediaPath path = paths.get(i); + String finalPath = path.path; + if (path.mimeType == null || !path.mimeType.startsWith("video/")) { + finalPath = getResizedImagePath(path.path, localImageOptions); + } + finalPaths.add(finalPath); + } + finishWithListSuccess(finalPaths); + } else { + for (int i = 0; i < paths.size(); i++) { + finalPaths.add(paths.get(i).path); + } + finishWithListSuccess(finalPaths); + } + } + + private void handleVideoResult(String path) { finishWithSuccess(path); } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 31b2303a37cb..b5deb289341a 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -19,10 +19,16 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImagePickerApi; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.Result; +import io.flutter.plugins.imagepicker.Messages.SourceCamera; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @SuppressWarnings("deprecation") @@ -279,7 +285,7 @@ final ImagePickerDelegate constructDelegate(final Activity setupActivity) { private void setCameraDevice( @NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) { - Messages.SourceCamera camera = source.getCamera(); + SourceCamera camera = source.getCamera(); if (camera != null) { ImagePickerDelegate.CameraDevice device; switch (camera) { @@ -298,9 +304,8 @@ private void setCameraDevice( @Override public void pickImages( @NonNull SourceSpecification source, - @NonNull Messages.ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull ImageSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -311,12 +316,12 @@ public void pickImages( } setCameraDevice(delegate, source); - if (allowMultiple) { - delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result); + if (generalOptions.getAllowMultiple()) { + delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); } else { switch (source.getType()) { case GALLERY: - delegate.chooseImageFromGallery(options, usePhotoPicker, result); + delegate.chooseImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeImageWithCamera(options, result); @@ -325,12 +330,26 @@ public void pickImages( } } + @Override + public void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result) { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + result.error( + new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null)); + return; + } + delegate.chooseMediaFromGallery(mediaSelectionOptions, generalOptions, result); + } + @Override public void pickVideos( @NonNull SourceSpecification source, - @NonNull Messages.VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull VideoSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -341,12 +360,12 @@ public void pickVideos( } setCameraDevice(delegate, source); - if (allowMultiple) { + if (generalOptions.getAllowMultiple()) { result.error(new RuntimeException("Multi-video selection is not implemented")); } else { switch (source.getType()) { case GALLERY: - delegate.chooseVideoFromGallery(options, usePhotoPicker, result); + delegate.chooseVideoFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeVideoWithCamera(options, result); @@ -357,7 +376,7 @@ public void pickVideos( @Nullable @Override - public Messages.CacheRetrievalResult retrieveLostResults() { + public CacheRetrievalResult retrieveLostResults() { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { throw new FlutterError( diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java index 17390ac6961c..8a19cfd3c55a 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java @@ -88,6 +88,79 @@ private CacheRetrievalType(final int index) { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class GeneralOptions { + private @NonNull Boolean allowMultiple; + + public @NonNull Boolean getAllowMultiple() { + return allowMultiple; + } + + public void setAllowMultiple(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"allowMultiple\" is null."); + } + this.allowMultiple = setterArg; + } + + private @NonNull Boolean usePhotoPicker; + + public @NonNull Boolean getUsePhotoPicker() { + return usePhotoPicker; + } + + public void setUsePhotoPicker(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"usePhotoPicker\" is null."); + } + this.usePhotoPicker = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + GeneralOptions() {} + + public static final class Builder { + + private @Nullable Boolean allowMultiple; + + public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) { + this.allowMultiple = setterArg; + return this; + } + + private @Nullable Boolean usePhotoPicker; + + public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) { + this.usePhotoPicker = setterArg; + return this; + } + + public @NonNull GeneralOptions build() { + GeneralOptions pigeonReturn = new GeneralOptions(); + pigeonReturn.setAllowMultiple(allowMultiple); + pigeonReturn.setUsePhotoPicker(usePhotoPicker); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(allowMultiple); + toListResult.add(usePhotoPicker); + return toListResult; + } + + static @NonNull GeneralOptions fromList(@NonNull ArrayList list) { + GeneralOptions pigeonResult = new GeneralOptions(); + Object allowMultiple = list.get(0); + pigeonResult.setAllowMultiple((Boolean) allowMultiple); + Object usePhotoPicker = list.get(1); + pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -193,6 +266,58 @@ ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class MediaSelectionOptions { + private @NonNull ImageSelectionOptions imageSelectionOptions; + + public @NonNull ImageSelectionOptions getImageSelectionOptions() { + return imageSelectionOptions; + } + + public void setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"imageSelectionOptions\" is null."); + } + this.imageSelectionOptions = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + MediaSelectionOptions() {} + + public static final class Builder { + + private @Nullable ImageSelectionOptions imageSelectionOptions; + + public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + this.imageSelectionOptions = setterArg; + return this; + } + + public @NonNull MediaSelectionOptions build() { + MediaSelectionOptions pigeonReturn = new MediaSelectionOptions(); + pigeonReturn.setImageSelectionOptions(imageSelectionOptions); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add((imageSelectionOptions == null) ? null : imageSelectionOptions.toList()); + return toListResult; + } + + static @NonNull MediaSelectionOptions fromList(@NonNull ArrayList list) { + MediaSelectionOptions pigeonResult = new MediaSelectionOptions(); + Object imageSelectionOptions = list.get(0); + pigeonResult.setImageSelectionOptions( + (imageSelectionOptions == null) + ? null + : ImageSelectionOptions.fromList((ArrayList) imageSelectionOptions)); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -523,10 +648,14 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 129: return CacheRetrievalResult.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); + return GeneralOptions.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return SourceSpecification.fromList((ArrayList) readValue(buffer)); + return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return MediaSelectionOptions.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return SourceSpecification.fromList((ArrayList) readValue(buffer)); + case (byte) 134: return VideoSelectionOptions.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -541,14 +670,20 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof CacheRetrievalResult) { stream.write(129); writeValue(stream, ((CacheRetrievalResult) value).toList()); - } else if (value instanceof ImageSelectionOptions) { + } else if (value instanceof GeneralOptions) { stream.write(130); + writeValue(stream, ((GeneralOptions) value).toList()); + } else if (value instanceof ImageSelectionOptions) { + stream.write(131); writeValue(stream, ((ImageSelectionOptions) value).toList()); + } else if (value instanceof MediaSelectionOptions) { + stream.write(132); + writeValue(stream, ((MediaSelectionOptions) value).toList()); } else if (value instanceof SourceSpecification) { - stream.write(131); + stream.write(133); writeValue(stream, ((SourceSpecification) value).toList()); } else if (value instanceof VideoSelectionOptions) { - stream.write(132); + stream.write(134); writeValue(stream, ((VideoSelectionOptions) value).toList()); } else { super.writeValue(stream, value); @@ -567,8 +702,7 @@ public interface ImagePickerApi { void pickImages( @NonNull SourceSpecification source, @NonNull ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** * Selects video and returns their paths. @@ -579,8 +713,17 @@ void pickImages( void pickVideos( @NonNull SourceSpecification source, @NonNull VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result); + /** + * Selects images and videos and returns their paths. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** Returns results from a previous app session, if any. */ @Nullable @@ -607,8 +750,7 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePicke ArrayList args = (ArrayList) message; SourceSpecification sourceArg = (SourceSpecification) args.get(0); ImageSelectionOptions optionsArg = (ImageSelectionOptions) args.get(1); - Boolean allowMultipleArg = (Boolean) args.get(2); - Boolean usePhotoPickerArg = (Boolean) args.get(3); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); Result> resultCallback = new Result>() { public void success(List result) { @@ -622,8 +764,7 @@ public void error(Throwable error) { } }; - api.pickImages( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); @@ -644,8 +785,38 @@ public void error(Throwable error) { ArrayList args = (ArrayList) message; SourceSpecification sourceArg = (SourceSpecification) args.get(0); VideoSelectionOptions optionsArg = (VideoSelectionOptions) args.get(1); - Boolean allowMultipleArg = (Boolean) args.get(2); - Boolean usePhotoPickerArg = (Boolean) args.get(3); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.pickVideos(sourceArg, optionsArg, generalOptionsArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + MediaSelectionOptions mediaSelectionOptionsArg = + (MediaSelectionOptions) args.get(0); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(1); Result> resultCallback = new Result>() { public void success(List result) { @@ -659,8 +830,7 @@ public void error(Throwable error) { } }; - api.pickVideos( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickMedia(mediaSelectionOptionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index efdbbae3b7f9..73ee5a0f0d49 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -29,7 +29,9 @@ import android.net.Uri; import androidx.annotation.Nullable; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; import java.io.IOException; @@ -61,6 +63,8 @@ public class ImagePickerDelegateTest { new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; @@ -161,6 +165,18 @@ public void chooseMultiImageFromGallery_whenPendingResultExists_finishesWithAlre verifyNoMoreInteractions(mockResult); } + @Test + public void chooseMediaFromGallery_whenPendingResultExists_finishesWithAlreadyActiveError() { + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); + GeneralOptions generalOptions = + new GeneralOptions.Builder().setAllowMultiple(true).setUsePhotoPicker(true).build(); + delegate.chooseMediaFromGallery(DEFAULT_MEDIA_OPTIONS, generalOptions, mockResult); + + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); + } + @Test @Config(sdk = 30) public void chooseImageFromGallery_launchesChooseFromGalleryIntent() { @@ -631,6 +647,19 @@ public void onActivityResult_whenMultipleImagesPickedFromGallery_returnsTrue() { assertTrue(isHandled); } + @Test + public void onActivityResult_whenMediaPickedFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + @Test public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() { ImagePickerDelegate delegate = createDelegate(); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index cd408c5cef43..b2c281ca540d 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -24,7 +24,9 @@ import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @@ -40,6 +42,16 @@ public class ImagePickerPluginTest { new ImageSelectionOptions.Builder().setQuality((long) 100).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(true).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build(); private static final SourceSpecification SOURCE_GALLERY = new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build(); private static final SourceSpecification SOURCE_CAMERA_FRONT = @@ -88,7 +100,10 @@ public void pickImages_whenActivityIsNull_finishesWithForegroundActivityRequired ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickImages( - SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -103,7 +118,10 @@ public void pickVideos_whenActivityIsNull_finishesWithForegroundActivityRequired ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickVideos( - SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -126,60 +144,126 @@ public void retrieveLostResults_whenActivityIsNull_finishesWithForegroundActivit @Test public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } + @Test + public void pickMedia_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickMedia_usingPhotoPicker_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).takeImageWithCamera(any(), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_FRONT, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_FRONT, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index fa87587857b0..7d58a2a69074 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; // #enddocregion photo-picker-example +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void appMain() { @@ -55,14 +56,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -77,18 +78,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -101,12 +94,13 @@ class _MyHomePageState extends State { ImageSource source, { required BuildContext context, bool isMultiImage = false, + bool isMedia = false, }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); if (file != null && context.mounted) { @@ -117,15 +111,54 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); + final List? pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); if (pickedFileList != null && context.mounted) { _showPickedSnackBar(context, pickedFileList); } - setState(() => _imageFileList = pickedFileList); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() => _pickImageError = e); } @@ -200,30 +233,37 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - final XFile image = _imageFileList![index]; + final XFile image = _mediaFileList![index]; + final String? mime = lookupMimeType(_mediaFileList![index].path); return Column( mainAxisSize: MainAxisSize.min, children: [ Text(image.name, key: const Key('image_picker_example_picked_image_name')), - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(image.path) - : Image.file(File(image.path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ), ], ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -239,8 +279,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -254,15 +305,15 @@ class _MyHomePageState extends State { } if (response.file != null) { if (response.type == RetrieveType.video) { - isVideo = true; + _isVideo = true; await _playVideo(response.file); } else { - isVideo = false; + _isVideo = false; setState(() { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -316,7 +367,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -328,7 +379,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -344,7 +428,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -357,7 +441,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -370,7 +454,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -510,3 +594,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index 8921a37a67e9..1cce21a99e7d 100644 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -19,7 +19,8 @@ dependencies: # 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: ../ - image_picker_platform_interface: ^2.3.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart index fbc7fa7c2ad2..c9e2c875e8b5 100644 --- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -11,12 +11,17 @@ import 'src/messages.g.dart'; /// An Android implementation of [ImagePickerPlatform]. class ImagePickerAndroid extends ImagePickerPlatform { - /// Creates a new plugin implemenation instance. + /// Creates a new plugin implementation instance. ImagePickerAndroid({@visibleForTesting ImagePickerApi? api}) : _hostApi = api ?? ImagePickerApi(); final ImagePickerApi _hostApi; + /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. + /// + /// Currently defaults to false, but the default is subject to change. + bool useAndroidPhotoPicker = false; + /// Registers this class as the default platform implementation. static void registerWith() { ImagePickerPlatform.instance = ImagePickerAndroid(); @@ -77,13 +82,14 @@ class ImagePickerAndroid extends ImagePickerPlatform { } return _hostApi.pickImages( - SourceSpecification(type: SourceType.gallery), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ true, - useAndroidPhotoPicker); + SourceSpecification(type: SourceType.gallery), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker), + ); } Future _getImagePath({ @@ -108,13 +114,16 @@ class ImagePickerAndroid extends ImagePickerPlatform { } final List paths = await _hostApi.pickImages( - _buildSourceSpec(source, preferredCameraDevice), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -138,10 +147,13 @@ class ImagePickerAndroid extends ImagePickerPlatform { Duration? maxDuration, }) async { final List paths = await _hostApi.pickVideos( - _buildSourceSpec(source, preferredCameraDevice), - VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -197,6 +209,21 @@ class ImagePickerAndroid extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + return (await _hostApi.pickMedia( + _mediaOptionsToMediaSelectionOptions(options), + GeneralOptions( + allowMultiple: options.allowMultiple, + usePhotoPicker: useAndroidPhotoPicker, + ), + )) + .map((String? path) => XFile(path!)) + .toList(); + } + @override Future getVideo({ required ImageSource source, @@ -211,6 +238,38 @@ class ImagePickerAndroid extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final ImageSelectionOptions imageSelectionOptions = + _imageOptionsToImageSelectionOptionsWithValidator( + mediaOptions.imageOptions); + return MediaSelectionOptions( + imageSelectionOptions: imageSelectionOptions, + ); + } + + ImageSelectionOptions _imageOptionsToImageSelectionOptionsWithValidator( + ImageOptions? imageOptions) { + final double? maxHeight = imageOptions?.maxHeight; + final double? maxWidth = imageOptions?.maxWidth; + final int? imageQuality = imageOptions?.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + return ImageSelectionOptions( + quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); + } + @override Future retrieveLostData() async { final LostDataResponse result = await getLostData(); @@ -243,7 +302,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { : PlatformException(code: error.code, message: error.message); // Entries are guaranteed not to be null, even though that's not currently - // expressable in Pigeon. + // expressible in Pigeon. final List pickedFileList = result.paths.map((String? path) => XFile(path!)).toList(); @@ -309,9 +368,4 @@ class ImagePickerAndroid extends ImagePickerPlatform { // ignore: dead_code return RetrieveType.image; } - - /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. - /// - /// Currently defaults to false, but the default is subject to change. - bool useAndroidPhotoPicker = false; } diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart index a4f15c847559..476e80db001e 100644 --- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart @@ -26,6 +26,32 @@ enum CacheRetrievalType { video, } +class GeneralOptions { + GeneralOptions({ + required this.allowMultiple, + required this.usePhotoPicker, + }); + + bool allowMultiple; + + bool usePhotoPicker; + + Object encode() { + return [ + allowMultiple, + usePhotoPicker, + ]; + } + + static GeneralOptions decode(Object result) { + result as List; + return GeneralOptions( + allowMultiple: result[0]! as bool, + usePhotoPicker: result[1]! as bool, + ); + } +} + /// Options for image selection and output. class ImageSelectionOptions { ImageSelectionOptions({ @@ -63,6 +89,28 @@ class ImageSelectionOptions { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; + + Object encode() { + return [ + imageSelectionOptions.encode(), + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + imageSelectionOptions: + ImageSelectionOptions.decode(result[0]! as List), + ); + } +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({ @@ -192,15 +240,21 @@ class _ImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -214,10 +268,14 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -242,17 +300,13 @@ class ImagePickerApi { Future> pickImages( SourceSpecification arg_source, ImageSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send([ - arg_source, - arg_options, - arg_allowMultiple, - arg_usePhotoPicker - ]) as List?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -281,17 +335,47 @@ class ImagePickerApi { Future> pickVideos( SourceSpecification arg_source, VideoSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send([ - arg_source, - arg_options, - arg_allowMultiple, - arg_usePhotoPicker - ]) as List?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions, + GeneralOptions arg_generalOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart index 31ff22f1fbbe..9d264b5a11f8 100644 --- a/packages/image_picker/image_picker_android/pigeons/messages.dart +++ b/packages/image_picker/image_picker_android/pigeons/messages.dart @@ -13,6 +13,11 @@ import 'package:pigeon/pigeon.dart'; ), copyrightHeader: 'pigeons/copyright.txt', )) +class GeneralOptions { + GeneralOptions(this.allowMultiple, this.usePhotoPicker); + bool allowMultiple; + bool usePhotoPicker; +} /// Options for image selection and output. class ImageSelectionOptions { @@ -30,6 +35,14 @@ class ImageSelectionOptions { int quality; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({this.maxDurationSeconds}); @@ -89,8 +102,11 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickImages(SourceSpecification source, - ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickImages( + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ); /// Selects video and returns their paths. /// @@ -98,8 +114,21 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickVideos(SourceSpecification source, - VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickVideos( + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + @async + List pickMedia( + MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions, + ); /// Returns results from a previous app session, if any. @TaskQueue(type: TaskQueueType.serialBackgroundThread) diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 1f044b3c50b5..ed7c8dbd5874 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+19 +version: 0.8.7 environment: sdk: ">=2.18.0 <4.0.0" @@ -22,7 +22,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.5.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index f17d078a9031..0b0cab4d6dfb 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -654,6 +654,129 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + const List fakePaths = ['/foo.jgp', 'bar.jpg']; + api.returnValue = fakePaths; + + final List files = await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.lastCall, _LastPickType.image); + expect(files.length, 2); + expect(files[0].path, fakePaths[0]); + expect(files[1].path, fakePaths[1]); + }); + + test('passes default image options', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxWidth: -1.0), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxHeight: -1.0), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: -1), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: 101), + ), + ), + throwsArgumentError, + ); + }); + + test('handles an empty path response gracefully', () async { + api.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, true); + }); + }); + group('#getImageFromSource', () { test('calls the method correctly', () async { const String fakePath = '/foo.jpg'; @@ -807,29 +930,41 @@ class _FakeImagePickerApi implements ImagePickerApi { @override Future> pickImages( - SourceSpecification source, - ImageSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.image; passedSource = source; passedImageOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + return returnValue as List? ?? []; + } + + @override + Future> pickMedia( + MediaSelectionOptions options, + GeneralOptions generalOptions, + ) async { + lastCall = _LastPickType.image; + passedImageOptions = options.imageSelectionOptions; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; return returnValue as List? ?? []; } @override Future> pickVideos( - SourceSpecification source, - VideoSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.video; passedSource = source; passedVideoOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; return returnValue as List? ?? []; } diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart index dbb6b143a91d..d3b68913a68a 100644 --- a/packages/image_picker/image_picker_android/test/test_api.g.dart +++ b/packages/image_picker/image_picker_android/test/test_api.g.dart @@ -23,15 +23,21 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -45,10 +51,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -66,14 +76,21 @@ abstract class TestHostImagePickerApi { /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 Future> pickImages(SourceSpecification source, - ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + ImageSelectionOptions options, GeneralOptions generalOptions); /// Selects video and returns their paths. /// /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 Future> pickVideos(SourceSpecification source, - VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + VideoSelectionOptions options, GeneralOptions generalOptions); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions); /// Returns results from a previous app session, if any. CacheRetrievalResult? retrieveLostResults(); @@ -102,14 +119,12 @@ abstract class TestHostImagePickerApi { (args[1] as ImageSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); - final bool? arg_allowMultiple = (args[2] as bool?); - assert(arg_allowMultiple != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); - final bool? arg_usePhotoPicker = (args[3] as bool?); - assert(arg_usePhotoPicker != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); - final List output = await api.pickImages(arg_source!, - arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); + final List output = await api.pickImages( + arg_source!, arg_options!, arg_generalOptions!); return [output]; }); } @@ -136,14 +151,40 @@ abstract class TestHostImagePickerApi { (args[1] as VideoSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.'); - final bool? arg_allowMultiple = (args[2] as bool?); - assert(arg_allowMultiple != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); - final bool? arg_usePhotoPicker = (args[3] as bool?); - assert(arg_usePhotoPicker != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); - final List output = await api.pickVideos(arg_source!, - arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); + final List output = await api.pickVideos( + arg_source!, arg_options!, arg_generalOptions!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final GeneralOptions? arg_generalOptions = + (args[1] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); + final List output = await api.pickMedia( + arg_mediaSelectionOptions!, arg_generalOptions!); return [output]; }); } diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 100a9b0490f8..8230dd7f130f 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds `getMedia` method. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 2.1.12 diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index 9fe40da2557c..256fe3463b68 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -87,7 +87,8 @@ void main() { )); }); - testWidgets('Can select multiple files', (WidgetTester tester) async { + testWidgets('getMultiImage can select multiple files', + (WidgetTester tester) async { final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); final ImagePickerPluginTestOverrides overrides = @@ -117,6 +118,38 @@ void main() { expect(secondFile.length(), completion(secondTextFile.size)); }); + testWidgets('getMedia can select multiple files', + (WidgetTester tester) async { + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); + + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); + + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final Future> files = + plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(files, completes); + + // And readable + expect((await files).first.readAsBytes(), completion(isNotEmpty)); + + // Peek into the second file... + final XFile secondFile = (await files).elementAt(1); + expect(secondFile.readAsBytes(), completion(isNotEmpty)); + expect(secondFile.name, secondTextFile.name); + expect(secondFile.length(), completion(secondTextFile.size)); + }); + // There's no good way of detecting when the user has "aborted" the selection. testWidgets('computeCaptureAttribute', (WidgetTester tester) async { diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index 9c431bd6e90d..433a1601834a 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter image_picker_for_web: path: ../ - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_driver: diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index bb261f76f320..fb88c96a5942 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart' as mime; import 'src/image_resizer.dart'; @@ -166,7 +167,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { return files.first; } - /// Injects a file input, and returns a list of XFile that the user selected locally. + /// Injects a file input, and returns a list of XFile images that the user selected locally. @override Future> getMultiImage({ double? maxWidth, @@ -189,6 +190,30 @@ class ImagePickerPlugin extends ImagePickerPlatform { return Future.wait(resized); } + /// Injects a file input, and returns a list of XFile media that the user selected locally. + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final List images = await getFiles( + accept: '$_kAcceptImageMimeType,$_kAcceptVideoMimeType', + multiple: options.allowMultiple, + ); + final Iterable> resized = images.map((XFile media) { + if (mime.lookupMimeType(media.path)?.startsWith('image/') ?? false) { + return _imageResizer.resizeImageIfNeeded( + media, + options.imageOptions.maxWidth, + options.imageOptions.maxHeight, + options.imageOptions.imageQuality, + ); + } + return Future.value(media); + }); + + return Future.wait(resized); + } + /// Injects a file input with the specified accept+capture attributes, and /// returns a list of XFile that the user selected locally. /// diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 06a7093f5962..a61a5b838c30 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.12 +version: 2.2.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -21,7 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 1173ddf27b7b..78805ad58139 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,4 +1,9 @@ +## 0.8.8 + +* Adds `getMedia` and `getMultipleMedia` methods. + ## 0.8.7+4 + * Fixes `BuildContext` handling in example. * Updates metadata unit test to work on iOS 16.2. diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist index 423e21fd1672..90eb79e9ad18 100755 --- a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist @@ -53,8 +53,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index ede62336a9c9..cc2262179ef8 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -182,6 +182,32 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } +- (void)testPickMediaShouldUseUIImagePickerControllerOnPreiOS14 { + if (@available(iOS 14, *)) { + return; + } + + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub(ClassMethod([photoLibrary authorizationStatus])) + .andReturn(PHAuthorizationStatusAuthorized); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + OCMVerify(times(1), + [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); +} + - (void)testPickImageWithoutFullMetadata { id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); id photoLibrary = OCMClassMock([PHPhotoLibrary class]); @@ -217,6 +243,28 @@ - (void)testPickMultiImageWithoutFullMetadata { OCMVerify(times(0), [photoLibrary authorizationStatus]); } +- (void)testPickMediaWithoutFullMetadata { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + OCMVerify(times(0), [photoLibrary authorizationStatus]); +} + #pragma mark - Test camera devices, no op on simulators - (void)testPluginPickImageDeviceCancelClickMultipleTimes { @@ -298,6 +346,36 @@ - (void)testPluginMultiImagePathHasItem { [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)testPluginMediaPathHasNoItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, @[]); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:@[]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testPluginMediaPathHasItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + NSArray *pathList = @[ @"test" ]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, pathList); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:pathList]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + - (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) { id mockPickerViewController = OCMClassMock([PHPickerViewController class]); diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index 76076a5dbd65..0f42b58ad2dd 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,14 +39,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -60,18 +61,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -80,13 +73,17 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); @@ -94,18 +91,27 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = - await _picker.getMultiImageWithOptions( - options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ), - ); + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); setState(() { - _imageFileList = pickedFileList; + _mediaFileList = pickedFileList; }); } catch (e) { setState(() { @@ -113,6 +119,31 @@ class _MyHomePageState extends State { }); } }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { @@ -186,22 +217,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -217,8 +254,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -240,8 +288,9 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -253,7 +302,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -269,7 +351,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -282,7 +364,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -295,7 +377,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -428,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index d0bca043d1e4..9d0863537003 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: # 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: ../ - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h index 0016765a0fe0..212f09236b0f 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -16,7 +16,10 @@ NS_ASSUME_NONNULL_BEGIN + (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)); -// Save image with correct meta data and extention copied from the original asset. +// Saves video to temporary URL. Returns nil on failure; ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL; + +// Saves image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m index bf712cdce39a..294bbc77947a 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -20,6 +20,20 @@ + (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(i return fetchResult.firstObject; } ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL { + if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { + return nil; + } + NSString *fileName = [videoURL lastPathComponent]; + NSURL *destination = [NSURL fileURLWithPath:[self temporaryFilePath:fileName]]; + NSError *error; + [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; + if (error) { + return nil; + } + return destination; +} + + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index 5aadecdf9482..c812e35186c6 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -109,7 +109,13 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; config.selectionLimit = context.maxImageCount; - config.filter = [PHPickerFilter imagesFilter]; + if (context.includeVideo) { + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ + [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] + ]]; + } else { + config.filter = [PHPickerFilter imagesFilter]; + } _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; @@ -128,7 +134,12 @@ - (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + if (context.includeVideo) { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ]; + + } else { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + } self.callContext = context; switch (source.type) { @@ -206,6 +217,29 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize } } +- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = [mediaSelectionOptions maxSize]; + context.imageQuality = [mediaSelectionOptions imageQuality]; + context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata]; + context.includeVideo = YES; + if (![[mediaSelectionOptions allowMultiple] boolValue]) { + context.maxImageCount = 1; + } + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; + } +} + - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion: @@ -538,25 +572,16 @@ - (void)imagePickerController:(UIImagePickerController *)picker } if (videoURL != nil) { if (@available(iOS 13.0, *)) { - NSString *fileName = [videoURL lastPathComponent]; - NSURL *destination = - [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - - if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { - NSError *error; - if (![[videoURL path] isEqualToString:[destination path]]) { - [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; - - if (error) { - [self sendCallResultWithError:[FlutterError - errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]]; - return; - } - } - videoURL = destination; + NSURL *destination = [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; + return; } + + videoURL = destination; } [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h index f84921160a31..99d3ef6e195b 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN /** - * The return hander used for all method calls, which internally adapts the provided result list + * The return handler used for all method calls, which internally adapts the provided result list * to return either a list or a single element depending on the original call. */ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterError *_Nullable); @@ -49,6 +49,9 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro /** Whether the image should be picked with full metadata (requires gallery permissions) */ @property(nonatomic, assign) BOOL requestFullMetadata; +/** Whether the picker should include videos in the list*/ +@property(nonatomic, assign) BOOL includeVideo; + @end #pragma mark - diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m index 80e03ddd6578..3476721ae615 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -107,9 +107,15 @@ - (void)start { [self completeOperationWithPath:nil error:flutterError]; } }]; + } else if ([self.result.itemProvider + // This supports uniform types that conform to UTTypeMovie. + // This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG, + // public.3gpp2, public.avi, kUTTypeQuickTimeMovie. + hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) { + [self processVideo]; } else { FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." + message:@"Invalid media source." details:nil]; [self completeOperationWithPath:nil error:flutterError]; } @@ -184,4 +190,41 @@ - (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) { } } +/** + * Processes the video. + */ +- (void)processVideo API_AVAILABLE(ios(14)) { + NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject; + [self.result.itemProvider + loadFileRepresentationForTypeIdentifier:typeIdentifier + completionHandler:^(NSURL *_Nullable videoURL, + NSError *_Nullable error) { + if (error != nil) { + FlutterError *flutterError = + [FlutterError errorWithCode:@"invalid_image" + message:error.localizedDescription + details:error.domain]; + [self completeOperationWithPath:nil error:flutterError]; + return; + } + + NSURL *destination = + [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self + completeOperationWithPath:nil + error:[FlutterError + errorWithCode: + @"flutter_image_picker_copy_" + @"video_error" + message:@"Could not cache " + @"the video file." + details:nil]]; + return; + } + + [self completeOperationWithPath:[destination path] error:nil]; + }]; +} + @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h index cdde03d50550..4e2c4b28c1f6 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { }; @class FLTMaxSize; +@class FLTMediaSelectionOptions; @class FLTSourceSpecification; @interface FLTMaxSize : NSObject @@ -32,6 +33,19 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { @property(nonatomic, strong, nullable) NSNumber *height; @end +@interface FLTMediaSelectionOptions : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple; +@property(nonatomic, strong) FLTMaxSize *maxSize; +@property(nonatomic, strong, nullable) NSNumber *imageQuality; +@property(nonatomic, strong) NSNumber *requestFullMetadata; +@property(nonatomic, strong) NSNumber *allowMultiple; +@end + @interface FLTSourceSpecification : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -57,6 +71,10 @@ NSObject *FLTImagePickerApiGetCodec(void); - (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +/// Selects images and videos and returns their paths. +- (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; @end extern void FLTImagePickerApiSetup(id binaryMessenger, diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m index a1d863639c44..2a24f8367037 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -30,6 +30,12 @@ + (nullable FLTMaxSize *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FLTMediaSelectionOptions () ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list; ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FLTSourceSpecification () + (FLTSourceSpecification *)fromList:(NSArray *)list; + (nullable FLTSourceSpecification *)nullableFromList:(NSArray *)list; @@ -60,6 +66,42 @@ - (NSArray *)toList { } @end +@implementation FLTMediaSelectionOptions ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = maxSize; + pigeonResult.imageQuality = imageQuality; + pigeonResult.requestFullMetadata = requestFullMetadata; + pigeonResult.allowMultiple = allowMultiple; + return pigeonResult; +} ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = [FLTMaxSize nullableFromList:(GetNullableObjectAtIndex(list, 0))]; + NSAssert(pigeonResult.maxSize != nil, @""); + pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1); + pigeonResult.requestFullMetadata = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.requestFullMetadata != nil, @""); + pigeonResult.allowMultiple = GetNullableObjectAtIndex(list, 3); + NSAssert(pigeonResult.allowMultiple != nil, @""); + return pigeonResult; +} ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list { + return (list) ? [FLTMediaSelectionOptions fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.maxSize ? [self.maxSize toList] : [NSNull null]), + (self.imageQuality ?: [NSNull null]), + (self.requestFullMetadata ?: [NSNull null]), + (self.allowMultiple ?: [NSNull null]), + ]; +} +@end + @implementation FLTSourceSpecification + (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; @@ -92,6 +134,8 @@ - (nullable id)readValueOfType:(UInt8)type { case 128: return [FLTMaxSize fromList:[self readValue]]; case 129: + return [FLTMediaSelectionOptions fromList:[self readValue]]; + case 130: return [FLTSourceSpecification fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -106,9 +150,12 @@ - (void)writeValue:(id)value { if ([value isKindOfClass:[FLTMaxSize class]]) { [self writeByte:128]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + } else if ([value isKindOfClass:[FLTMediaSelectionOptions class]]) { [self writeByte:129]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -220,4 +267,28 @@ void FLTImagePickerApiSetup(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Selects images and videos and returns their paths. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMedia" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMediaWithMediaSelectionOptions:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMediaWithMediaSelectionOptions:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMediaSelectionOptions *arg_mediaSelectionOptions = GetNullableObjectAtIndex(args, 0); + [api pickMediaWithMediaSelectionOptions:arg_mediaSelectionOptions + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index 3f76784ff07c..02105f95e5a1 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -175,6 +175,51 @@ class ImagePickerIOS extends ImagePickerPlatform { ); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final MediaSelectionOptions mediaSelectionOptions = + _mediaOptionsToMediaSelectionOptions(options); + + return (await _hostApi.pickMedia(mediaSelectionOptions)) + .map((String? path) => XFile(path!)) + .toList(); + } + + MaxSize _imageOptionsToMaxSizeWithValidation(ImageOptions imageOptions) { + final double? maxHeight = imageOptions.maxHeight; + final double? maxWidth = imageOptions.maxWidth; + final int? imageQuality = imageOptions.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return MaxSize(width: maxWidth, height: maxHeight); + } + + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final MaxSize maxSize = + _imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions); + return MediaSelectionOptions( + maxSize: maxSize, + imageQuality: mediaOptions.imageOptions.imageQuality, + requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata, + allowMultiple: mediaOptions.allowMultiple, + ); + } + @override Future pickVideo({ required ImageSource source, diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart index 87596b78ebf6..91dde827a60e 100644 --- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -47,6 +47,42 @@ class MaxSize { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + + int? imageQuality; + + bool requestFullMetadata; + + bool allowMultiple; + + Object encode() { + return [ + maxSize.encode(), + imageQuality, + requestFullMetadata, + allowMultiple, + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + maxSize: MaxSize.decode(result[0]! as List), + imageQuality: result[1] as int?, + requestFullMetadata: result[2]! as bool, + allowMultiple: result[3]! as bool, + ); + } +} + class SourceSpecification { SourceSpecification({ required this.type, @@ -80,9 +116,12 @@ class _ImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -94,6 +133,8 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -184,4 +225,33 @@ class ImagePickerApi { return (replyList[0] as String?); } } + + /// Selects images and videos and returns their paths. + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } } diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index d04841b0fde9..fb69a6d13349 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -20,6 +20,20 @@ class MaxSize { double? height; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + int? imageQuality; + bool requestFullMetadata; + bool allowMultiple; +} + // Corresponds to `CameraDevice` from the platform interface package. enum SourceCamera { rear, front } @@ -45,4 +59,9 @@ abstract class ImagePickerApi { @async @ObjCSelector('pickVideoWithSource:maxDuration:') String? pickVideo(SourceSpecification source, int? maxDurationSeconds); + + /// Selects images and videos and returns their paths. + @async + @ObjCSelector('pickMediaWithMediaSelectionOptions:') + List pickMedia(MediaSelectionOptions mediaSelectionOptions); } diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 90d6dff73d60..a9246891458c 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+4 +version: 0.8.8 environment: sdk: ">=2.18.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index 2c9d52509f26..da74e31f0a33 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -71,6 +71,19 @@ class _ApiLogger implements TestHostImagePickerApi { return returnValue as List?; } + @override + Future> pickMedia( + MediaSelectionOptions mediaSelectionOptions) async { + calls.add(_LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': mediaSelectionOptions.maxSize.width, + 'maxHeight': mediaSelectionOptions.maxSize.height, + 'imageQuality': mediaSelectionOptions.imageQuality, + 'requestFullMetadata': mediaSelectionOptions.requestFullMetadata, + 'allowMultiple': mediaSelectionOptions.allowMultiple, + })); + return returnValue as List; + } + @override Future pickVideo( SourceSpecification source, int? maxDurationSeconds) async { @@ -878,6 +891,227 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes request metadata argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(requestFullMetadata: false), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes allowMultiple argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: false, + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': false + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxWidth: -1.0), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxHeight: -1.0), + )), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: -1), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: 101), + )), + throwsArgumentError, + ); + }); + + test('handles a empty path response gracefully', () async { + log.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart index 4ac619590f93..6da0400b1a07 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.g.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -20,9 +20,12 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -34,6 +37,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -55,6 +60,9 @@ abstract class TestHostImagePickerApi { Future pickVideo( SourceSpecification source, int? maxDurationSeconds); + /// Selects images and videos and returns their paths. + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions); + static void setup(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -140,5 +148,29 @@ abstract class TestHostImagePickerApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final List output = + await api.pickMedia(arg_mediaSelectionOptions!); + return [output]; + }); + } + } } } diff --git a/packages/image_picker/image_picker_linux/AUTHORS b/packages/image_picker/image_picker_linux/AUTHORS new file mode 100644 index 000000000000..26e81c7fb254 --- /dev/null +++ b/packages/image_picker/image_picker_linux/AUTHORS @@ -0,0 +1,7 @@ +# 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 + +Google Inc. +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md new file mode 100644 index 000000000000..9f14cc71ced6 --- /dev/null +++ b/packages/image_picker/image_picker_linux/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + +## 0.2.0 + +* Implements initial Linux support. diff --git a/script/tool/LICENSE b/packages/image_picker/image_picker_linux/LICENSE similarity index 100% rename from script/tool/LICENSE rename to packages/image_picker/image_picker_linux/LICENSE diff --git a/packages/image_picker/image_picker_linux/README.md b/packages/image_picker/image_picker_linux/README.md new file mode 100644 index 000000000000..1f1833e81e62 --- /dev/null +++ b/packages/image_picker/image_picker_linux/README.md @@ -0,0 +1,27 @@ +# image\_picker\_linux + +A Linux implementation of [`image_picker`][1]. + +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + +### pickImage() +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. + +### pickVideo() +The argument `maxDuration` is not currently supported. + +## Usage + +### Import the package + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_linux/example/README.md b/packages/image_picker/image_picker_linux/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart new file mode 100644 index 000000000000..8f4887095c13 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/lib/main.dart @@ -0,0 +1,516 @@ +// 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: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, this.title}); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _mediaFileList; + + // This must be called from within a setState() callback + void _setImageFileListFromFile(XFile? value) { + _mediaFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (context.mounted) { + if (_isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); + } else { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_mediaFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + return Semantics( + label: 'image_picker_example_picked_image', + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), + ); + }, + itemCount: _mediaFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {super.key}); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_linux/example/linux/.gitignore b/packages/image_picker/image_picker_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt b/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..1fbfa72731c0 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flutter.plugins.imagePickerExample") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt b/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake b/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker_linux/example/linux/main.cc b/packages/image_picker/image_picker_linux/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/main.cc @@ -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. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/image_picker/image_picker_linux/example/linux/my_application.cc b/packages/image_picker/image_picker_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..3a67810f5612 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/my_application.cc @@ -0,0 +1,111 @@ +// 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 "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/image_picker/image_picker_linux/example/linux/my_application.h b/packages/image_picker/image_picker_linux/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/my_application.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml new file mode 100644 index 000000000000..76e8f25ac106 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: example +description: Example for image_picker_linux implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + image_picker_linux: + # When depending on this package from a real application you should use: + # image_picker_linux: ^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: .. + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart new file mode 100644 index 000000000000..72596ea931f2 --- /dev/null +++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart @@ -0,0 +1,180 @@ +// 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:file_selector_linux/file_selector_linux.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The Linux implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// Linux. +class ImagePickerLinux extends CameraDelegatingImagePickerPlatform { + /// Constructs a platform implementation. + ImagePickerLinux(); + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static FileSelectorPlatform fileSelector = FileSelectorLinux(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerLinux(); + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', mimeTypes: ['image/*']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', mimeTypes: ['video/*']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', mimeTypes: ['image/*']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', extensions: ['image/*', 'video/*']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } +} diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml new file mode 100644 index 000000000000..9698991e336e --- /dev/null +++ b/packages/image_picker/image_picker_linux/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_linux +description: Linux platform implementation of image_picker +repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.2.1 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: image_picker + platforms: + linux: + dartPluginClass: ImagePickerLinux + +dependencies: + file_selector_linux: ^0.9.1+3 + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.8.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: 5.4.1 diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart new file mode 100644 index 000000000000..004bfcc4dc85 --- /dev/null +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart @@ -0,0 +1,180 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_linux/image_picker_linux.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_linux_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Returns the captured type groups from a mock call result, assuming that + // exactly one call was made and only the type groups were captured. + List capturedTypeGroups(VerificationResult result) { + return result.captured.single as List; + } + + late ImagePickerLinux plugin; + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + plugin = ImagePickerLinux(); + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerLinux.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerLinux.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImageFromSource passes the accepted type groups correctly', + () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImageFromSource calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getImageFromSource(source: ImageSource.camera))!.path, + fakePath); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', + () async { + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + }); + + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['video/*']); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['video/*']); + }); + + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getVideo(source: ImageSource.camera))!.path, fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', + () async { + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + }); + }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['image/*', 'video/*']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); +} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart new file mode 100644 index 000000000000..6cde8261f501 --- /dev/null +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart @@ -0,0 +1,120 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in image_picker_linux/test/image_picker_linux_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFile, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future<_i2.XFile?>.value(), + ) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFiles, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value(<_i2.XFile>[]), + ) as _i3.Future>); + @override + _i3.Future getSavePath({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getSavePath, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPath, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPaths, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); +} diff --git a/packages/image_picker/image_picker_macos/AUTHORS b/packages/image_picker/image_picker_macos/AUTHORS new file mode 100644 index 000000000000..26e81c7fb254 --- /dev/null +++ b/packages/image_picker/image_picker_macos/AUTHORS @@ -0,0 +1,7 @@ +# 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 + +Google Inc. +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md new file mode 100644 index 000000000000..bd79a8674cd8 --- /dev/null +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + +## 0.2.0 + +* Implements initial macOS support. diff --git a/packages/image_picker/image_picker_macos/LICENSE b/packages/image_picker/image_picker_macos/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_macos/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/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md new file mode 100644 index 000000000000..ec76d85e26be --- /dev/null +++ b/packages/image_picker/image_picker_macos/README.md @@ -0,0 +1,38 @@ +# image\_picker\_macos + +A macOS implementation of [`image_picker`][1]. + +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + +### pickImage() +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. + +### pickVideo() +The argument `maxDuration` is not currently supported. + +## Usage + +### Import the package + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +### Entitlements + +This package is currently implemented using [`file_selector`][3], so you will +need to add a read-only file acces [entitlement][4]: +```xml + com.apple.security.files.user-selected.read-only + +``` + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/file_selector +[4]: https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox diff --git a/packages/image_picker/image_picker_macos/example/README.md b/packages/image_picker/image_picker_macos/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart new file mode 100644 index 000000000000..8f4887095c13 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -0,0 +1,516 @@ +// 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: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, this.title}); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _mediaFileList; + + // This must be called from within a setState() callback + void _setImageFileListFromFile(XFile? value) { + _mediaFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (context.mounted) { + if (_isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); + } else { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_mediaFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + return Semantics( + label: 'image_picker_example_picked_image', + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), + ); + }, + itemCount: _mediaFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {super.key}); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_macos/example/macos/.gitignore b/packages/image_picker/image_picker_macos/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Podfile b/packages/image_picker/image_picker_macos/example/macos/Podfile new file mode 100644 index 000000000000..049abe295427 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# 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', 'ephemeral', '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 Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..d9333e4704c4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,573 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..fb7259e17785 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift b/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// 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 Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..82b6f9d9a33e Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..13b35eba55c6 Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..0a3f5fa40fb3 Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bdb57226d5f2 Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..f083318e09ca Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..326c0e72c9d8 Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..2f1632cfddf3 Binary files /dev/null and b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..e0c85ac3c1f7 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 The Flutter Authors. All rights reserved. diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements b/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..0ceee8dff196 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-only + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist b/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift @@ -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 Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements b/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..18aff0ce43c2 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml new file mode 100644 index 000000000000..785a2afb227b --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: example +description: Example for image_picker_macos implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + image_picker_macos: + # When depending on this package from a real application you should use: + # image_picker_macos: ^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: .. + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart new file mode 100644 index 000000000000..9e9447a5710c --- /dev/null +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -0,0 +1,186 @@ +// 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:file_selector_macos/file_selector_macos.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The macOS implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// macOS. +class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { + /// Constructs a platform implementation. + ImagePickerMacOS(); + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static FileSelectorPlatform fileSelector = FileSelectorMacOS(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerMacOS(); + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + // TODO(stuartmorgan): Add a native implementation that can use + // PHPickerViewController on macOS 13+, with this as a fallback for + // older OS versions: https://github.com/flutter/flutter/issues/125829. + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.image']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.movie']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + // TODO(stuartmorgan): Add a native implementation that can use + // PHPickerViewController on macOS 13+, with this as a fallback for + // older OS versions: https://github.com/flutter/flutter/issues/125829. + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.image']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: ['public.image', 'public.movie']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } +} diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml new file mode 100644 index 000000000000..9ace885e666f --- /dev/null +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_macos +description: macOS platform implementation of image_picker +repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.2.1 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: image_picker + platforms: + macos: + dartPluginClass: ImagePickerMacOS + +dependencies: + file_selector_macos: ^0.9.1+1 + file_selector_platform_interface: ^2.3.0 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.8.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: 5.4.1 diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart new file mode 100644 index 000000000000..7e94161d4a40 --- /dev/null +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -0,0 +1,186 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_macos/image_picker_macos.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_macos_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Returns the captured type groups from a mock call result, assuming that + // exactly one call was made and only the type groups were captured. + List capturedTypeGroups(VerificationResult result) { + return result.captured.single as List; + } + + late ImagePickerMacOS plugin; + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + plugin = ImagePickerMacOS(); + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerMacOS.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerMacOS.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImageFromSource passes the accepted type groups correctly', + () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImageFromSource calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getImageFromSource(source: ImageSource.camera))!.path, + fakePath); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', + () async { + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + }); + + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.movie']); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.movie']); + }); + + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getVideo(source: ImageSource.camera))!.path, fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', + () async { + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + }); + }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['public.image', 'public.movie']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); +} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart new file mode 100644 index 000000000000..5b8769c27aad --- /dev/null +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart @@ -0,0 +1,120 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in image_picker_macos/test/image_picker_macos_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFile, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future<_i2.XFile?>.value(), + ) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFiles, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value(<_i2.XFile>[]), + ) as _i3.Future>); + @override + _i3.Future getSavePath({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getSavePath, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPath, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPaths, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); +} diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 0e50cd22ecfc..bcba89bd3974 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.8.0 + +* Adds `getMedia` method. + +## 2.7.0 + +* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform + implementations that don't support `ImageSource.camera`, but allow for an- + implementation to be provided at the application level via implementation + of `CameraDelegatingImagePickerPlatform`. +* Adds `supportsImageSource` to check source support at runtime. + ## 2.6.4 * Adds compatibility with `http` 1.0. @@ -32,7 +44,7 @@ * Adds `requestFullMetadata` option that allows disabling extra permission requests on certain platforms. * Moves optional image picking parameters to `ImagePickerOptions` class. -* Minor fixes for new analysis options. +* Minor fixes for new analysis options. ## 2.4.4 diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index c2c39f93fe18..b21fd29a8d2d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -252,6 +252,30 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final ImageOptions imageOptions = options.imageOptions; + + final Map args = { + 'maxImageWidth': imageOptions.maxWidth, + 'maxImageHeight': imageOptions.maxHeight, + 'imageQuality': imageOptions.imageQuality, + 'allowMultiple': options.allowMultiple, + }; + + final List? paths = await _channel + .invokeMethod?>( + 'pickMedia', + args, + ) + .then((List? paths) => + paths?.map((dynamic path) => XFile(path as String)).toList()); + + return paths ?? []; + } + @override Future getVideo({ required ImageSource source, @@ -280,13 +304,21 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); + assert( + type == kTypeImage || type == kTypeVideo || type == kTypeMedia, + ); RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; + switch (type) { + case kTypeImage: + retrieveType = RetrieveType.image; + break; + case kTypeVideo: + retrieveType = RetrieveType.video; + break; + case kTypeMedia: + retrieveType = RetrieveType.media; + break; } PlatformException? exception; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index c8942cd2da0e..66c5d3b57854 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -32,8 +32,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [ImagePickerPlatform] when they register themselves. - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 static set instance(ImagePickerPlatform instance) { PlatformInterface.verify(instance, _token); _instance = instance; @@ -215,6 +213,24 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getMultiImage() has not been implemented.'); } + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the gallery. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// In Android, the MainActivity can be destroyed for various reasons. + /// If that happens, the result will be lost in this call. You can then + /// call [getLostData] when your app relaunches to retrieve the lost data. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> getMedia({ + required MediaOptions options, + }) { + throw UnimplementedError('getMedia() has not been implemented.'); + } + /// Returns a [XFile] containing the video that was picked. /// /// The [source] argument controls where the video comes from. This can @@ -305,4 +321,75 @@ abstract class ImagePickerPlatform extends PlatformInterface { ); return pickedImages ?? []; } + + /// Returns true if the implementation supports [source]. + /// + /// Defaults to true for the original image sources, `gallery` and `camera`, + /// for backwards compatibility. + bool supportsImageSource(ImageSource source) { + return source == ImageSource.gallery || source == ImageSource.camera; + } +} + +/// A base class for an [ImagePickerPlatform] implementation that does not +/// directly support [ImageSource.camera], but supports delegating to a +/// provided [ImagePickerCameraDelegate]. +abstract class CameraDelegatingImagePickerPlatform extends ImagePickerPlatform { + /// A delegate to respond to calls that use [ImageSource.camera]. + /// + /// When it is null, attempting to use [ImageSource.camera] will throw a + /// [StateError]. + ImagePickerCameraDelegate? cameraDelegate; + + @override + bool supportsImageSource(ImageSource source) { + if (source == ImageSource.camera) { + return cameraDelegate != null; + } + return super.supportsImageSource(source); + } + + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takePhoto( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: options.preferredCameraDevice, + )); + } + return super.getImageFromSource(source: source, options: options); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takeVideo( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: preferredCameraDevice, + maxVideoDuration: maxDuration)); + } + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart new file mode 100644 index 000000000000..39584c923b03 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart @@ -0,0 +1,53 @@ +// 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:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart' show immutable; + +import 'camera_device.dart'; + +/// Options for [ImagePickerCameraDelegate] methods. +/// +/// New options may be added in the future. +@immutable +class ImagePickerCameraDelegateOptions { + /// Creates a new set of options for taking an image or video. + const ImagePickerCameraDelegateOptions({ + this.preferredCameraDevice = CameraDevice.rear, + this.maxVideoDuration, + }); + + /// The camera device to default to, if available. + /// + /// Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; + + /// The maximum duration to allow when recording a video. + /// + /// Defaults to null, meaning no maximum duration. + final Duration? maxVideoDuration; +} + +/// A delegate for `ImagePickerPlatform` implementations that do not provide +/// a camera implementation, or that have a default but allow substituting an +/// alternate implementation. +abstract class ImagePickerCameraDelegate { + /// Takes a photo with the given [options] and returns an [XFile] to the + /// resulting image file. + /// + /// Returns null if the photo could not be taken, or the user cancelled. + Future takePhoto({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); + + /// Records a video with the given [options] and returns an [XFile] to the + /// resulting video file. + /// + /// Returns null if the video could not be recorded, or the user cancelled. + Future takeVideo({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart index 2cc01c92da1d..374ff27063fa 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart @@ -2,6 +2,40 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'types.dart'; + +/// Specifies options for picking a single image from the device's camera or gallery. +/// +/// This class inheritance is a byproduct of the api changing over time. +/// It exists solely to avoid breaking changes. +class ImagePickerOptions extends ImageOptions { + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + const ImagePickerOptions({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super(); + + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + ImagePickerOptions.createAndValidate({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super.createAndValidate(); + + /// Used to specify the camera to use when the `source` is [ImageSource.camera]. + /// + /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not + /// supported on the device. Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; +} + /// Specifies image-specific options for picking. class ImageOptions { /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] @@ -13,6 +47,18 @@ class ImageOptions { this.requestFullMetadata = true, }); + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] + /// and [requestFullMetadata]. Throws if options are not valid. + ImageOptions.createAndValidate({ + this.maxHeight, + this.maxWidth, + this.imageQuality, + this.requestFullMetadata = true, + }) { + _validateOptions( + maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality); + } + /// The maximum width of the image, in pixels. /// /// If null, the image will only be resized if [maxHeight] is specified. @@ -38,4 +84,19 @@ class ImageOptions { // // Defaults to true. final bool requestFullMetadata; + + /// Validates that all values are within required ranges. Throws if not. + static void _validateOptions( + {double? maxWidth, final double? maxHeight, int? imageQuality}) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart deleted file mode 100644 index 0d85c918f649..000000000000 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart +++ /dev/null @@ -1,50 +0,0 @@ -// 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 'types.dart'; - -/// Specifies options for picking a single image from the device's camera or gallery. -class ImagePickerOptions { - /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], - /// [referredCameraDevice] and [requestFullMetadata]. - const ImagePickerOptions({ - this.maxHeight, - this.maxWidth, - this.imageQuality, - this.preferredCameraDevice = CameraDevice.rear, - this.requestFullMetadata = true, - }); - - /// The maximum width of the image, in pixels. - /// - /// If null, the image will only be resized if [maxHeight] is specified. - final double? maxWidth; - - /// The maximum height of the image, in pixels. - /// - /// If null, the image will only be resized if [maxWidth] is specified. - final double? maxHeight; - - /// Modifies the quality of the image, ranging from 0-100 where 100 is the - /// original/max quality. - /// - /// Compression is only supported for certain image types such as JPEG. If - /// compression is not supported for the image that is picked, a warning - /// message will be logged. - /// - /// If null, the image will be returned with the original quality. - final int? imageQuality; - - /// Used to specify the camera to use when the `source` is [ImageSource.camera]. - /// - /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not - /// supported on the device. Defaults to [CameraDevice.rear]. - final CameraDevice preferredCameraDevice; - - /// If true, requests full image metadata, which may require extra permissions - /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS). - // - // Defaults to true. - final bool requestFullMetadata; -} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 10af812a3109..0f802f19719f 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -36,7 +36,8 @@ class LostDataResponse { /// An empty response should have [file], [exception] and [type] to be null. bool get isEmpty => _empty; - /// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed. + /// The file that was lost in a previous [getImage], [getMultiImage], + /// [getVideo] or [getMedia] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. final XFile? file; @@ -51,7 +52,7 @@ class LostDataResponse { /// Note that it is not the exception that caused the destruction of the MainActivity. final PlatformException? exception; - /// Can either be [RetrieveType.image] or [RetrieveType.video]; + /// Can either be [RetrieveType.image], [RetrieveType.video], or [RetrieveType.media]. /// /// If the lost data is empty, this will be null. final RetrieveType? type; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart new file mode 100644 index 000000000000..70a048f7147d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart @@ -0,0 +1,23 @@ +// 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 '../../image_picker_platform_interface.dart'; + +/// Specifies options for selecting items when using [ImagePickerPlatform.getMedia]. +@immutable +class MediaOptions { + /// Construct a new MediaOptions instance. + const MediaOptions({ + this.imageOptions = const ImageOptions(), + required this.allowMultiple, + }); + + /// Options that will apply to images upon selection. + final ImageOptions imageOptions; + + /// Whether to allow for selecting multiple media. + final bool allowMultiple; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart new file mode 100644 index 000000000000..cd0113497ea1 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart @@ -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. + +import '../../image_picker_platform_interface.dart'; + +/// The type of media to allow the user to select with [ImagePickerPlatform.getMedia]. +enum MediaSelectionType { + /// Static pictures. + image, + + /// Videos. + video, +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart index 445445e5d7fb..94fed59f238d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart @@ -8,5 +8,8 @@ enum RetrieveType { image, /// A video. See [ImagePicker.pickVideo]. - video + video, + + /// Either a video or a static picture. See [ImagePicker.pickMedia]. + media, } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index fbe12e8e825a..0339d98b575e 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'camera_delegate.dart'; export 'camera_device.dart'; export 'image_options.dart'; -export 'image_picker_options.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; +export 'media_options.dart'; +export 'media_selection_type.dart'; export 'multi_image_picker_options.dart'; export 'picked_file/picked_file.dart'; export 'retrieve_type.dart'; @@ -16,3 +18,6 @@ const String kTypeImage = 'image'; /// Denotes that a video is being picked. const String kTypeVideo = 'video'; + +/// Denotes that either a video or image is being picked. +const String kTypeMedia = 'media'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 9c8a55ad96ad..67a5070f6c78 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.6.4 +version: 2.8.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart new file mode 100644 index 000000000000..89dc1ae382da --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart @@ -0,0 +1,104 @@ +// 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_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +void main() { + group('ImagePickerPlatform', () { + test('supportsImageSource defaults to true for original values', () async { + final ImagePickerPlatform implementation = FakeImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + expect(implementation.supportsImageSource(ImageSource.gallery), true); + }); + }); + + group('CameraDelegatingImagePickerPlatform', () { + test( + 'supportsImageSource returns false for camera when there is no delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), false); + }); + + test('supportsImageSource returns true for camera when there is a delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = FakeCameraDelegate(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + }); + + test('getImageFromSource for camera throws if delegate is not set', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater( + implementation.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getVideo for camera throws if delegate is not set', () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater(implementation.getVideo(source: ImageSource.camera), + throwsStateError); + }); + + test('getImageFromSource for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect( + (await implementation.getImageFromSource(source: ImageSource.camera))! + .path, + fakePath); + }); + + test('getVideo for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect((await implementation.getVideo(source: ImageSource.camera))!.path, + fakePath); + }); + }); +} + +class FakeImagePickerPlatform extends ImagePickerPlatform {} + +class FakeCameraDelegatingImagePickerPlatform + extends CameraDelegatingImagePickerPlatform {} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart similarity index 91% rename from packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart rename to packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart index 244af3982672..cf92c2cfa145 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart @@ -872,6 +872,152 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + ], + ); + }); + + test('passes the selection options correctly', () async { + // Default options + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + // Various image options + returnValue = ['0']; + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 70, + ), + ), + ); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': 10.0, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': 10.0, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': 70, + 'allowMultiple': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: -1.0, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: -1.0, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: -1, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 101, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('handles a null path response gracefully', () async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); diff --git a/packages/image_picker/image_picker_windows/AUTHORS b/packages/image_picker/image_picker_windows/AUTHORS index 5db3d584e6bc..26e81c7fb254 100644 --- a/packages/image_picker/image_picker_windows/AUTHORS +++ b/packages/image_picker/image_picker_windows/AUTHORS @@ -4,4 +4,4 @@ # Name/Organization Google Inc. -Alexandre Zollinger Chohfi \ No newline at end of file +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 9c6267cbc0cf..bd881d1fc3b5 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,4 +1,8 @@ -## NEXT +## 0.2.1 + +* Adds `getMedia` method. + +## 0.2.0 * Updates minimum Flutter version to 3.3. diff --git a/packages/image_picker/image_picker_windows/README.md b/packages/image_picker/image_picker_windows/README.md index 0336723884ca..1aa30b17fc0b 100644 --- a/packages/image_picker/image_picker_windows/README.md +++ b/packages/image_picker/image_picker_windows/README.md @@ -2,19 +2,26 @@ A Windows implementation of [`image_picker`][1]. +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + ### pickImage() -The arguments `source`, `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` are not supported on Windows. +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. ### pickVideo() -The arguments `source`, `preferredCameraDevice`, and `maxDuration` are not supported on Windows. +The argument `maxDuration` is not currently supported. ## Usage ### Import the package -This package is not yet [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), -which means you need to [add `image_picker_windows` as a dependency](https://pub.dev/packages/image_picker_windows/install) -in addition to `image_picker`. +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. -Once you do, you can use the `image_picker` APIs as you normally would, other -than the limitations noted above. +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index b9ee929977db..8f4887095c13 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback - void _setImageFileListFromFile(PickedFile? value) { - _imageFileList = value == null ? null : [value]; + void _setImageFileListFromFile(XFile? value) { + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -56,7 +57,7 @@ class _MyHomePageState extends State { final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(PickedFile? file) async { + Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); final VideoPlayerController controller = @@ -70,62 +71,98 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.pickMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final PickedFile? pickedFile = await _picker.pickImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { if (_isVideo) { - final PickedFile? file = await _picker.pickVideo( + final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -178,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -205,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -228,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -246,10 +305,11 @@ class _MyHomePageState extends State { ImageSource.gallery, context: context, isMultiImage: true, + isMedia: true, ); }, - heroTag: 'image1', - tooltip: 'Pick Multiple Image from gallery', + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', child: const Icon(Icons.photo_library), ), ), @@ -258,39 +318,73 @@ class _MyHomePageState extends State { child: FloatingActionButton( onPressed: () { _isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); }, - heroTag: 'image2', - tooltip: 'Take a Photo', - child: const Icon(Icons.camera_alt), + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), ), ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( - backgroundColor: Colors.red, onPressed: () { - _isVideo = true; - _onImageButtonPressed(ImageSource.gallery, context: context); + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); }, - heroTag: 'video0', - tooltip: 'Pick Video from gallery', - child: const Icon(Icons.video_library), + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), ), ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { _isVideo = true; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed(ImageSource.gallery, context: context); }, - heroTag: 'video1', - tooltip: 'Take a Video', - child: const Icon(Icons.videocam), + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), ), ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), ], ), ); @@ -416,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index 3f13f771185a..6515d507696a 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.4.3 + image_picker_platform_interface: ^2.8.0 image_picker_windows: # When depending on this package from a real application you should use: # image_picker_windows: ^x.y.z @@ -18,6 +18,7 @@ dependencies: # 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: .. + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart index 90e86bf486b4..e9e414628c93 100644 --- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -13,7 +13,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. /// /// This class implements the `package:image_picker` functionality for /// Windows. -class ImagePickerWindows extends ImagePickerPlatform { +class ImagePickerWindows extends CameraDelegatingImagePickerPlatform { /// Constructs a ImagePickerWindows. ImagePickerWindows(); @@ -53,11 +53,8 @@ class ImagePickerWindows extends ImagePickerPlatform { ImagePickerPlatform.instance = ImagePickerWindows(); } - // `maxWidth`, `maxHeight`, `imageQuality` and `preferredCameraDevice` - // arguments are not supported on Windows. If any of these arguments - // is supplied, it'll be silently ignored by the Windows version of - // the plugin. `source` is not implemented for `ImageSource.camera` - // and will throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. @override Future pickImage({ required ImageSource source, @@ -66,23 +63,21 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - final XFile? file = await getImage( + final XFile? file = await getImageFromSource( source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice); + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); if (file != null) { return PickedFile(file.path); } return null; } - // `preferredCameraDevice` and `maxDuration` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. - // `source` is not implemented for `ImageSource.camera` and will - // throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. @override Future pickVideo({ required ImageSource source, @@ -99,11 +94,8 @@ class ImagePickerWindows extends ImagePickerPlatform { return null; } - // `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` - // arguments are not supported on Windows. If any of these arguments - // is supplied, it'll be silently ignored by the Windows version - // of the plugin. `source` is not implemented for `ImageSource.camera` - // and will throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. @override Future getImage({ required ImageSource source, @@ -112,46 +104,73 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - if (source != ImageSource.gallery) { - // TODO(azchohfi): Support ImageSource.camera. - // See https://github.com/flutter/flutter/issues/102115 - throw UnimplementedError( - 'ImageSource.gallery is currently the only supported source on Windows'); + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', extensions: imageFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; } - const XTypeGroup typeGroup = - XTypeGroup(label: 'images', extensions: imageFormats); - final XFile? file = await fileSelector - .openFile(acceptedTypeGroups: [typeGroup]); - return file; + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); } - // `preferredCameraDevice` and `maxDuration` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. - // `source` is not implemented for `ImageSource.camera` and will - // throw an exception. + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. @override Future getVideo({ required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - if (source != ImageSource.gallery) { - // TODO(azchohfi): Support ImageSource.camera. - // See https://github.com/flutter/flutter/issues/102115 - throw UnimplementedError( - 'ImageSource.gallery is currently the only supported source on Windows'); + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', extensions: videoFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; } - const XTypeGroup typeGroup = - XTypeGroup(label: 'videos', extensions: videoFormats); - final XFile? file = await fileSelector - .openFile(acceptedTypeGroups: [typeGroup]); - return file; + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); } - // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. @override Future> getMultiImage({ double? maxWidth, @@ -159,9 +178,33 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, }) async { const XTypeGroup typeGroup = - XTypeGroup(label: 'images', extensions: imageFormats); + XTypeGroup(label: 'Images', extensions: imageFormats); final List files = await fileSelector .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not + // supported on Windows. If any of these arguments is supplied, + // they will be silently ignored by the Windows version of the plugin. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: [...imageFormats, ...videoFormats]); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 5984efd3536a..e16ecbda993b 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.1.0+6 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -17,10 +17,10 @@ flutter: dependencies: file_selector_platform_interface: ^2.2.0 - file_selector_windows: ^0.8.2 + file_selector_windows: ^0.9.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.4.3 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart index f8adde4051c7..6da0873af5b4 100644 --- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -21,11 +21,12 @@ void main() { return result.captured.single as List; } - group('$ImagePickerWindows()', () { - final ImagePickerWindows plugin = ImagePickerWindows(); + group('ImagePickerWindows', () { + late ImagePickerWindows plugin; late MockFileSelectorPlatform mockFileSelectorPlatform; setUp(() { + plugin = ImagePickerWindows(); mockFileSelectorPlatform = MockFileSelectorPlatform(); when(mockFileSelectorPlatform.openFile( @@ -55,12 +56,6 @@ void main() { ImagePickerWindows.imageFormats); }); - test('pickImage throws UnimplementedError when source is camera', - () async { - expect(() async => plugin.pickImage(source: ImageSource.camera), - throwsA(isA())); - }); - test('getImage passes the accepted type groups correctly', () async { await plugin.getImage(source: ImageSource.gallery); @@ -71,10 +66,21 @@ void main() { ImagePickerWindows.imageFormats); }); - test('getImage throws UnimplementedError when source is camera', + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ImagePickerWindows.imageFormats); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', () async { - expect(() async => plugin.getImage(source: ImageSource.camera), - throwsA(isA())); + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); }); test('getMultiImage passes the accepted type groups correctly', () async { @@ -87,6 +93,7 @@ void main() { ImagePickerWindows.imageFormats); }); }); + group('videos', () { test('pickVideo passes the accepted type groups correctly', () async { await plugin.pickVideo(source: ImageSource.gallery); @@ -98,12 +105,6 @@ void main() { ImagePickerWindows.videoFormats); }); - test('pickVideo throws UnimplementedError when source is camera', - () async { - expect(() async => plugin.pickVideo(source: ImageSource.camera), - throwsA(isA())); - }); - test('getVideo passes the accepted type groups correctly', () async { await plugin.getVideo(source: ImageSource.gallery); @@ -114,11 +115,73 @@ void main() { ImagePickerWindows.videoFormats); }); - test('getVideo throws UnimplementedError when source is camera', + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect((await plugin.getVideo(source: ImageSource.camera))!.path, + fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', () async { - expect(() async => plugin.getVideo(source: ImageSource.camera), - throwsA(isA())); + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + }); + }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, [ + ...ImagePickerWindows.imageFormats, + ...ImagePickerWindows.videoFormats + ]); + }); + + test('multiple media handles an empty path response gracefully', + () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); }); }); }); } + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist index 3c493732947a..5304b6ed1f84 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 34af66f18b83..0ed82c7e10fc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.0+6 + +* Bumps org.jetbrains.kotlin:kotlin-bom from 1.8.21 to 1.8.22. + +## 0.3.0+5 + +* Bumps org.jetbrains.kotlin:kotlin-bom from 1.8.0 to 1.8.21. + ## 0.3.0+4 * Fixes unawaited_futures violations. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index a07a53a77d8f..81d69a40cce2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.6.0' // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) implementation 'com.android.billingclient:billing:5.2.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20230227' diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index 39ae8f75ca23..86cd8d8841bb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -145,7 +145,7 @@ class _MyAppState extends State<_MyApp> { _buildConnectionCheckTile(), _buildProductList(), _buildConsumableBox(), - _FeatureCard(), + const _FeatureCard(), ], ), ); @@ -444,9 +444,9 @@ class _MyAppState extends State<_MyApp> { } class _FeatureCard extends StatelessWidget { - _FeatureCard(); + const _FeatureCard(); - final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchaseAndroidPlatformAddition get addition => InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index c294ad968980..d8ac5f6d38c2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.0+4 +version: 0.3.0+6 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist index 3c493732947a..5304b6ed1f84 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/ios_platform_images/example/ios/Runner/Info.plist b/packages/ios_platform_images/example/ios/Runner/Info.plist index bebb28ae7cf0..c454edd1f86e 100644 --- a/packages/ios_platform_images/example/ios/Runner/Info.plist +++ b/packages/ios_platform_images/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index b542eda207af..fb370ef1afb0 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,7 +1,8 @@ ## NEXT -* Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 2.1.6 diff --git a/packages/local_auth/local_auth/example/ios/Runner/Info.plist b/packages/local_auth/local_auth/example/ios/Runner/Info.plist index 1af663b3f83c..2dc92f5dff1c 100644 --- a/packages/local_auth/local_auth/example/ios/Runner/Info.plist +++ b/packages/local_auth/local_auth/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - NSFaceIDUsageDescription App needs to authenticate using faces. CADisableMinimumFrameDurationOnPhone diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index 8a99ee56b485..f8e08863b564 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -183,11 +183,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -198,11 +196,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index 53c5b88dbc06..7b5fb2322eff 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 89ec28846fd2..f5812afa5ff5 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.6 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 614d21d1bb4e..ed2b24d16c74 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.32 + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. +* Updates androidx.fragment version to 1.6.0. + ## 1.0.31 * Updates androidx.fragment version to 1.5.7. diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 0d7223191644..b55df15f3c9b 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -61,7 +61,7 @@ android { dependencies { api "androidx.core:core:1.10.1" api "androidx.biometric:biometric:1.1.0" - api "androidx.fragment:fragment:1.5.7" + api "androidx.fragment:fragment:1.6.0" testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.10.3' diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart index db30c01cd4f1..378ada594aa1 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -188,11 +188,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -203,11 +201,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index d5801103e818..e0be107cc926 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_android plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 0a7063683a8c..4c492fd3c6cf 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,11 +2,11 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.31 +version: 1.0.32 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index 2d47acf2f013..867297555d87 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 1.1.3 * Migrates internal implementation to Pigeon. diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist index 1af663b3f83c..2dc92f5dff1c 100644 --- a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - NSFaceIDUsageDescription App needs to authenticate using faces. CADisableMinimumFrameDurationOnPhone diff --git a/packages/local_auth/local_auth_ios/example/lib/main.dart b/packages/local_auth/local_auth_ios/example/lib/main.dart index b7acc99f9d7d..3e857bd88ef8 100644 --- a/packages/local_auth/local_auth_ios/example/lib/main.dart +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -187,11 +187,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -202,11 +200,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml index 300f2ff967ea..74d524743b6c 100644 --- a/packages/local_auth/local_auth_ios/example/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_ios plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 000a82e26838..cfdfc0b4e5a4 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.1.3 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index d184e5509a72..ed9de2a1025c 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT -* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 1.0.8 diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart index e2e7747d0e59..3f4ee19d6864 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -150,11 +150,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -165,11 +163,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 2b31020e0753..30d4ec2ae373 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_windows plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index f0b97d3a1609..c40d11f9db0c 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.8 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/palette_generator/example/ios/Runner/Info.plist b/packages/palette_generator/example/ios/Runner/Info.plist index cdf4f69014e5..fb29f390fe3b 100644 --- a/packages/palette_generator/example/ios/Runner/Info.plist +++ b/packages/palette_generator/example/ios/Runner/Info.plist @@ -39,7 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - diff --git a/packages/path_provider/path_provider/example/ios/Runner/Info.plist b/packages/path_provider/path_provider/example/ios/Runner/Info.plist index 150b4d3f2dc7..122243319e2b 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/Info.plist +++ b/packages/path_provider/path_provider/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist index 5bdb9bcc0635..45ccb93dc4d5 100644 --- a/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/example/app/ios/Runner/Info.plist b/packages/pigeon/example/app/ios/Runner/Info.plist index 6bca58717f45..b6439ae077fb 100644 --- a/packages/pigeon/example/app/ios/Runner/Info.plist +++ b/packages/pigeon/example/app/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist index 5520fc125c4d..08ea2be3c97b 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist index 192997d10b4d..ccd141f08045 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/tool/run_tests.dart b/packages/pigeon/tool/run_tests.dart index 250e18c4cef7..6cc96f2b2069 100644 --- a/packages/pigeon/tool/run_tests.dart +++ b/packages/pigeon/tool/run_tests.dart @@ -119,21 +119,14 @@ Future main(List args) async { // androidJavaIntegrationTests, // androidKotlinIntegrationTests, ]; - // Run macOS and iOS tests on macOS, since that's the only place they can run. - // TODO(stuartmorgan): Move everything to LUCI, and eliminate the LUCI/Cirrus - // separation. See https://github.com/flutter/flutter/issues/120231. - const List macOSHostLuciTests = [ + const List macOSHostTests = [ iOSObjCUnitTests, - // TODO(stuartmorgan): Enable by default once CI issues are solved; see - // https://github.com/flutter/packages/pull/2816. - //iOSObjCIntegrationTests, + iOSObjCIntegrationTests, // Currently these are testing exactly the same thing as // macOSSwiftIntegrationTests, so we don't need to run both by default. This // should be enabled if any iOS-only tests are added (e.g., for a feature // not supported by macOS). // iOSSwiftIntegrationTests, - ]; - const List macOSHostCirrusTests = [ iOSSwiftUnitTests, macOSSwiftUnitTests, macOSSwiftIntegrationTests, @@ -146,8 +139,7 @@ Future main(List args) async { _validateTestCoverage(>[ linuxHostTests, - macOSHostLuciTests, - macOSHostCirrusTests, + macOSHostTests, windowsHostTests, // Tests that are deliberately not included in CI: [ @@ -177,11 +169,7 @@ Future main(List args) async { final List testsToRun; if (Platform.isMacOS) { - if (Platform.environment['LUCI_CI'] != null) { - testsToRun = macOSHostLuciTests; - } else { - testsToRun = macOSHostCirrusTests; - } + testsToRun = macOSHostTests; } else if (Platform.isWindows) { testsToRun = windowsHostTests; } else if (Platform.isLinux) { diff --git a/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist index 2128c14bb939..f572a5d0dfda 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index 2128c14bb939..f572a5d0dfda 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 2a6e3a4e12bd..347db107d360 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 1.0.9 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/rfw/example/hello/ios/Runner/Info.plist b/packages/rfw/example/hello/ios/Runner/Info.plist index 2f6dda1e835f..8d1d37dff8d6 100644 --- a/packages/rfw/example/hello/ios/Runner/Info.plist +++ b/packages/rfw/example/hello/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/local/ios/Runner/Info.plist b/packages/rfw/example/local/ios/Runner/Info.plist index 031ce64cd1e8..675f7bfd9240 100644 --- a/packages/rfw/example/local/ios/Runner/Info.plist +++ b/packages/rfw/example/local/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/remote/ios/Runner/Info.plist b/packages/rfw/example/remote/ios/Runner/Info.plist index c11cb1e95dc8..6b4c897e78ca 100644 --- a/packages/rfw/example/remote/ios/Runner/Info.plist +++ b/packages/rfw/example/remote/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/remote/lib/main.dart b/packages/rfw/example/remote/lib/main.dart index b6d409c8389e..f0b5092511f3 100644 --- a/packages/rfw/example/remote/lib/main.dart +++ b/packages/rfw/example/remote/lib/main.dart @@ -87,21 +87,13 @@ class _ExampleState extends State { }, ); } else { - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - result = Material( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + result = const Material( child: SafeArea( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Padding( - padding: const EdgeInsets.all(20.0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + padding: EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: const [ + children: [ Padding(padding: EdgeInsets.only(right: 100.0), child: Text('REMOTE', textAlign: TextAlign.center, style: TextStyle(letterSpacing: 12.0))), Expanded(child: DecoratedBox(decoration: FlutterLogoDecoration(style: FlutterLogoStyle.horizontal))), Padding(padding: EdgeInsets.only(left: 100.0), child: Text('WIDGETS', textAlign: TextAlign.center, style: TextStyle(letterSpacing: 12.0))), diff --git a/packages/rfw/example/remote/pubspec.yaml b/packages/rfw/example/remote/pubspec.yaml index 0b744cc987a3..a801d881cc82 100644 --- a/packages/rfw/example/remote/pubspec.yaml +++ b/packages/rfw/example/remote/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/rfw/lib/src/flutter/core_widgets.dart b/packages/rfw/lib/src/flutter/core_widgets.dart index 0ff1b1226979..99a9f7a04c5b 100644 --- a/packages/rfw/lib/src/flutter/core_widgets.dart +++ b/packages/rfw/lib/src/flutter/core_widgets.dart @@ -644,7 +644,7 @@ Map get _coreWidgetsDefinitions => (['softWrap']), overflow: ArgumentDecoders.enumValue(TextOverflow.values, source, ['overflow']), - textScaleFactor: source.v(['textScaleFactor']), + textScaleFactor: source.v(['textScaleFactor']), // ignore: deprecated_member_use maxLines: source.v(['maxLines']), semanticsLabel: source.v(['semanticsLabel']), textWidthBasis: ArgumentDecoders.enumValue(TextWidthBasis.values, source, ['textWidthBasis']), diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index 5db6474642d1..c15b97011cc2 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.9 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 3ee0cabefd7a..af254c9aa780 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 2.1.2 +* Fixes singleton initialization race condition introduced during NNBD + transition. * Updates minimum supported macOS version to 10.14. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist index 99a4c9290737..12a4ff084cbc 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 142f422077c8..6164f1bb2423 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -72,11 +72,12 @@ class SharedPreferences { if (_completer == null) { final Completer completer = Completer(); + _completer = completer; try { final Map preferencesMap = await _getSharedPreferencesMap(); completer.complete(SharedPreferences._(preferencesMap)); - } on Exception catch (e) { + } catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. completer.completeError(e); @@ -84,7 +85,6 @@ class SharedPreferences { _completer = null; return sharedPrefsFuture; } - _completer = completer; } return _completer!.future; } diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index bcc2a14128a5..91a14d14b082 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index 0ee9f88bddb6..0cdfaea5e4b5 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -11,199 +11,196 @@ import 'package:shared_preferences_platform_interface/types.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('SharedPreferences', () { - const String testString = 'hello world'; - const bool testBool = true; - const int testInt = 42; - const double testDouble = 3.14159; - const List testList = ['foo', 'bar']; - const Map testValues = { - 'flutter.String': testString, - 'flutter.bool': testBool, - 'flutter.int': testInt, - 'flutter.double': testDouble, - 'flutter.List': testList, - }; - - const String testString2 = 'goodbye world'; - const bool testBool2 = false; - const int testInt2 = 1337; - const double testDouble2 = 2.71828; - const List testList2 = ['baz', 'quox']; - const Map testValues2 = { - 'flutter.String': testString2, - 'flutter.bool': testBool2, - 'flutter.int': testInt2, - 'flutter.double': testDouble2, - 'flutter.List': testList2, - }; - - late FakeSharedPreferencesStore store; - late SharedPreferences preferences; - - setUp(() async { - store = FakeSharedPreferencesStore(testValues); - SharedPreferencesStorePlatform.instance = store; - preferences = await SharedPreferences.getInstance(); - store.log.clear(); - }); + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + const Map testValues = { + 'flutter.String': testString, + 'flutter.bool': testBool, + 'flutter.int': testInt, + 'flutter.double': testDouble, + 'flutter.List': testList, + }; + + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; + const Map testValues2 = { + 'flutter.String': testString2, + 'flutter.bool': testBool2, + 'flutter.int': testInt2, + 'flutter.double': testDouble2, + 'flutter.List': testList2, + }; + + late FakeSharedPreferencesStore store; + late SharedPreferences preferences; + + setUp(() async { + store = FakeSharedPreferencesStore(testValues); + SharedPreferencesStorePlatform.instance = store; + preferences = await SharedPreferences.getInstance(); + store.log.clear(); + }); - test('reading', () async { - expect(preferences.get('String'), testString); - expect(preferences.get('bool'), testBool); - expect(preferences.get('int'), testInt); - expect(preferences.get('double'), testDouble); - expect(preferences.get('List'), testList); - expect(preferences.getString('String'), testString); - expect(preferences.getBool('bool'), testBool); - expect(preferences.getInt('int'), testInt); - expect(preferences.getDouble('double'), testDouble); - expect(preferences.getStringList('List'), testList); - expect(store.log, []); - }); + test('reading', () async { + expect(preferences.get('String'), testString); + expect(preferences.get('bool'), testBool); + expect(preferences.get('int'), testInt); + expect(preferences.get('double'), testDouble); + expect(preferences.get('List'), testList); + expect(preferences.getString('String'), testString); + expect(preferences.getBool('bool'), testBool); + expect(preferences.getInt('int'), testInt); + expect(preferences.getDouble('double'), testDouble); + expect(preferences.getStringList('List'), testList); + expect(store.log, []); + }); - test('writing', () async { - await Future.wait(>[ - preferences.setString('String', testString2), - preferences.setBool('bool', testBool2), - preferences.setInt('int', testInt2), - preferences.setDouble('double', testDouble2), - preferences.setStringList('List', testList2) - ]); - expect( + test('writing', () async { + await Future.wait(>[ + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) + ]); + expect( + store.log, + [ + isMethodCall('setValue', arguments: [ + 'String', + 'flutter.String', + testString2, + ]), + isMethodCall('setValue', arguments: [ + 'Bool', + 'flutter.bool', + testBool2, + ]), + isMethodCall('setValue', arguments: [ + 'Int', + 'flutter.int', + testInt2, + ]), + isMethodCall('setValue', arguments: [ + 'Double', + 'flutter.double', + testDouble2, + ]), + isMethodCall('setValue', arguments: [ + 'StringList', + 'flutter.List', + testList2, + ]), + ], + ); + store.log.clear(); + + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); + expect(store.log, equals([])); + }); + + test('removing', () async { + const String key = 'testKey'; + await preferences.remove(key); + expect( store.log, - [ - isMethodCall('setValue', arguments: [ - 'String', - 'flutter.String', - testString2, - ]), - isMethodCall('setValue', arguments: [ - 'Bool', - 'flutter.bool', - testBool2, - ]), - isMethodCall('setValue', arguments: [ - 'Int', - 'flutter.int', - testInt2, - ]), - isMethodCall('setValue', arguments: [ - 'Double', - 'flutter.double', - testDouble2, - ]), - isMethodCall('setValue', arguments: [ - 'StringList', - 'flutter.List', - testList2, - ]), - ], - ); - store.log.clear(); - - expect(preferences.getString('String'), testString2); - expect(preferences.getBool('bool'), testBool2); - expect(preferences.getInt('int'), testInt2); - expect(preferences.getDouble('double'), testDouble2); - expect(preferences.getStringList('List'), testList2); - expect(store.log, equals([])); - }); + List.filled( + 1, + isMethodCall( + 'remove', + arguments: 'flutter.$key', + ), + growable: true, + )); + }); - test('removing', () async { - const String key = 'testKey'; - await preferences.remove(key); - expect( - store.log, - List.filled( - 1, - isMethodCall( - 'remove', - arguments: 'flutter.$key', - ), - growable: true, - )); - }); + test('containsKey', () async { + const String key = 'testKey'; - test('containsKey', () async { - const String key = 'testKey'; + expect(false, preferences.containsKey(key)); - expect(false, preferences.containsKey(key)); + await preferences.setString(key, 'test'); + expect(true, preferences.containsKey(key)); + }); - await preferences.setString(key, 'test'); - expect(true, preferences.containsKey(key)); - }); + test('clearing', () async { + await preferences.clear(); + expect(preferences.getString('String'), null); + expect(preferences.getBool('bool'), null); + expect(preferences.getInt('int'), null); + expect(preferences.getDouble('double'), null); + expect(preferences.getStringList('List'), null); + expect(store.log, [isMethodCall('clear', arguments: null)]); + }); - test('clearing', () async { - await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); - expect(store.log, [isMethodCall('clear', arguments: null)]); - }); + test('reloading', () async { + await preferences.setString('String', testString); + expect(preferences.getString('String'), testString); - test('reloading', () async { - await preferences.setString('String', testString); - expect(preferences.getString('String'), testString); + SharedPreferences.setMockInitialValues(testValues2.cast()); + expect(preferences.getString('String'), testString); - SharedPreferences.setMockInitialValues( - testValues2.cast()); - expect(preferences.getString('String'), testString); + await preferences.reload(); + expect(preferences.getString('String'), testString2); + }); - await preferences.reload(); - expect(preferences.getString('String'), testString2); - }); + test('back to back calls should return same instance.', () async { + final Future first = SharedPreferences.getInstance(); + final Future second = SharedPreferences.getInstance(); + expect(await first, await second); + }); - test('back to back calls should return same instance.', () async { - final Future first = SharedPreferences.getInstance(); - final Future second = SharedPreferences.getInstance(); - expect(await first, await second); + test('string list type is dynamic (usually from method channel)', () async { + SharedPreferences.setMockInitialValues({ + 'dynamic_list': ['1', '2'] }); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List? value = prefs.getStringList('dynamic_list'); + expect(value, ['1', '2']); + }); + + group('mocking', () { + const String key = 'dummy'; + const String prefixedKey = 'flutter.$key'; - test('string list type is dynamic (usually from method channel)', () async { - SharedPreferences.setMockInitialValues({ - 'dynamic_list': ['1', '2'] - }); + test('test 1', () async { + SharedPreferences.setMockInitialValues( + {prefixedKey: 'my string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final List? value = prefs.getStringList('dynamic_list'); - expect(value, ['1', '2']); + final String? value = prefs.getString(key); + expect(value, 'my string'); }); - group('mocking', () { - const String key = 'dummy'; - const String prefixedKey = 'flutter.$key'; - - test('test 1', () async { - SharedPreferences.setMockInitialValues( - {prefixedKey: 'my string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(key); - expect(value, 'my string'); - }); - - test('test 2', () async { - SharedPreferences.setMockInitialValues( - {prefixedKey: 'my other string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(key); - expect(value, 'my other string'); - }); + test('test 2', () async { + SharedPreferences.setMockInitialValues( + {prefixedKey: 'my other string'}); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String? value = prefs.getString(key); + expect(value, 'my other string'); }); + }); - test('writing copy of strings list', () async { - final List myList = []; - await preferences.setStringList('myList', myList); - myList.add('foobar'); + test('writing copy of strings list', () async { + final List myList = []; + await preferences.setStringList('myList', myList); + myList.add('foobar'); - final List cachedList = preferences.getStringList('myList')!; - expect(cachedList, []); + final List cachedList = preferences.getStringList('myList')!; + expect(cachedList, []); - cachedList.add('foobar2'); + cachedList.add('foobar2'); - expect(preferences.getStringList('myList'), []); - }); + expect(preferences.getStringList('myList'), []); }); test('calling mock initial values with non-prefixed keys succeeds', () async { @@ -215,6 +212,16 @@ void main() { expect(value, 'foo'); }); + test('getInstance always returns the same instance', () async { + SharedPreferencesStorePlatform.instance = SlowInitSharedPreferencesStore(); + + final Future firstFuture = + SharedPreferences.getInstance(); + final Future secondFuture = + SharedPreferences.getInstance(); + expect(identical(await firstFuture, await secondFuture), true); + }); + test('calling setPrefix after getInstance throws', () async { const String newPrefix = 'newPrefix'; @@ -399,6 +406,15 @@ class UnimplementedSharedPreferencesStore } } +class SlowInitSharedPreferencesStore + extends UnimplementedSharedPreferencesStore { + @override + Future> getAll() async { + await Future.delayed(const Duration(seconds: 1)); + return {}; + } +} + class ThrowingSharedPreferencesStore extends SharedPreferencesStorePlatform { @override Future clear() { diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist index 30d5f4b0e845..6d2946cb3ea5 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index ebb56fde9496..7360ed630027 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,6 +1,8 @@ ## NEXT * Updates minimum supported macOS version to 10.14. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 6.1.11 diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist index 7d28adf648b2..be02cc9d4bf2 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist +++ b/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/url_launcher/url_launcher/example/lib/encoding.dart b/packages/url_launcher/url_launcher/example/lib/encoding.dart index 575eb5f42387..0875a8587269 100644 --- a/packages/url_launcher/url_launcher/example/lib/encoding.dart +++ b/packages/url_launcher/url_launcher/example/lib/encoding.dart @@ -22,17 +22,11 @@ String? encodeQueryParameters(Map params) { // #enddocregion encode-query-parameters void main() => runApp( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - MaterialApp( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + const MaterialApp( home: Material( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: const [ + children: [ ElevatedButton( onPressed: _composeMail, child: Text('Compose an email'), diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index e6550de37841..28c8cc5c1c1f 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 029ad6603d25..6cf68a7210a9 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 6.1.11 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart index b2fde31d526d..e35312831755 100644 --- a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -7,6 +7,7 @@ import 'dart:ui' show Brightness; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher/src/legacy_api.dart'; @@ -241,15 +242,18 @@ void main() { _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - binding.renderView.automaticSystemUiAdjustment = true; + // TODO(goderbauer): Migrate to binding.renderViews when that is available in the oldest supported stable. + final RenderView renderView = + binding.renderView; // ignore: deprecated_member_use + renderView.automaticSystemUiAdjustment = true; final Future launchResult = launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); // Should take over control of the automaticSystemUiAdjustment while it's // pending, then restore it back to normal after the launch finishes. - expect(binding.renderView.automaticSystemUiAdjustment, isFalse); + expect(renderView.automaticSystemUiAdjustment, isFalse); await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, isTrue); + expect(renderView.automaticSystemUiAdjustment, isTrue); }); test('sets automaticSystemUiAdjustment to not be null', () async { @@ -270,15 +274,18 @@ void main() { _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.android; - expect(binding.renderView.automaticSystemUiAdjustment, true); + // TODO(goderbauer): Migrate to binding.renderViews when that is available in the oldest supported stable. + final RenderView renderView = + binding.renderView; // ignore: deprecated_member_use + expect(renderView.automaticSystemUiAdjustment, true); final Future launchResult = launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); // The automaticSystemUiAdjustment should be set before the launch // and equal to true after the launch result is complete. - expect(binding.renderView.automaticSystemUiAdjustment, true); + expect(renderView.automaticSystemUiAdjustment, true); await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, true); + expect(renderView.automaticSystemUiAdjustment, true); }); test('open non-parseable url', () async { diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist index 7d28adf648b2..be02cc9d4bf2 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index fa03c093e91e..fb5e36bec5c5 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.1.3 * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. +* Removes deprecated API calls. ## 2.1.2 diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart index aacd55af39ea..a82c16ed3f10 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -74,8 +74,6 @@ abstract class LinkInfo { bool get isDisabled; } -typedef _SendMessage = Function(String, ByteData?, void Function(ByteData?)); - /// Pushes the [routeName] into Flutter's navigation system via a platform /// message. /// @@ -91,11 +89,7 @@ Future pushRouteNameToFramework(Object? _, String routeName) { // https://github.com/flutter/flutter/issues/124045. // ignore: deprecated_member_use SystemNavigator.routeInformationUpdated(location: routeName); - final _SendMessage sendMessage = _ambiguate(WidgetsBinding.instance) - ?.platformDispatcher - .onPlatformMessage ?? - ui.channelBuffers.push; - sendMessage( + ui.channelBuffers.push( 'flutter/navigation', _codec.encodeMethodCall( MethodCall('pushRouteInformation', { @@ -107,9 +101,3 @@ Future pushRouteNameToFramework(Object? _, String routeName) { ); return completer.future; } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index f8270a15f984..eb897385c0cc 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/video_player/video_player/example/ios/Runner/Info.plist b/packages/video_player/video_player/example/ios/Runner/Info.plist index 74d07293aa9e..4e29652e6d2e 100644 --- a/packages/video_player/video_player/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player/example/ios/Runner/Info.plist @@ -48,8 +48,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index ef4f0bfa2104..bef0298c80c1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -350,12 +350,12 @@ void main() { test('file with special characters', () async { final VideoPlayerController controller = - VideoPlayerController.file(File('A #1 Hit?.avi')); + VideoPlayerController.file(File('A #1 Hit.avi')); await controller.initialize(); final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/A%20%231%20Hit%3F.avi'), true, + expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist index 74d07293aa9e..4e29652e6d2e 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist @@ -48,8 +48,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist index 6ee44fd0e2fd..6e0d80c22b91 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist @@ -39,11 +39,14 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS index 22e2b0ef78fc..b664f363f743 100644 --- a/packages/webview_flutter/webview_flutter_android/AUTHORS +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -66,4 +66,5 @@ Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom Nick Bradshaw +Kai Yu diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 3160b8594f6e..adc5034f11df 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.8.0 + +* Adds support for handling geolocation permissions. See + `AndroidWebViewController.setGeolocationPermissionsPromptCallbacks`. + ## 3.7.1 * Removes obsolete null checks on non-nullable values. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 4dc43fa1d9db..cc52b0b10888 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -2589,6 +2589,33 @@ public void onPermissionRequest( new ArrayList(Arrays.asList(instanceIdArg, requestInstanceIdArg)), channelReply -> callback.reply(null)); } + /** Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. */ + public void onGeolocationPermissionsShowPrompt( + @NonNull Long instanceIdArg, + @NonNull Long paramsInstanceIdArg, + @NonNull String originArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, paramsInstanceIdArg, originArg)), + channelReply -> callback.reply(null)); + } + /** Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. */ + public void onGeolocationPermissionsHidePrompt( + @NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { @@ -2839,4 +2866,106 @@ public void create( channelReply -> callback.reply(null)); } } + /** + * Host API for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to + * a Dart instance or handle method calls on the associated native class or an instance of the + * class. + * + *

See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface GeolocationPermissionsCallbackHostApi { + /** Handles Dart method `GeolocationPermissionsCallback.invoke`. */ + void invoke( + @NonNull Long instanceId, + @NonNull String origin, + @NonNull Boolean allow, + @NonNull Boolean retain); + + /** The codec used by GeolocationPermissionsCallbackHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `GeolocationPermissionsCallbackHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, + @Nullable GeolocationPermissionsCallbackHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + String originArg = (String) args.get(1); + Boolean allowArg = (Boolean) args.get(2); + Boolean retainArg = (Boolean) args.get(3); + try { + api.invoke( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + originArg, + allowArg, + retainArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** + * Flutter API for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding Dart instances that are attached to a native + * instance or receiving callback methods from an overridden native class. + * + *

See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class GeolocationPermissionsCallbackFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public GeolocationPermissionsCallbackFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by GeolocationPermissionsCallbackFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Create a new Dart instance and add it to the `InstanceManager`. */ + public void create(@NonNull Long instanceIdArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(instanceIdArg)), + channelReply -> callback.reply(null)); + } + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java new file mode 100644 index 000000000000..32c66c0d9f31 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java @@ -0,0 +1,62 @@ +// 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.webviewflutter; + +import android.webkit.GeolocationPermissions; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackFlutterApi; + +/** + * Flutter API implementation for `GeolocationPermissionsCallback`. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class GeolocationPermissionsCallbackFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private GeolocationPermissionsCallbackFlutterApi api; + + /** + * Constructs a {@link GeolocationPermissionsCallbackFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public GeolocationPermissionsCallbackFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new GeolocationPermissionsCallbackFlutterApi(binaryMessenger); + } + + /** + * Stores the `GeolocationPermissionsCallback` instance and notifies Dart to create and store a + * new `GeolocationPermissionsCallback` instance that is attached to this one. If `instance` has + * already been added, this method does nothing. + */ + public void create( + @NonNull GeolocationPermissions.Callback instance, + @NonNull GeolocationPermissionsCallbackFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create(instanceManager.addHostCreatedInstance(instance), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull GeolocationPermissionsCallbackFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java new file mode 100644 index 000000000000..58981b6e3703 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java @@ -0,0 +1,52 @@ +// 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.webviewflutter; + +import android.webkit.GeolocationPermissions; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackHostApi; +import java.util.Objects; + +/** + * Host API implementation for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class GeolocationPermissionsCallbackHostApiImpl + implements GeolocationPermissionsCallbackHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + + /** + * Constructs a {@link GeolocationPermissionsCallbackHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public GeolocationPermissionsCallbackHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void invoke( + @NonNull Long instanceId, + @NonNull String origin, + @NonNull Boolean allow, + @NonNull Boolean retain) { + getGeolocationPermissionsCallbackInstance(instanceId).invoke(origin, allow, retain); + } + + private GeolocationPermissions.Callback getGeolocationPermissionsCallbackInstance( + @NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index fab34fc212d7..ad5168fa110a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.os.Build; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; import android.webkit.WebView; @@ -72,6 +73,32 @@ public void onShowFileChooser( callback); } + /** Passes arguments from {@link WebChromeClient#onGeolocationPermissionsShowPrompt} to Dart. */ + public void onGeolocationPermissionsShowPrompt( + @NonNull WebChromeClient webChromeClient, + @NonNull String origin, + @NonNull GeolocationPermissions.Callback callback, + @NonNull WebChromeClientFlutterApi.Reply replyCallback) { + new GeolocationPermissionsCallbackFlutterApiImpl(binaryMessenger, instanceManager) + .create(callback, reply -> {}); + onGeolocationPermissionsShowPrompt( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(callback)), + origin, + replyCallback); + } + + /** + * Sends a message to Dart to call `WebChromeClient.onGeolocationPermissionsHidePrompt` on the + * Dart object representing `instance`. + */ + public void onGeolocationPermissionsHidePrompt( + @NonNull WebChromeClient instance, @NonNull WebChromeClientFlutterApi.Reply callback) { + super.onGeolocationPermissionsHidePrompt( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + callback); + } + /** * Sends a message to Dart to call `WebChromeClient.onPermissionRequest` on the Dart object * representing `instance`. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 38ebcb8932b8..74ea45e5359a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; @@ -51,6 +52,17 @@ public void onProgressChanged(@NonNull WebView view, int progress) { flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); } + @Override + public void onGeolocationPermissionsShowPrompt( + @NonNull String origin, @NonNull GeolocationPermissions.Callback callback) { + flutterApi.onGeolocationPermissionsShowPrompt(this, origin, callback, reply -> {}); + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + flutterApi.onGeolocationPermissionsHidePrompt(this, reply -> {}); + } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @SuppressWarnings("LambdaLast") @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 79640b90b0b4..305821943e3c 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -17,6 +17,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.InstanceManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaObjectHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; @@ -137,6 +138,9 @@ private void setUp( PermissionRequestHostApi.setup( binaryMessenger, new PermissionRequestHostApiImpl(binaryMessenger, instanceManager)); } + GeolocationPermissionsCallbackHostApi.setup( + binaryMessenger, + new GeolocationPermissionsCallbackHostApiImpl(binaryMessenger, instanceManager)); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java new file mode 100644 index 000000000000..14f8e15bf864 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java @@ -0,0 +1,74 @@ +// 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.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.GeolocationPermissions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class GeolocationPermissionsCallbackTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public GeolocationPermissions.Callback mockGeolocationPermissionsCallback; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock public GeolocationPermissionsCallbackFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void invoke() { + final String origin = "testString"; + final boolean allow = true; + final boolean retain = true; + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockGeolocationPermissionsCallback, instanceIdentifier); + + final GeolocationPermissionsCallbackHostApiImpl hostApi = + new GeolocationPermissionsCallbackHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.invoke(instanceIdentifier, origin, allow, retain); + + verify(mockGeolocationPermissionsCallback).invoke(origin, allow, retain); + } + + @Test + public void flutterApiCreate() { + final GeolocationPermissionsCallbackFlutterApiImpl flutterApi = + new GeolocationPermissionsCallbackFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockGeolocationPermissionsCallback, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockGeolocationPermissionsCallback)); + verify(mockFlutterApi).create(eq(instanceIdentifier), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 9a97cd6f37a6..e2d5a444b716 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Message; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebResourceRequest; import android.webkit.WebView; @@ -120,9 +121,24 @@ public void onCreateWindow() { public void onPermissionRequest() { final PermissionRequest mockRequest = mock(PermissionRequest.class); instanceManager.addDartCreatedInstance(mockRequest, 10); - webChromeClient.onPermissionRequest(mockRequest); - verify(mockFlutterApi).onPermissionRequest(eq(webChromeClient), eq(mockRequest), any()); } + + @Test + public void onGeolocationPermissionsShowPrompt() { + final GeolocationPermissions.Callback mockCallback = + mock(GeolocationPermissions.Callback.class); + webChromeClient.onGeolocationPermissionsShowPrompt("https://flutter.dev", mockCallback); + + verify(mockFlutterApi) + .onGeolocationPermissionsShowPrompt( + eq(webChromeClient), eq("https://flutter.dev"), eq(mockCallback), any()); + } + + @Test + public void onGeolocationPermissionsHidePrompt() { + webChromeClient.onGeolocationPermissionsHidePrompt(); + verify(mockFlutterApi).onGeolocationPermissionsHidePrompt(eq(webChromeClient), any()); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index 6cbb932532aa..e95de8cfa6f1 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -28,18 +28,22 @@ class AndroidWebViewProxy { final android_webview.WebView Function() createAndroidWebView; /// Constructs a [android_webview.WebChromeClient]. - final android_webview.WebChromeClient Function({ - void Function(android_webview.WebView webView, int progress)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - }) createAndroidWebChromeClient; + final android_webview.WebChromeClient Function( + {void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt}) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. final android_webview.WebViewClient Function({ diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 85d0ea009f73..85ec6b902c57 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -59,6 +59,57 @@ class JavaObject with Copyable { } } +/// A callback interface used by the host application to set the Geolocation +/// permission state for an origin. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@immutable +class GeolocationPermissionsCallback extends JavaObject { + /// Instantiates a [GeolocationPermissionsCallback] without creating and + /// attaching to an instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy. + @protected + GeolocationPermissionsCallback.detached({ + super.binaryMessenger, + super.instanceManager, + }) : _geolocationPermissionsCallbackApi = + GeolocationPermissionsCallbackHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final GeolocationPermissionsCallbackHostApiImpl + _geolocationPermissionsCallbackApi; + + /// Sets the Geolocation permission state for the supplied origin. + /// + /// [origin]: The origin for which permissions are set. + /// + /// [allow]: Whether or not the origin should be allowed to use the Geolocation API. + /// + /// [retain]: Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + Future invoke(String origin, bool allow, bool retain) { + return _geolocationPermissionsCallbackApi.invokeFromInstances( + this, + origin, + allow, + retain, + ); + } + + @override + GeolocationPermissionsCallback copy() { + return GeolocationPermissionsCallback.detached( + binaryMessenger: _geolocationPermissionsCallbackApi.binaryMessenger, + instanceManager: _geolocationPermissionsCallbackApi.instanceManager, + ); + } +} + /// An Android View that displays web pages. /// /// **Basic usage** @@ -962,6 +1013,17 @@ class DownloadListener extends JavaObject { } } +/// Responsible for request the Geolocation API. +typedef GeolocationPermissionsShowPrompt = Future Function( + String origin, + GeolocationPermissionsCallback callback, +); + +/// Responsible for request the Geolocation API is Cancel. +typedef GeolocationPermissionsHidePrompt = void Function( + WebChromeClient instance, +); + /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. @@ -969,6 +1031,8 @@ class WebChromeClient extends JavaObject { this.onProgressChanged, this.onShowFileChooser, this.onPermissionRequest, + this.onGeolocationPermissionsShowPrompt, + this.onGeolocationPermissionsHidePrompt, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -986,6 +1050,8 @@ class WebChromeClient extends JavaObject { this.onProgressChanged, this.onShowFileChooser, this.onPermissionRequest, + this.onGeolocationPermissionsShowPrompt, + this.onGeolocationPermissionsHidePrompt, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -1020,6 +1086,16 @@ class WebChromeClient extends JavaObject { PermissionRequest request, )? onPermissionRequest; + /// Indicates the client should handle geolocation permissions. + final GeolocationPermissionsShowPrompt? onGeolocationPermissionsShowPrompt; + + /// Notify the host application that a request for Geolocation permissions, + /// made with a previous call to [onGeolocationPermissionsShowPrompt] has been + /// canceled. + final void Function( + WebChromeClient instance, + )? onGeolocationPermissionsHidePrompt; + /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. /// @@ -1054,6 +1130,8 @@ class WebChromeClient extends JavaObject { return WebChromeClient.detached( onProgressChanged: onProgressChanged, onShowFileChooser: onShowFileChooser, + onGeolocationPermissionsShowPrompt: onGeolocationPermissionsShowPrompt, + onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, binaryMessenger: _api.binaryMessenger, instanceManager: _api.instanceManager, ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index 5a2c56ef16ab..70473bfc13cf 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -377,6 +377,7 @@ class CookieManagerHostApi { class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1528,6 +1529,7 @@ class WebViewClientHostApi { class _WebViewClientFlutterApiCodec extends StandardMessageCodec { const _WebViewClientFlutterApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebResourceErrorData) { @@ -1990,6 +1992,13 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. + void onGeolocationPermissionsShowPrompt( + int instanceId, int paramsInstanceId, String origin); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. + void onGeolocationPermissionsHidePrompt(int identifier); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2069,6 +2078,53 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null int.'); + final int? arg_paramsInstanceId = (args[1] as int?); + assert(arg_paramsInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null int.'); + final String? arg_origin = (args[2] as String?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null String.'); + api.onGeolocationPermissionsShowPrompt( + arg_instanceId!, arg_paramsInstanceId!, arg_origin!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt was null, expected non-null int.'); + api.onGeolocationPermissionsHidePrompt(arg_identifier!); + return; + }); + } + } } } @@ -2129,6 +2185,7 @@ class WebStorageHostApi { class _FileChooserParamsFlutterApiCodec extends StandardMessageCodec { const _FileChooserParamsFlutterApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is FileChooserModeEnumData) { @@ -2301,3 +2358,85 @@ abstract class PermissionRequestFlutterApi { } } } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +class GeolocationPermissionsCallbackHostApi { + /// Constructor for [GeolocationPermissionsCallbackHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeolocationPermissionsCallbackHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + Future invoke(int arg_instanceId, String arg_origin, bool arg_allow, + bool arg_retain) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_origin, arg_allow, arg_retain]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +/// Flutter API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +abstract class GeolocationPermissionsCallbackFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId); + + static void setup(GeolocationPermissionsCallbackFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 7f7c3427ead6..3f8e10b04032 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -45,6 +45,8 @@ class AndroidWebViewFlutterApis { WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, + GeolocationPermissionsCallbackFlutterApiImpl? + geolocationPermissionsCallbackFlutterApi, WebViewFlutterApiImpl? webViewFlutterApi, PermissionRequestFlutterApiImpl? permissionRequestFlutterApi, }) { @@ -60,6 +62,9 @@ class AndroidWebViewFlutterApis { javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); this.fileChooserParamsFlutterApi = fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); + this.geolocationPermissionsCallbackFlutterApi = + geolocationPermissionsCallbackFlutterApi ?? + GeolocationPermissionsCallbackFlutterApiImpl(); this.webViewFlutterApi = webViewFlutterApi ?? WebViewFlutterApiImpl(); this.permissionRequestFlutterApi = permissionRequestFlutterApi ?? PermissionRequestFlutterApiImpl(); @@ -90,6 +95,10 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [FileChooserParams]. late final FileChooserParamsFlutterApiImpl fileChooserParamsFlutterApi; + /// Flutter Api for [GeolocationPermissionsCallback]. + late final GeolocationPermissionsCallbackFlutterApiImpl + geolocationPermissionsCallbackFlutterApi; + /// Flutter Api for [WebView]. late final WebViewFlutterApiImpl webViewFlutterApi; @@ -105,6 +114,8 @@ class AndroidWebViewFlutterApis { WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); + GeolocationPermissionsCallbackFlutterApi.setup( + geolocationPermissionsCallbackFlutterApi); WebViewFlutterApi.setup(webViewFlutterApi); PermissionRequestFlutterApi.setup(permissionRequestFlutterApi); _haveBeenSetUp = true; @@ -920,6 +931,32 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { return Future>.value(const []); } + @override + void onGeolocationPermissionsShowPrompt( + int instanceId, int paramsInstanceId, String origin) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + final GeolocationPermissionsCallback callback = + instanceManager.getInstanceWithWeakReference(paramsInstanceId)! + as GeolocationPermissionsCallback; + final GeolocationPermissionsShowPrompt? onShowPrompt = + instance.onGeolocationPermissionsShowPrompt; + if (onShowPrompt != null) { + onShowPrompt(origin, callback); + } + } + + @override + void onGeolocationPermissionsHidePrompt(int identifier) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + final GeolocationPermissionsHidePrompt? onHidePrompt = + instance.onGeolocationPermissionsHidePrompt; + if (onHidePrompt != null) { + return onHidePrompt(instance); + } + } + @override void onPermissionRequest( int instanceId, @@ -1007,6 +1044,75 @@ class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { } } +/// Host api implementation for [GeolocationPermissionsCallback]. +class GeolocationPermissionsCallbackHostApiImpl + extends GeolocationPermissionsCallbackHostApi { + /// Constructs a [GeolocationPermissionsCallbackHostApiImpl]. + GeolocationPermissionsCallbackHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with java objects. + final InstanceManager instanceManager; + + /// Helper method to convert instances ids to objects. + Future invokeFromInstances( + GeolocationPermissionsCallback instance, + String origin, + bool allow, + bool retain, + ) { + return invoke( + instanceManager.getIdentifier(instance)!, + origin, + allow, + retain, + ); + } +} + +/// Flutter API implementation for [GeolocationPermissionsCallback]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +class GeolocationPermissionsCallbackFlutterApiImpl + implements GeolocationPermissionsCallbackFlutterApi { + /// Constructs a [GeolocationPermissionsCallbackFlutterApiImpl]. + GeolocationPermissionsCallbackFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int instanceId) { + instanceManager.addHostCreatedInstance( + GeolocationPermissionsCallback.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + instanceId, + ); + } +} + /// Host api implementation for [PermissionRequest]. class PermissionRequestHostApiImpl extends PermissionRequestHostApi { /// Constructs a [PermissionRequestHostApiImpl]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index a6d3eacddbad..151cc81b8237 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -110,6 +110,33 @@ class AndroidWebViewController extends PlatformWebViewController { } }; }), + onGeolocationPermissionsShowPrompt: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (String origin, + android_webview.GeolocationPermissionsCallback callback) async { + final OnGeolocationPermissionsShowPrompt? onShowPrompt = + weakReference.target?._onGeolocationPermissionsShowPrompt; + if (onShowPrompt != null) { + final GeolocationPermissionsResponse response = await onShowPrompt( + GeolocationPermissionsRequestParams(origin: origin), + ); + callback.invoke(origin, response.allow, response.retain); + } else { + // default don't allow + callback.invoke(origin, false, false); + } + }; + }), + onGeolocationPermissionsHidePrompt: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebChromeClient instance) { + final OnGeolocationPermissionsHidePrompt? onHidePrompt = + weakReference.target?._onGeolocationPermissionsHidePrompt; + if (onHidePrompt != null) { + onHidePrompt(); + } + }; + }), onShowFileChooser: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -180,6 +207,11 @@ class AndroidWebViewController extends PlatformWebViewController { Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; + + OnGeolocationPermissionsShowPrompt? _onGeolocationPermissionsShowPrompt; + + OnGeolocationPermissionsHidePrompt? _onGeolocationPermissionsHidePrompt; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; /// Whether to enable the platform's webview content debugging tools. @@ -434,6 +466,33 @@ class AndroidWebViewController extends PlatformWebViewController { ) async { _onPermissionRequestCallback = onPermissionRequest; } + + /// Sets the callback that is invoked when the client request handle geolocation permissions. + /// + /// Param [onShowPrompt] notifies the host application that web content from the specified origin is attempting to use the Geolocation API, + /// but no permission state is currently set for that origin. + /// + /// The host application should invoke the specified callback with the desired permission state. + /// See GeolocationPermissions for details. + /// + /// Note that for applications targeting Android N and later SDKs (API level > Build.VERSION_CODES.M) + /// this method is only called for requests originating from secure origins such as https. + /// On non-secure origins geolocation requests are automatically denied. + /// + /// Param [onHidePrompt] notifies the host application that a request for Geolocation permissions, + /// made with a previous call to onGeolocationPermissionsShowPrompt() has been canceled. + /// Any related UI should therefore be hidden. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient#onGeolocationPermissionsShowPrompt(java.lang.String,%20android.webkit.GeolocationPermissions.Callback) + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient#onGeolocationPermissionsHidePrompt() + Future setGeolocationPermissionsPromptCallbacks({ + OnGeolocationPermissionsShowPrompt? onShowPrompt, + OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) async { + _onGeolocationPermissionsShowPrompt = onShowPrompt; + _onGeolocationPermissionsHidePrompt = onHidePrompt; + } } /// Android implementation of [PlatformWebViewPermissionRequest]. @@ -472,6 +531,46 @@ class AndroidWebViewPermissionRequest extends PlatformWebViewPermissionRequest { } } +/// Signature for the `setGeolocationPermissionsPromptCallbacks` callback responsible for request the Geolocation API. +typedef OnGeolocationPermissionsShowPrompt + = Future Function( + GeolocationPermissionsRequestParams request); + +/// Signature for the `setGeolocationPermissionsPromptCallbacks` callback responsible for request the Geolocation API is cancel. +typedef OnGeolocationPermissionsHidePrompt = void Function(); + +/// A request params used by the host application to set the Geolocation permission state for an origin. +@immutable +class GeolocationPermissionsRequestParams { + /// [origin]: The origin for which permissions are set. + const GeolocationPermissionsRequestParams({ + required this.origin, + }); + + /// [origin]: The origin for which permissions are set. + final String origin; +} + +/// A response used by the host application to set the Geolocation permission state for an origin. +@immutable +class GeolocationPermissionsResponse { + /// [allow]: Whether or not the origin should be allowed to use the Geolocation API. + /// + /// [retain]: Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + const GeolocationPermissionsResponse({ + required this.allow, + required this.retain, + }); + + /// Whether or not the origin should be allowed to use the Geolocation API. + final bool allow; + + /// Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + final bool retain; +} + /// Mode of how to select files for a file chooser. enum FileSelectorMode { /// Open single file and requires that the file exists before allowing the diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index a4886b9bc5c4..f75eb3235bde 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -365,6 +365,16 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. + void onGeolocationPermissionsShowPrompt( + int instanceId, + int paramsInstanceId, + String origin, + ); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. + void onGeolocationPermissionsHidePrompt(int identifier); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -416,3 +426,29 @@ abstract class PermissionRequestFlutterApi { /// Create a new Dart instance and add it to the `InstanceManager`. void create(int instanceId, List resources); } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@HostApi(dartHostTestHandler: 'TestGeolocationPermissionsCallbackHostApi') +abstract class GeolocationPermissionsCallbackHostApi { + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + void invoke(int instanceId, String origin, bool allow, bool retain); +} + +/// Flutter API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@FlutterApi() +abstract class GeolocationPermissionsCallbackFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index fb173760ba7d..189843e4f6d6 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.7.1 +version: 3.8.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 7d834ba7466d..3f93d46b7705 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -515,6 +515,8 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, super.onShowFileChooser, + super.onGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsHidePrompt, super.onPermissionRequest, super.binaryMessenger, super.instanceManager, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 8dcc1ad19831..d8527ca9e0eb 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -18,6 +18,8 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; +import 'android_webview_test.mocks.dart' + show MockTestGeolocationPermissionsCallbackHostApi; import 'test_android_webview.g.dart'; @GenerateNiceMocks(>[ @@ -55,6 +57,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + android_webview.GeolocationPermissionsShowPrompt? + onGeolocationPermissionsShowPrompt, + android_webview.GeolocationPermissionsHidePrompt? + onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, @@ -73,18 +79,25 @@ void main() { androidWebStorage: mockWebStorage ?? MockWebStorage(), androidWebViewProxy: AndroidWebViewProxy( createAndroidWebChromeClient: createWebChromeClient ?? - ({ - void Function(android_webview.WebView, int)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - }) => + ( + {void Function(android_webview.WebView, int)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function( + String origin, + android_webview.GeolocationPermissionsCallback + callback, + )? onGeolocationPermissionsShowPrompt, + void Function( + android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt}) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, createAndroidWebViewClient: ({ @@ -574,6 +587,8 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, }) { onShowFileChooserCallback = onShowFileChooser!; @@ -609,6 +624,75 @@ void main() { expect(fileSelectorParams.mode, FileSelectorMode.open); }); + test('setGeolocationPermissionsPromptCallbacks', () async { + final MockTestGeolocationPermissionsCallbackHostApi mockApi = + MockTestGeolocationPermissionsCallbackHostApi(); + TestGeolocationPermissionsCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final android_webview.GeolocationPermissionsCallback testCallback = + android_webview.GeolocationPermissionsCallback.detached( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(testCallback, instanceIdentifier); + + late final Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback) + onGeoPermissionHandle; + late final void Function(android_webview.WebChromeClient instance) + onGeoPermissionHidePromptHandle; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt, + dynamic onPermissionRequest, + }) { + onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!; + onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!; + return mockWebChromeClient; + }, + ); + + String testValue = 'origin'; + const String allowOrigin = 'https://www.allow.com'; + bool isAllow = false; + + late final GeolocationPermissionsResponse response; + controller.setGeolocationPermissionsPromptCallbacks( + onShowPrompt: (GeolocationPermissionsRequestParams request) async { + isAllow = request.origin == allowOrigin; + response = + GeolocationPermissionsResponse(allow: isAllow, retain: isAllow); + return response; + }, + onHidePrompt: () { + testValue = 'changed'; + }, + ); + + await onGeoPermissionHandle( + allowOrigin, + testCallback, + ); + + expect(isAllow, true); + + onGeoPermissionHidePromptHandle(mockWebChromeClient); + expect(testValue, 'changed'); + }); + test('setOnPlatformPermissionRequest', () async { late final void Function( android_webview.WebChromeClient instance, @@ -620,6 +704,8 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, @@ -670,6 +756,8 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index 093312e06e1b..f6e7124c4d95 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -719,6 +719,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setGeolocationPermissionsPromptCallbacks({ + _i8.OnGeolocationPermissionsShowPrompt? onShowPrompt, + _i8.OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) => + (super.noSuchMethod( + Invocation.method( + #setGeolocationPermissionsPromptCallbacks, + [], + { + #onShowPrompt: onShowPrompt, + #onHidePrompt: onHidePrompt, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -740,6 +757,11 @@ class MockAndroidWebViewProxy extends _i1.Mock ) as _i2.WebView Function()); @override _i2.WebChromeClient Function({ + void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -755,6 +777,12 @@ class MockAndroidWebViewProxy extends _i1.Mock }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), returnValue: ({ + void Function(_i2.WebChromeClient)? + onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -773,6 +801,12 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), returnValueForMissingStub: ({ + void Function(_i2.WebChromeClient)? + onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -791,6 +825,11 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), ) as _i2.WebChromeClient Function({ + void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index 9f4aa1dfc706..ee0b48188ef6 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -454,6 +454,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setGeolocationPermissionsPromptCallbacks({ + _i6.OnGeolocationPermissionsShowPrompt? onShowPrompt, + _i6.OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) => + (super.noSuchMethod( + Invocation.method( + #setGeolocationPermissionsPromptCallbacks, + [], + { + #onShowPrompt: onShowPrompt, + #onHidePrompt: onHidePrompt, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 922d7c78bbca..995694fee029 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -19,6 +19,7 @@ import 'test_android_webview.g.dart'; JavaScriptChannel, TestCookieManagerHostApi, TestDownloadListenerHostApi, + TestGeolocationPermissionsCallbackHostApi, TestInstanceManagerHostApi, TestJavaObjectHostApi, TestJavaScriptChannelHostApi, @@ -887,6 +888,28 @@ void main() { expect(result, containsAllInOrder([mockWebView, 76])); }); + test('onGeolocationPermissionsShowPrompt', () async { + const String origin = 'https://www.example.com'; + final GeolocationPermissionsCallback callback = + GeolocationPermissionsCallback.detached(); + final int paramsId = instanceManager.addDartCreatedInstance(callback); + late final GeolocationPermissionsCallback outerCallback; + when(mockWebChromeClient.onGeolocationPermissionsShowPrompt).thenReturn( + (String origin, GeolocationPermissionsCallback callback) async { + outerCallback = callback; + }, + ); + flutterApi.onGeolocationPermissionsShowPrompt( + mockWebChromeClientInstanceId, + paramsId, + origin, + ); + await expectLater( + outerCallback, + callback, + ); + }); + test('onShowFileChooser', () async { late final List result; when(mockWebChromeClient.onShowFileChooser).thenReturn( @@ -1019,32 +1042,59 @@ void main() { }); }); - group('FileChooserParams', () { - test('FlutterApi create', () { - final InstanceManager instanceManager = InstanceManager( - onWeakReferenceRemoved: (_) {}, - ); + test('onGeolocationPermissionsHidePrompt', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); - final FileChooserParamsFlutterApiImpl flutterApi = - FileChooserParamsFlutterApiImpl( - instanceManager: instanceManager, - ); + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onGeolocationPermissionsHidePrompt: (WebChromeClient instance) { + callbackParameters = [instance]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); - flutterApi.create( - 0, - false, - const ['my', 'list'], - FileChooserModeEnumData(value: FileChooserMode.openMultiple), - 'filenameHint', - ); + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl(instanceManager: instanceManager); - final FileChooserParams instance = instanceManager - .getInstanceWithWeakReference(0)! as FileChooserParams; - expect(instance.isCaptureEnabled, false); - expect(instance.acceptTypes, const ['my', 'list']); - expect(instance.mode, FileChooserMode.openMultiple); - expect(instance.filenameHint, 'filenameHint'); - }); + flutterApi.onGeolocationPermissionsHidePrompt(instanceIdentifier); + + expect(callbackParameters, [instance]); + }); + + test('copy', () { + expect(WebChromeClient.detached().copy(), isA()); + }); + }); + + group('FileChooserParams', () { + test('FlutterApi create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final FileChooserParamsFlutterApiImpl flutterApi = + FileChooserParamsFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create( + 0, + false, + const ['my', 'list'], + FileChooserModeEnumData(value: FileChooserMode.openMultiple), + 'filenameHint', + ); + + final FileChooserParams instance = + instanceManager.getInstanceWithWeakReference(0)! as FileChooserParams; + expect(instance.isCaptureEnabled, false); + expect(instance.acceptTypes, const ['my', 'list']); + expect(instance.mode, FileChooserMode.openMultiple); + expect(instance.filenameHint, 'filenameHint'); }); }); @@ -1233,6 +1283,60 @@ void main() { verify(mockApi.deny(instanceIdentifier)); }); + }); + + group('GeolocationPermissionsCallback', () { + tearDown(() { + TestGeolocationPermissionsCallbackHostApi.setup(null); + }); + + test('invoke', () async { + final MockTestGeolocationPermissionsCallbackHostApi mockApi = + MockTestGeolocationPermissionsCallbackHostApi(); + TestGeolocationPermissionsCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final GeolocationPermissionsCallback instance = + GeolocationPermissionsCallback.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const String origin = 'testString'; + const bool allow = true; + const bool retain = true; + + await instance.invoke( + origin, + allow, + retain, + ); + + verify(mockApi.invoke(instanceIdentifier, origin, allow, retain)); + }); + + test('Geolocation FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final GeolocationPermissionsCallbackFlutterApiImpl api = + GeolocationPermissionsCallbackFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + api.create(instanceIdentifier); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); test('FlutterAPI create', () { final InstanceManager instanceManager = InstanceManager( diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 3b55d856d05b..a7f825fa16c8 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -333,6 +333,36 @@ class MockTestDownloadListenerHostApi extends _i1.Mock ); } +/// A class which mocks [TestGeolocationPermissionsCallbackHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestGeolocationPermissionsCallbackHostApi extends _i1.Mock + implements _i6.TestGeolocationPermissionsCallbackHostApi { + MockTestGeolocationPermissionsCallbackHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void invoke( + int? instanceId, + String? origin, + bool? allow, + bool? retain, + ) => + super.noSuchMethod( + Invocation.method( + #invoke, + [ + instanceId, + origin, + allow, + retain, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestInstanceManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index 20b084e40c3c..98060e042f9b 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -217,6 +217,7 @@ abstract class TestCookieManagerHostApi { class _TestWebViewHostApiCodec extends StandardMessageCodec { const _TestWebViewHostApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1714,3 +1715,55 @@ abstract class TestPermissionRequestHostApi { } } } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +abstract class TestGeolocationPermissionsCallbackHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + void invoke(int instanceId, String origin, bool allow, bool retain); + + static void setup(TestGeolocationPermissionsCallbackHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null int.'); + final String? arg_origin = (args[1] as String?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null String.'); + final bool? arg_allow = (args[2] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null bool.'); + final bool? arg_retain = (args[3] as bool?); + assert(arg_retain != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null bool.'); + api.invoke(arg_instanceId!, arg_origin!, arg_allow!, arg_retain!); + return []; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 566f23fb67be..69c700f52285 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.5.0 + +* Adds support to limit navigation to pages within the app’s domain. See + `WebKitWebViewControllerCreationParams.limitsNavigationsToAppBoundDomains`. + ## 3.4.4 * Removes obsolete null checks on non-nullable values. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 35368a316f1c..6aa6702a86e1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -39,13 +39,16 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents NSCameraUsageDescription If you want to use the camera, you have to give permission. + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m index 2ec74d0522dd..98be6dfe9e2b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m @@ -62,6 +62,24 @@ - (void)testSetAllowsInlineMediaPlayback { XCTAssertNil(error); } +- (void)testSetLimitsNavigationsToAppBoundDomains API_AVAILABLE(ios(14.0)) { + WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebViewConfiguration withIdentifier:0]; + + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:@0 + isLimited:@NO + error:&error]; + OCMVerify([mockWebViewConfiguration setLimitsNavigationsToAppBoundDomains:NO]); + XCTAssertNil(error); +} + - (void)testSetMediaTypesRequiringUserActionForPlayback { WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 45b1e42a355a..24b6346e14eb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -480,6 +480,10 @@ NSObject *FWFWKWebViewConfigurationHostApiGetCodec(void); error: (FlutterError *_Nullable *_Nonnull) error; +- (void)setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:(NSNumber *)identifier + isLimited:(NSNumber *)limit + error:(FlutterError *_Nullable + *_Nonnull)error; - (void) setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(NSNumber *)identifier forTypes: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index 3a5dff6a5d59..fc28c28cc553 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "FWFGeneratedWebKitApis.h" @@ -1002,6 +1002,34 @@ void FWFWKWebViewConfigurationHostApiSetup(id binaryMess [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." + @"setLimitsNavigationsToAppBoundDomains" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier: + isLimited:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:" + @"isLimited:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_limit = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:arg_identifier + isLimited:arg_limit + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m index 987d3f45ff2c..762c07b5abe6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m @@ -104,6 +104,23 @@ - (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(nonnull NSNu setAllowsInlineMediaPlayback:allow.boolValue]; } +- (void)setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier: + (nonnull NSNumber *)identifier + isLimited: + (nonnull NSNumber *)limit + error:(FlutterError *_Nullable + *_Nonnull)error { + if (@available(iOS 14, *)) { + [[self webViewConfigurationForIdentifier:identifier] + setLimitsNavigationsToAppBoundDomains:limit.boolValue]; + } else { + *error = [FlutterError + errorWithCode:@"FWFUnsupportedVersionError" + message:@"setLimitsNavigationsToAppBoundDomains is only supported on versions 14+." + details:nil]; + } +} + - (void) setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(nonnull NSNumber *)identifier forTypes: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index 2ce5055d056c..0f3547a5a44f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -1067,6 +1067,30 @@ class WKWebViewConfigurationHostApi { } } + Future setLimitsNavigationsToAppBoundDomains( + int arg_identifier, bool arg_limit) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_limit]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future setMediaTypesRequiringUserActionForPlayback(int arg_identifier, List arg_types) async { final BasicMessageChannel channel = BasicMessageChannel( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index 070f554a5d5a..31cbbf3e1cd7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -678,6 +678,20 @@ class WKWebViewConfiguration extends NSObject { ); } + /// Indicates whether the web view limits navigation to pages within the app’s domain. + /// + /// When navigation is limited, Javascript evaluation is unrestricted. + /// See https://webkit.org/blog/10882/app-bound-domains/ + /// + /// Sets [WKWebViewConfiguration.limitsNavigationsToAppBoundDomains](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/3585117-limitsnavigationstoappbounddomai?language=objc). + Future setLimitsNavigationsToAppBoundDomains(bool limit) { + return _webViewConfigurationApi + .setLimitsNavigationsToAppBoundDomainsForInstances( + this, + limit, + ); + } + /// The media types that require a user gesture to begin playing. /// /// Use [WKAudiovisualMediaType.none] to indicate that no user gestures are diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index 07a32aee8d33..daba854763a7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -629,6 +629,17 @@ class WKWebViewConfigurationHostApiImpl extends WKWebViewConfigurationHostApi { ); } + /// Calls [setLimitsNavigationsToAppBoundDomains] with the ids of the provided object instances. + Future setLimitsNavigationsToAppBoundDomainsForInstances( + WKWebViewConfiguration instance, + bool limit, + ) { + return setLimitsNavigationsToAppBoundDomains( + instanceManager.getIdentifier(instance)!, + limit, + ); + } + /// Calls [setMediaTypesRequiringUserActionForPlayback] with the ids of the provided object instances. Future setMediaTypesRequiringUserActionForPlaybackForInstances( WKWebViewConfiguration instance, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index e49d41257e83..b0666e6dd7cd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -48,6 +48,7 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, this.allowsInlineMediaPlayback = false, + this.limitsNavigationsToAppBoundDomains = false, @visibleForTesting InstanceManager? instanceManager, }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager { _configuration = webKitProxy.createWebViewConfiguration( @@ -68,6 +69,8 @@ class WebKitWebViewControllerCreationParams ); } _configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); + _configuration.setLimitsNavigationsToAppBoundDomains( + limitsNavigationsToAppBoundDomains); } /// Constructs a [WebKitWebViewControllerCreationParams] using a @@ -83,11 +86,14 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, bool allowsInlineMediaPlayback = false, + bool limitsNavigationsToAppBoundDomains = false, @visibleForTesting InstanceManager? instanceManager, }) : this( webKitProxy: webKitProxy, mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, allowsInlineMediaPlayback: allowsInlineMediaPlayback, + limitsNavigationsToAppBoundDomains: + limitsNavigationsToAppBoundDomains, instanceManager: instanceManager, ); @@ -104,6 +110,13 @@ class WebKitWebViewControllerCreationParams /// Defaults to false. final bool allowsInlineMediaPlayback; + /// Whether to limit navigation to configured domains. + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ + /// (Only available for iOS > 14.0) + /// Defaults to false. + final bool limitsNavigationsToAppBoundDomains; + /// Handles constructing objects and calling static methods for the WebKit /// native library. @visibleForTesting diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 20d4ee41f4fe..ac13958089cd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -416,6 +416,11 @@ abstract class WKWebViewConfigurationHostApi { ) void setAllowsInlineMediaPlayback(int identifier, bool allow); + @ObjCSelector( + 'setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:isLimited:', + ) + void setLimitsNavigationsToAppBoundDomains(int identifier, bool limit); + @ObjCSelector( 'setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:', ) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index db7407ba56f9..9056fd9ecae4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.4.4 +version: 3.5.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index ff09d4401ee5..faf01239918d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -369,6 +369,8 @@ abstract class TestWKWebViewConfigurationHostApi { void setAllowsInlineMediaPlayback(int identifier, bool allow); + void setLimitsNavigationsToAppBoundDomains(int identifier, bool limit); + void setMediaTypesRequiringUserActionForPlayback( int identifier, List types); @@ -448,6 +450,33 @@ abstract class TestWKWebViewConfigurationHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null, expected non-null int.'); + final bool? arg_limit = (args[1] as bool?); + assert(arg_limit != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null, expected non-null bool.'); + api.setLimitsNavigationsToAppBoundDomains( + arg_identifier!, arg_limit!); + return []; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart index 355d69e7dc90..8a9cc199885b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -69,6 +69,21 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock returnValueForMissingStub: null, ); @override + void setLimitsNavigationsToAppBoundDomains( + int? identifier, + bool? limit, + ) => + super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [ + identifier, + limit, + ], + ), + returnValueForMissingStub: null, + ); + @override void setMediaTypesRequiringUserActionForPlayback( int? identifier, List<_i3.WKAudiovisualMediaTypeEnumData?>? types, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index e2d53bc9fec3..ac266ff79ebb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -441,6 +441,14 @@ void main() { )); }); + test('limitsNavigationsToAppBoundDomains', () { + webViewConfiguration.setLimitsNavigationsToAppBoundDomains(true); + verify(mockPlatformHostApi.setLimitsNavigationsToAppBoundDomains( + instanceManager.getIdentifier(webViewConfiguration), + true, + )); + }); + test('mediaTypesRequiringUserActionForPlayback', () { webViewConfiguration.setMediaTypesRequiringUserActionForPlayback( { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart index 601964044dea..0291867aaf16 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -298,6 +298,21 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock returnValueForMissingStub: null, ); @override + void setLimitsNavigationsToAppBoundDomains( + int? identifier, + bool? limit, + ) => + super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [ + identifier, + limit, + ], + ), + returnValueForMissingStub: null, + ); + @override void setMediaTypesRequiringUserActionForPlayback( int? identifier, List<_i4.WKAudiovisualMediaTypeEnumData?>? types, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 7e61d36ec7ad..4e56c21dea87 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -140,6 +140,24 @@ void main() { ); }); + test('limitsNavigationsToAppBoundDomains', () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + limitsNavigationsToAppBoundDomains: true, + ); + + verify( + mockConfiguration.setLimitsNavigationsToAppBoundDomains(true), + ); + }); + test('mediaTypesRequiringUserAction', () { final MockWKWebViewConfiguration mockConfiguration = MockWKWebViewConfiguration(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart index 9eb03971e828..de787a1e5e28 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart @@ -848,6 +848,16 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override + _i6.Future setLimitsNavigationsToAppBoundDomains(bool? limit) => + (super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [limit], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override _i6.Future setMediaTypesRequiringUserActionForPlayback( Set<_i5.WKAudiovisualMediaType>? types) => (super.noSuchMethod( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart index fe86de77af0c..b171e28a3bfb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -176,6 +176,16 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); @override + _i3.Future setLimitsNavigationsToAppBoundDomains(bool? limit) => + (super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [limit], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override _i3.Future setMediaTypesRequiringUserActionForPlayback( Set<_i2.WKAudiovisualMediaType>? types) => (super.noSuchMethod( diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index 1cf35c8881e2..d027396f93dd 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -48,6 +48,7 @@ - logging - markdown - meta +- mime - path - shelf - shelf_static diff --git a/script/configs/exclude_integration_linux.yaml b/script/configs/exclude_integration_linux.yaml index a83550e6808f..1f5ad8c843bc 100644 --- a/script/configs/exclude_integration_linux.yaml +++ b/script/configs/exclude_integration_linux.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_linux +- image_picker_linux diff --git a/script/configs/exclude_integration_macos.yaml b/script/configs/exclude_integration_macos.yaml index 7a9e287da05f..e2b639cb96f9 100644 --- a/script/configs/exclude_integration_macos.yaml +++ b/script/configs/exclude_integration_macos.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_macos +- image_picker_macos diff --git a/script/configs/windows_unit_tests_exceptions.yaml b/script/configs/windows_unit_tests_exceptions.yaml index b837d567713a..76396080fb7d 100644 --- a/script/configs/windows_unit_tests_exceptions.yaml +++ b/script/configs/windows_unit_tests_exceptions.yaml @@ -11,20 +11,12 @@ # Unit tests for plugins that support web currently run in # Chrome, which isn't currently supported by web infrastructure. # TODO(ditman): Fix this in the repo tooling. -- camera - camera_web -- file_selector - file_selector_web -- google_maps_flutter - google_maps_flutter_web -- google_sign_in - google_sign_in_web -- image_picker - image_picker_for_web -- shared_preferences - shared_preferences_web -- url_launcher - url_launcher_web -- video_player - video_player_web - webview_flutter_web diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md deleted file mode 100644 index 44d1d72473e7..000000000000 --- a/script/tool/CHANGELOG.md +++ /dev/null @@ -1,675 +0,0 @@ -## 0.13.4+4 - -* Allows code excerpts in `example/README.md`. - -## 0.13.4+3 - -* Moves source to flutter/packages. - -## 0.13.4+2 - -* Expands the `--packages-for-branch` detection of main to include ancestors - of `origin/main` and `upstream/main`. - -## 0.13.4+1 - -* Makes `--packages-for-branch` detect any commit on `main` as being `main`, - so that it works with pinned checkouts (e.g., on LUCI). - -## 0.13.4 - -* Adds the ability to validate minimum supported Dart/Flutter versions in - `pubspec-check`. - -## 0.13.3 - -* Renames `podspecs` to `podspec-check`. The old name will continue to work. -* Adds validation of the Swift-in-Obj-C-projects workaround in the podspecs of - iOS plugin implementations that use Swift. - -## 0.13.2+1 - -* Replaces deprecated `flutter format` with `dart format` in `format` - implementation. - -## 0.13.2 - -* Falls back to other executables in PATH when `clang-format` does not run. - -## 0.13.1 - -* Updates `version-check` to recognize Pigeon's platform test structure. -* Pins `package:git` dependency to `2.0.x` until `dart >=2.18.0` becomes our - oldest legacy. -* Updates test mocks. - -## 0.13.0 - -* Renames `all-plugins-app` to `create-all-packages-app` to clarify what it - actually does. Also renames the project directory it creates from - `all_plugins` to `all_packages`. - -## 0.12.1 - -* Modifies `publish_check_command.dart` to do a `dart pub get` in all examples - of the package being checked. Workaround for [dart-lang/pub#3618](https://github.com/dart-lang/pub/issues/3618). - -## 0.12.0 - -* Changes the behavior of `--packages-for-branch` on main/master to run for - packages changed in the last commit, rather than running for all packages. - This allows CI to test the same filtered set of packages in post-submit as are - tested in presubmit. -* Adds a `fix` command to run `dart fix --apply` in target packages. - -## 0.11.0 - -* Renames `publish-plugin` to `publish`. -* Renames arguments to `list`: - * `--package` now lists top-level packages (previously `--plugin`). - * `--package-or-subpackage` now lists top-level packages (previously - `--package`). - -## 0.10.0+1 - -* Recognizes `run_test.sh` as a developer-only file in `version-check`. -* Adds `readme-check` validation that the example/README.md for a federated - plugin's implementation packages has a warning about the intended use of the - example instead of the template boilerplate. - -## 0.10.0 - -* Improves the logic in `version-check` to determine what changes don't require - version changes, as well as making any dev-only changes also not require - changelog changes since in practice we almost always override the check in - that case. -* Removes special-case handling of Dependabot PRs, and the (fragile) - `--change-description-file` flag was only still used for that case, as - the improved diff analysis now handles that case more robustly. - -## 0.9.3 - -* Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. - -## 0.9.2 - -* Adds checking of `code-excerpt` configuration to `readme-check`, to validate - that if the excerpting tags are added to a README they are actually being - used. - -## 0.9.1 - -* Adds a `--downgrade` flag to `analyze` for analyzing with the oldest possible - versions of packages. - -## 0.9.0 - -* Replaces PR-description-based version/changelog/breaking change check - overrides in `version-check` with label-based overrides using a new - `pr-labels` flag, since we don't actually have reliable access to the - PR description in checks. - -## 0.8.10 - -- Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` - entries to make legacy version analysis possible in more cases. -- Adds a `--lib-only` option to `analyze` to allow only analyzing the client - parts of a library for legacy verison compatibility. - -## 0.8.9 - -- Includes `dev_dependencies` when overridding dependencies using - `make-deps-path-based`. -- Bypasses version and CHANGELOG checks for Dependabot PRs for packages - that are known not to be client-affecting. - -## 0.8.8 - -- Allows pre-release versions in `version-check`. - -## 0.8.7 - -- Supports empty custom analysis allow list files. -- `drive-examples` now validates files to ensure that they don't accidentally - use `test(...)`. -- Adds a new `dependabot-check` command to ensure complete Dependabot coverage. -- Adds `skip-if-not-supporting-dart-version` to allow for the same use cases - as `skip-if-not-supporting-flutter-version` but for packages without Flutter - constraints. - -## 0.8.6 - -- Adds `update-release-info` to apply changelog and optional version changes - across multiple packages. -- Fixes changelog validation when reverting to a `NEXT` state. -- Fixes multiplication of `--force` flag when publishing multiple packages. -- Adds minimum deployment target flags to `xcode-analyze` to allow - enforcing deprecation warning handling in advance of actually dropping - support for an OS version. -- Checks for template boilerplate in `readme-check`. -- `readme-check` now validates example READMEs when present. - -## 0.8.5 - -- Updates `test` to inculde the Dart unit tests of examples, if any. -- `drive-examples` now supports non-plugin packages. -- Commands that iterate over examples now include non-Flutter example packages. - -## 0.8.4 - -- `readme-check` now validates that there's a info tag on code blocks to - identify (and for supported languages, syntax highlight) the language. -- `readme-check` now has a `--require-excerpts` flag to require that any Dart - code blocks be managed by `code_excerpter`. - -## 0.8.3 - -- Adds a new `update-excerpts` command to maintain README files using the - `code-excerpter` package from flutter/site-shared. -- `license-check` now ignores submodules. -- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so - that running multiple times won't fail after the first time. -- Removes UWP support, since Flutter has dropped support for UWP. - -## 0.8.2+1 - -- Adds a new `readme-check` command. -- Updates `publish-plugin` command documentation. -- Fixes `all-plugins-app` to preserve the original application's Dart SDK - version to avoid changing language feature opt-ins that the template may - rely on. -- Fixes `custom-test` to run `pub get` before running Dart test scripts. - -## 0.8.2 - -- Adds a new `custom-test` command. -- Switches from deprecated `flutter packages` alias to `flutter pub`. - -## 0.8.1 - -- Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` - sub-packages. - -## 0.8.0 - -- Ensures that `firebase-test-lab` runs include an `integration_test` runner. -- Adds a `make-deps-path-based` command to convert inter-repo package - dependencies to path-based dependencies. -- Adds a (hidden) `--run-on-dirty-packages` flag for use with - `make-deps-path-based` in CI. -- `--packages` now allows using a federated plugin's package as a target without - fully specifying it (if it is not the same as the plugin's name). E.g., - `--packages=path_provide_ios` now works. -- `--run-on-changed-packages` now includes only the changed packages in a - federated plugin, not all packages in that plugin. -- Fixes `federation-safety-check` handling of plugin deletion, and of top-level - files in unfederated plugins whose names match federated plugin heuristics - (e.g., `packages/foo/foo_android.iml`). -- Adds an auto-retry for failed Firebase Test Lab tests as a short-term patch - for flake issues. -- Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar - `flutter` behavior. -- Validates `default_package` entries in plugins. -- Removes `allow-warnings` from the `podspecs` command. -- Adds `skip-if-not-supporting-flutter-version` to allow running tests using a - version of Flutter that not all packages support. (E.g., to allow for running - some tests against old versions of Flutter to help avoid accidental breakage.) - -## 0.7.3 - -- `native-test` now builds unit tests before running them on Windows and Linux, - matching the behavior of other platforms. -- Adds `--log-timing` to add timing information to package headers in looping - commands. -- Adds a `--check-for-missing-changes` flag to `version-check` that requires - version updates (except for recognized exemptions) and CHANGELOG changes when - modifying packages, unless the PR description explains why it's not needed. - -## 0.7.2 - -- Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). -- `native-test --android`, `--ios`, and `--macos` now fail plugins that don't - have unit tests, rather than skipping them. -- Added a new `federation-safety-check` command to help catch changes to - federated packages that have been done in such a way that they will pass in - CI, but fail once the change is landed and published. -- `publish-check` now validates that there is an `AUTHORS` file. -- Added flags to `version-check` to allow overriding the platform interface - major version change restriction. -- Improved error handling and error messages in CHANGELOG version checks. -- `license-check` now validates Kotlin files. -- `pubspec-check` now checks that the description is of the pub-recommended - length. -- Fix `license-check` when run on Windows with line ending conversion enabled. -- Fixed `pubspec-check` on Windows. -- Add support for `main` as a primary branch. `master` continues to work for - compatibility. - -## 0.7.1 - -- Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. - -## 0.7.0 - -- `native-test` now supports `--linux` for unit tests. -- Formatting now skips Dart files that contain a line that exactly - matches the string `// This file is hand-formatted.`. - -## 0.6.0+1 - -- Fixed `build-examples` to work for non-plugin packages. - -## 0.6.0 - -- Added Android native integration test support to `native-test`. -- Added a new `android-lint` command to lint Android plugin native code. -- Pubspec validation now checks for `implements` in implementation packages. -- Pubspec valitation now checks the full relative path of `repository` entries. -- `build-examples` now supports UWP plugins via a `--winuwp` flag. -- `native-test` now supports `--windows` for unit tests. -- **Breaking change**: `publish` no longer accepts `--no-tag-release` or - `--no-push-flags`. Releases now always tag and push. -- **Breaking change**: `publish`'s `--package` flag has been replaced with the - `--packages` flag used by most other packages. -- **Breaking change** Passing both `--run-on-changed-packages` and `--packages` - is now an error; previously it the former would be ignored. - -## 0.5.0 - -- `--exclude` and `--custom-analysis` now accept paths to YAML files that - contain lists of packages to exclude, in addition to just package names, - so that exclude lists can be maintained separately from scripts and CI - configuration. -- Added an `xctest` flag to select specific test targets, to allow running only - unit tests or integration tests. -- **Breaking change**: Split Xcode analysis out of `xctest` and into a new - `xcode-analyze` command. -- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more - than one plugin's tests in a single run. -- **Breaking change**: If `firebase-test-lab` is run on a package that supports - Android, but for which no tests are run, it now fails instead of skipping. - This matches `drive-examples`, as this command is what is used for driving - Android Flutter integration tests on CI. -- **Breaking change**: Replaced `xctest` with a new `native-test` command that - will eventually be able to run native unit and integration tests for all - platforms. - - Adds the ability to disable test types via `--no-unit` or - `--no-integration`. -- **Breaking change**: Replaced `java-test` with Android unit test support for - the new `native-test` command. -- Commands that print a run summary at the end now track and log exclusions - similarly to skips for easier auditing. -- `version-check` now validates that `NEXT` is not present when changing - the version. - -## 0.4.1 - -- Improved `license-check` output. -- Use `java -version` rather than `java --version`, for compatibility with more - versions of Java. - -## 0.4.0 - -- Modified the output format of many commands -- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` - files, only `integration_test/*_test.dart`. -- Add a summary to the end of successful command runs for commands using the - new output format. -- Fixed some cases where a failure in a command for a single package would - immediately abort the test. -- Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to - work for now, but will be removed in the future. -- Make `drive-examples` device detection robust against Flutter tool banners. -- `format` is now supported on Windows. - -## 0.3.0 - -- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of - `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward - compatibility. -- `xctest` now supports running macOS tests in addition to iOS - - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. -- **Breaking change**: `build-examples` for iOS now uses `--ios` rather than - `--ipa`. -- The tooling now runs in strong null-safe mode. -- `publish plugins` check against pub.dev to determine if a release should happen. -- Modified the output format of many commands -- Removed `podspec`'s `--skip` in favor of `--ignore` using the new structure. - -## 0.2.0 - -- Remove `xctest`'s `--skip`, which is redundant with `--ignore`. - -## 0.1.4 - -- Add a `pubspec-check` command - -## 0.1.3 - -- Cosmetic fix to `publish-check` output -- Add a --dart-sdk option to `analyze` -- Allow reverts in `version-check` - -## 0.1.2 - -- Add `against-pub` flag for version-check, which allows the command to check version with pub. -- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. -- Add `skip-conformation` flag to publish-plugin to allow auto publishing. -- Change `run-on-changed-packages` to consider all packages as changed if any - files have been changed that could affect the entire repository. - -## 0.1.1 - -- Update the allowed third-party licenses for flutter/packages. - -## 0.1.0+1 - -- Re-add the bin/ directory. - -## 0.1.0 - -- **NOTE**: This is no longer intended as a general-purpose package, and is now - supported only for flutter/plugins and flutter/tools. -- Fix version checks - - Remove handling of pre-release null-safe versions -- Fix build all for null-safe template apps -- Improve handling of web integration tests -- Supports enforcing standardized copyright files -- Improve handling of iOS tests - -## v.0.0.45+3 - -- Pin `collection` to `1.14.13` to be able to target Flutter stable (v1.22.6). - -## v.0.0.45+2 - -- Make `publish-plugin` to work on non-flutter packages. - -## v.0.0.45+1 - -- Don't call `flutter format` if there are no Dart files to format. - -## v.0.0.45 - -- Add exclude flag to exclude any plugin from further processing. - -## v.0.0.44+7 - -- `all-plugins-app` doesn't override the AGP version. - -## v.0.0.44+6 - -- Fix code formatting. - -## v.0.0.44+5 - -- Remove `-v` flag on drive-examples. - -## v.0.0.44+4 - -- Fix bug where directory isn't passed - -## v.0.0.44+3 - -- More verbose logging - -## v.0.0.44+2 - -- Remove pre-alpha Windows workaround to create examples on the fly. - -## v.0.0.44+1 - -- Print packages that passed tests in `xctest` command. -- Remove printing the whole list of simulators. - -## v.0.0.44 - -- Add 'xctest' command to run xctests. - -## v.0.0.43 - -- Allow minor `*-nullsafety` pre release packages. - -## v.0.0.42+1 - -- Fix test command when `--enable-experiment` is called. - -## v.0.0.42 - -- Allow `*-nullsafety` pre release packages. - -## v.0.0.41 - -- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, -and `firebase-test-lab`. - -## v.0.0.40 - -- Support `integration_test/` directory for `drive-examples` command - -## v.0.0.39 - -- Support `integration_test/` directory for `package:integration_test` - -## v.0.0.38 - -- Add C++ and ObjC++ to clang-format. - -## v.0.0.37+2 - -- Make `http` and `http_multi_server` dependency version constraint more flexible. - -## v.0.0.37+1 - -- All_plugin test puts the plugin dependencies into dependency_overrides. - -## v.0.0.37 - -- Only builds mobile example apps when necessary. - -## v.0.0.36+3 - -- Add support for Linux plugins. - -## v.0.0.36+2 - -- Default to showing podspec lint warnings - -## v.0.0.36+1 - -- Serialize linting podspecs. - -## v.0.0.36 - -- Remove retry on Firebase Test Lab's call to gcloud set. -- Remove quiet flag from Firebase Test Lab's gcloud set command. -- Allow Firebase Test Lab command to continue past gcloud set network failures. - This is a mitigation for the network service sometimes not responding, - but it isn't actually necessary to have a network connection for this command. - -## v.0.0.35+1 - -- Minor cleanup to the analyze test. - -## v.0.0.35 - -- Firebase Test Lab command generates a configurable unique path suffix for results. - -## v.0.0.34 - -- Firebase Test Lab command now only tries to configure the project once -- Firebase Test Lab command now retries project configuration up to five times. - -## v.0.0.33+1 - -- Fixes formatting issues that got past our CI due to - https://github.com/flutter/flutter/issues/51585. -- Changes the default package name for testing method `createFakePubspec` back - its previous behavior. - -## v.0.0.33 - -- Version check command now fails on breaking changes to platform interfaces. -- Updated version check test to be more flexible. - -## v.0.0.32+7 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. - -## v.0.0.32+6 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. - -## v.0.0.32+5 - -- Remove --fail-fast and --silent from lint podspec command. - -## v.0.0.32+4 - -- Update `publish-plugin` to use `flutter pub publish` instead of just `pub - publish`. Enforces a `pub publish` command that matches the Dart SDK in the - user's Flutter install. - -## v.0.0.32+3 - -- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). - -## v.0.0.32+2 - -- Runs pub get before building macos to avoid failures. - -## v.0.0.32+1 - -- Default macOS example builds to false. Previously they were running whenever - CI was itself running on macOS. - -## v.0.0.32 - -- `analyze` now asserts that the global `analysis_options.yaml` is the only one - by default. Individual directories can be excluded from this check with the - new `--custom-analysis` flag. - -## v.0.0.31+1 - -- Add --skip and --no-analyze flags to podspec command. - -## v.0.0.31 - -- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. - -## v.0.0.30 - -- Adopt pedantic analysis options, fix firebase_test_lab_test. - -## v.0.0.29 - -- Add a command to run pod lib lint on podspec files. - -## v.0.0.28 - -- Increase Firebase test lab timeouts to 5 minutes. - -## v.0.0.27 - -- Run tests with `--platform=chrome` for web plugins. - -## v.0.0.26 - -- Add a command for publishing plugins to pub. - -## v.0.0.25 - -- Update `DriveExamplesCommand` to use `ProcessRunner`. -- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. -- Add simple tests for `DriveExamplesCommand`. - -## v.0.0.24 - -- Gracefully handle pubspec.yaml files for new plugins. -- Additional unit testing. - -## v.0.0.23 - -- Add a test case for transitive dependency solving in the - `create_all_plugins_app` command. - -## v.0.0.22 - -- Updated firebase-test-lab command with updated conventions for test locations. -- Updated firebase-test-lab to add an optional "device" argument. -- Updated version-check command to always compare refs instead of using the working copy. -- Added unit tests for the firebase-test-lab and version-check commands. -- Add ProcessRunner to mock running processes for testing. - -## v.0.0.21 - -- Support the `--plugins` argument for federated plugins. - -## v.0.0.20 - -- Support for finding federated plugins, where one directory contains - multiple packages for different platform implementations. - -## v.0.0.19+3 - -- Use `package:file` for file I/O. - -## v.0.0.19+2 - -- Use java as language when calling `flutter create`. - -## v.0.0.19+1 - -- Rename command for `CreateAllPluginsAppCommand`. - -## v.0.0.19 - -- Use flutter create to build app testing plugin compilation. - -## v.0.0.18+2 - -- Fix `.travis.yml` file name in `README.md`. - -## v0.0.18+1 - -- Skip version check if it contains `publish_to: none`. - -## v0.0.18 - -- Add option to exclude packages from generated pubspec command. - -## v0.0.17+4 - -- Avoid trying to version-check pubspecs that are missing a version. - -## v0.0.17+3 - -- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). - -## v0.0.17+2 - -- Fix exception handling for version checker - -## v0.0.17+1 - -- Fix bug where we used a flag instead of an option - -## v0.0.17 - -- Add a command for checking the version number - -## v0.0.16 - -- Add a command for generating `pubspec.yaml` for All Plugins app. - -## v0.0.15 - -- Add a command for running driver tests of plugin examples. - -## v0.0.14 - -- Check for dependencies->flutter instead of top level flutter node. - -## v0.0.13 - -- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/script/tool/README.md b/script/tool/README.md index bffa2b66ee9d..435394d80752 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -3,12 +3,8 @@ This is a set of utilities used in this repository, both for CI and for local development. -The tool is designed to be run at the root of the repository or `/packages/`. - ## Getting Started -In flutter/packages, the tool is run from source. - Set up: ```sh @@ -42,31 +38,36 @@ command is targetting. An package name can be any of: - A combination federated_plugin_name/package_name (e.g., `path_provider/path_provider` for the app-facing package). +The examples below assume they are being run from the repository root, but +the script works from anywhere. If you develop in flutter/packages frequently, +it may be useful to make an alias for +`dart run /absolute/path/to/script/tool/bin/flutter_plugin_tools.dart` so that +you can easily run commands from within packages. For that use case there is +also a `--current-package` flag as an alternative to `--packages`, to target the +current working directory's package (or enclosing package; it can be used from +anywhere within a package). + ### Format Code ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart format --packages package_name ``` ### Run the Dart Static Analyzer ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart analyze --packages package_name ``` ### Run Dart Unit Tests ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart test --packages package_name ``` ### Run Dart Integration Tests ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages package_name dart run script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages package_name ``` @@ -83,7 +84,6 @@ runs both unit tests and (on platforms that support it) integration tests, but Examples: ```sh -cd # Run just unit tests for iOS and Android: dart run script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages package_name # Run all tests for macOS: @@ -99,7 +99,6 @@ with submodules, you will need to `git submodule update --init --recursive` before running this command. ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages package_name ``` @@ -114,7 +113,6 @@ For instance, if you add a new analysis option that requires production code changes across many packages: ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-release-info \ --version=minimal \ --base-branch=upstream/main \ @@ -143,7 +141,6 @@ For instance, to updated to version 3.0.0 of `some_package` in every package that depends on it: ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-dependency \ --pub-package=some_package \ --version=3.0.0 \ diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 9b0006123c47..00a38e7045a1 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -71,6 +71,7 @@ abstract class PackageCommand extends Command { defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, + negatable: false, help: 'Run the command on changed packages.\n' 'If no packages have changed, or if there have been changes that may\n' 'affect all packages, the command runs on all packages.\n' @@ -78,18 +79,29 @@ abstract class PackageCommand extends Command { 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); argParser.addFlag(_runOnDirtyPackagesArg, + negatable: false, help: 'Run the command on packages with changes that have not been committed.\n' 'Packages excluded with $_excludeArg are excluded even if changed.\n' 'Cannot be combined with $_packagesArg.\n', hide: true); argParser.addFlag(_packagesForBranchArg, + negatable: false, help: 'This runs on all packages changed in the last commit on main ' '(or master), and behaves like --run-on-changed-packages on ' 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); + argParser.addFlag(_currentPackageArg, + negatable: false, + help: + 'Set the target package(s) based on the current working directory.\n' + '- If the current working directory is (or is inside) a package, ' + 'that package will be targeted.\n' + '- If the current working directory is the root of a federated ' + 'plugin group, that group will be targeted.\n' + 'Cannot be combined with $_packagesArg.\n'); argParser.addOption(_baseShaArg, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' @@ -104,17 +116,22 @@ abstract class PackageCommand extends Command { 'but more information may be added in the future.'); } - static const String _baseBranchArg = 'base-branch'; - static const String _baseShaArg = 'base-sha'; - static const String _excludeArg = 'exclude'; - static const String _logTimingArg = 'log-timing'; + // Package selection. static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; + static const String _currentPackageArg = 'current-package'; static const String _pluginsLegacyAliasArg = 'plugins'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; + static const String _excludeArg = 'exclude'; + // Diff base selection. + static const String _baseBranchArg = 'base-branch'; + static const String _baseShaArg = 'base-sha'; + // Sharding. static const String _shardCountArg = 'shardCount'; static const String _shardIndexArg = 'shardIndex'; + // Utility. + static const String _logTimingArg = 'log-timing'; /// The directory containing the packages. final Directory packagesDir; @@ -308,13 +325,15 @@ abstract class PackageCommand extends Command { _runOnChangedPackagesArg, _runOnDirtyPackagesArg, _packagesForBranchArg, + _currentPackageArg, }; if (packageSelectionFlags .where((String flag) => argResults!.wasParsed(flag)) .length > 1) { - printError('Only one of --$_packagesArg, --$_runOnChangedPackagesArg, or ' - '--$_packagesForBranchArg can be provided.'); + printError('Only one of the package selection arguments ' + '(${packageSelectionFlags.join(", ")}) ' + 'can be provided.'); throw ToolExit(exitInvalidArguments); } @@ -383,6 +402,14 @@ abstract class PackageCommand extends Command { if (packages.isEmpty) { return; } + } else if (getBoolArg(_currentPackageArg)) { + final String? currentPackageName = _getCurrentDirectoryPackageName(); + if (currentPackageName == null) { + printError('Unable to determine packages; --$_currentPackageArg can ' + 'only be used within a repository package or package group.'); + throw ToolExit(exitInvalidArguments); + } + packages = {currentPackageName}; } final Directory thirdPartyPackagesDirectory = packagesDir.parent @@ -545,6 +572,32 @@ abstract class PackageCommand extends Command { return packages; } + String? _getCurrentDirectoryPackageName() { + // Ensure that the current directory is within the packages directory. + final Directory absolutePackagesDir = packagesDir.absolute; + Directory currentDir = packagesDir.fileSystem.currentDirectory.absolute; + if (!currentDir.path.startsWith(absolutePackagesDir.path) || + currentDir.path == packagesDir.path) { + return null; + } + // If the current directory is a direct subdirectory of the packages + // directory, then that's the target. + if (currentDir.parent.path == absolutePackagesDir.path) { + return currentDir.basename; + } + // Otherwise, walk up until a package is found... + while (!isPackage(currentDir)) { + currentDir = currentDir.parent; + if (currentDir.path == absolutePackagesDir.path) { + return null; + } + } + // ... and then check whether it has an enclosing package. + final RepositoryPackage package = RepositoryPackage(currentDir); + final RepositoryPackage? enclosingPackage = package.getEnclosingPackage(); + return (enclosingPackage ?? package).directory.basename; + } + // Returns true if the current checkout is on an ancestor of [branch]. // // This is used because CI may check out a specific hash rather than a branch, diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart index 0ef6e602c070..aac8fa8122c3 100644 --- a/script/tool/lib/src/custom_test_command.dart +++ b/script/tool/lib/src/custom_test_command.dart @@ -28,6 +28,9 @@ class CustomTestCommand extends PackageLoopingCommand { @override final String name = 'custom-test'; + @override + List get aliases => ['test-custom']; + @override final String description = 'Runs package-specific custom tests defined in ' "a package's tool/$_scriptName file.\n\n" diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/dart_test_command.dart similarity index 85% rename from script/tool/lib/src/test_command.dart rename to script/tool/lib/src/dart_test_command.dart index 5101b8f19e7e..9a93d2d9a2aa 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/dart_test_command.dart @@ -12,9 +12,9 @@ import 'common/process_runner.dart'; import 'common/repository_package.dart'; /// A command to run Dart unit tests for packages. -class TestCommand extends PackageLoopingCommand { +class DartTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. - TestCommand( + DartTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -30,7 +30,13 @@ class TestCommand extends PackageLoopingCommand { } @override - final String name = 'test'; + final String name = 'dart-test'; + + // TODO(stuartmorgan): Eventually remove 'test', which is a legacy name from + // before there were other test commands that made it ambiguous. For now it's + // an alias to avoid breaking people's workflows. + @override + List get aliases => ['test', 'test-dart']; @override final String description = 'Runs the Dart tests for all packages.\n\n' @@ -66,7 +72,9 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', + if (pluginSupportsPlatform(platformWeb, package, + requiredMode: PlatformSupport.inline)) + '--platform=chrome', ], workingDir: package.directory, ); diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart index 77b44e11b59e..d16fb33e92f3 100644 --- a/script/tool/lib/src/dependabot_check_command.dart +++ b/script/tool/lib/src/dependabot_check_command.dart @@ -32,6 +32,9 @@ class DependabotCheckCommand extends PackageLoopingCommand { @override final String name = 'dependabot-check'; + @override + List get aliases => ['check-dependabot']; + @override final String description = 'Checks that all packages have Dependabot coverage.'; diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 30d4d178d065..837193b1cccc 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -52,6 +52,9 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { @override final String name = 'federation-safety-check'; + @override + List get aliases => ['check-federation-safety']; + @override final String description = 'Checks that the change does not violate repository rules around changes ' diff --git a/script/tool/lib/src/gradle_check_command.dart b/script/tool/lib/src/gradle_check_command.dart index 53da6405beb7..cbce766b3763 100644 --- a/script/tool/lib/src/gradle_check_command.dart +++ b/script/tool/lib/src/gradle_check_command.dart @@ -23,6 +23,9 @@ class GradleCheckCommand extends PackageLoopingCommand { @override final String name = 'gradle-check'; + @override + List get aliases => ['check-gradle']; + @override final String description = 'Checks that gradle files follow repository conventions.'; diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 0517bcf43298..a8d4a7be5f62 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -114,6 +114,9 @@ class LicenseCheckCommand extends PackageCommand { @override final String name = 'license-check'; + @override + List get aliases => ['check-license']; + @override final String description = 'Ensures that all code files have copyright/license blocks.'; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 1839524a85e9..78fa5a3f0c7b 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -13,6 +13,7 @@ import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_packages_app_command.dart'; import 'custom_test_command.dart'; +import 'dart_test_command.dart'; import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; @@ -31,7 +32,6 @@ import 'publish_command.dart'; import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'remove_dev_dependencies_command.dart'; -import 'test_command.dart'; import 'update_dependency_command.dart'; import 'update_excerpts_command.dart'; import 'update_min_sdk_command.dart'; @@ -41,17 +41,18 @@ import 'xcode_analyze_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); - - Directory packagesDir = - fileSystem.currentDirectory.childDirectory('packages'); + final Directory scriptDir = + fileSystem.file(io.Platform.script.toFilePath()).parent; + // Support running either via directly invoking main.dart, or the wrapper in + // bin/. + final Directory toolsDir = + scriptDir.basename == 'bin' ? scriptDir.parent : scriptDir.parent.parent; + final Directory root = toolsDir.parent.parent; + final Directory packagesDir = root.childDirectory('packages'); if (!packagesDir.existsSync()) { - if (fileSystem.currentDirectory.basename == 'packages') { - packagesDir = fileSystem.currentDirectory; - } else { - print('Error: Cannot find a "packages" sub-directory'); - io.exit(1); - } + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); } final CommandRunner commandRunner = CommandRunner( @@ -79,7 +80,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(RemoveDevDependenciesCommand(packagesDir)) - ..addCommand(TestCommand(packagesDir)) + ..addCommand(DartTestCommand(packagesDir)) ..addCommand(UpdateDependencyCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(UpdateMinSdkCommand(packagesDir)) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index f6eb3c164e24..d5f720a02e3a 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -73,6 +73,9 @@ class NativeTestCommand extends PackageLoopingCommand { @override final String name = 'native-test'; + @override + List get aliases => ['test-native']; + @override final String description = ''' Runs native unit tests and native integration tests. diff --git a/script/tool/lib/src/podspec_check_command.dart b/script/tool/lib/src/podspec_check_command.dart index dda08eee32be..9fcaa469955c 100644 --- a/script/tool/lib/src/podspec_check_command.dart +++ b/script/tool/lib/src/podspec_check_command.dart @@ -31,7 +31,7 @@ class PodspecCheckCommand extends PackageLoopingCommand { final String name = 'podspec-check'; @override - List get aliases => ['podspec', 'podspecs']; + List get aliases => ['podspec', 'podspecs', 'check-podspec']; @override final String description = diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 2d7f3ca47f29..0ac01535b445 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -53,6 +53,9 @@ class PublishCheckCommand extends PackageLoopingCommand { @override final String name = 'publish-check'; + @override + List get aliases => ['check-publish']; + @override final String description = 'Checks to make sure that a package *could* be published.'; diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index c56fa2723bb2..838aac0541b1 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -89,6 +89,9 @@ class PubspecCheckCommand extends PackageLoopingCommand { @override final String name = 'pubspec-check'; + @override + List get aliases => ['check-pubspec']; + @override final String description = 'Checks that pubspecs follow repository conventions.'; @@ -104,8 +107,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future initializeRun() async { // Find all local, published packages. for (final File pubspecFile in (await packagesDir.parent - .list(recursive: true, followLinks: false) - .toList()) + .list(recursive: true, followLinks: false) + .toList()) .whereType() .where((File entity) => p.basename(entity.path) == 'pubspec.yaml')) { final Pubspec? pubspec = _tryParsePubspec(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 3c119f04289f..7e28979c9565 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -48,6 +48,9 @@ class ReadmeCheckCommand extends PackageLoopingCommand { @override final String name = 'readme-check'; + @override + List get aliases => ['check-readme']; + @override final String description = 'Checks that READMEs follow repository conventions.'; diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart index 8583e4993c74..d50082377f86 100644 --- a/script/tool/lib/src/update_excerpts_command.dart +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -15,7 +15,7 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -/// A command to update README code excerpts from code files. +/// A command to update .md code excerpts from code files. class UpdateExcerptsCommand extends PackageLoopingCommand { /// Creates a excerpt updater command instance. UpdateExcerptsCommand( @@ -51,7 +51,7 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { final String name = 'update-excerpts'; @override - final String description = 'Updates code excerpts in README.md files, based ' + final String description = 'Updates code excerpts in .md files, based ' 'on code from code files, via code-excerpt'; @override @@ -105,13 +105,16 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { } if (getBoolArg(_failOnChangeFlag)) { - final String? stateError = await _validateRepositoryState(); + final String? stateError = await _validateRepositoryState(package); if (stateError != null) { - printError('README.md is out of sync with its source excerpts.\n\n' - 'If you edited code in README.md directly, you should instead edit ' - 'the example source files. If you edited source files, run the ' - 'repository tooling\'s "$name" command on this package, and update ' - 'your PR with the resulting changes.'); + printError('One or more .md files are out of sync with their source ' + 'excerpts.\n\n' + 'If you edited code in a .md file directly, you should instead ' + 'edit the example source files. If you edited source files, run ' + 'the repository tooling\'s "$name" command on this package, and ' + 'update your PR with the resulting changes.\n\n' + 'For more information, see ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code'); return PackageResult.fail([stateError]); } } @@ -138,14 +141,24 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { return exitCode == 0; } - /// Runs the injection step to update [targetPackage]'s README with the latest - /// excerpts from [example], returning true on success. + /// Runs the injection step to update [targetPackage]'s top-level .md files + /// with the latest excerpts from [example], returning true on success. Future _injectSnippets( RepositoryPackage example, { required RepositoryPackage targetPackage, }) async { - final String relativeReadmePath = - getRelativePosixPath(targetPackage.readmeFile, from: example.directory); + final List relativeMdPaths = targetPackage.directory + .listSync() + .whereType() + .where((File f) => + f.basename.toLowerCase().endsWith('.md') && + // Exclude CHANGELOG since it should never have excerpts. + f.basename != 'CHANGELOG.md') + .map((File f) => getRelativePosixPath(f, from: example.directory)) + .toList(); + if (relativeMdPaths.isEmpty) { + return true; + } final int exitCode = await processRunner.runAndStream( 'dart', [ @@ -154,7 +167,7 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { '--write-in-place', '--yaml', '--no-escape-ng-interpolation', - relativeReadmePath, + ...relativeMdPaths, ], workingDir: example.directory); return exitCode == 0; @@ -212,11 +225,11 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { /// Checks the git state, returning an error string if any .md files have /// changed. - Future _validateRepositoryState() async { + Future _validateRepositoryState(RepositoryPackage package) async { final io.ProcessResult checkFiles = await processRunner.run( 'git', ['ls-files', '--modified'], - workingDir: packagesDir, + workingDir: package.directory, logOnError: true, ); if (checkFiles.exitCode != 0) { diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fa9f43519499..a2ea016c3311 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -171,6 +171,9 @@ class VersionCheckCommand extends PackageLoopingCommand { @override final String name = 'version-check'; + @override + List get aliases => ['check-version']; + @override final String description = 'Checks if the versions of packages have been incremented per pub specification.\n' diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index a81bf15477af..7a9a5953f526 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -42,6 +42,9 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override final String name = 'xcode-analyze'; + @override + List get aliases => ['analyze-xcode']; + @override final String description = 'Runs Xcode analysis on the iOS and/or macOS example apps.'; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 44babd7f0f4b..925d4b60697a 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_plugin_tools description: Productivity and CI utils for flutter/packages repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.13.4+4 +version: 1.0.0 +publish_to: none dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index 24100bafede3..2ef9b9e8438e 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -319,8 +319,7 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') ])); }); @@ -338,8 +337,25 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') + ])); + }); + + test('does not allow --packages with --current-package', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of the package selection arguments') ])); }); @@ -359,10 +375,94 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') + ])); + }); + }); + + group('current-package', () { + test('throws when run from outside of the packages directory', () async { + fileSystem.currentDirectory = packagesDir.parent; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('--current-package can only be used within a repository ' + 'package or package group') ])); }); + + test('throws when run directly in the packages directory', () async { + fileSystem.currentDirectory = packagesDir; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('--current-package can only be used within a repository ' + 'package or package group') + ])); + }); + + test('runs on a package when run from the package directory', () async { + final RepositoryPackage package = + createFakePlugin('a_package', packagesDir); + createFakePlugin('another_package', packagesDir); + fileSystem.currentDirectory = package.directory; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, unorderedEquals([package.path])); + }); + + test('runs on a package when run from a package example directory', + () async { + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, + examples: ['a', 'b', 'c']); + createFakePlugin('another_package', packagesDir); + fileSystem.currentDirectory = package.getExamples().first.directory; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, unorderedEquals([package.path])); + }); + + test('runs on a package group when run from the group directory', + () async { + final Directory pluginGroup = packagesDir.childDirectory('a_plugin'); + final RepositoryPackage plugin1 = + createFakePlugin('a_plugin_foo', pluginGroup); + final RepositoryPackage plugin2 = + createFakePlugin('a_plugin_bar', pluginGroup); + createFakePlugin('unrelated_plugin', packagesDir); + fileSystem.currentDirectory = pluginGroup; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); }); group('test run-on-changed-packages', () { diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/dart_test_command_test.dart similarity index 79% rename from script/tool/test/test_command_test.dart rename to script/tool/test/dart_test_command_test.dart index 5d34e390fd00..7f52fcb176c6 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/dart_test_command_test.dart @@ -7,7 +7,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:flutter_plugin_tools/src/dart_test_command.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -15,7 +15,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$TestCommand', () { + group('TestCommand', () { late FileSystem fileSystem; late Platform mockPlatform; late Directory packagesDir; @@ -27,23 +27,38 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final TestCommand command = TestCommand( + final DartTestCommand command = DartTestCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, ); - runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner = CommandRunner('test_test', 'Test for $DartTestCommand'); runner.addCommand(command); }); + test('legacy "test" name still works', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, + extraFiles: ['test/a_test.dart']); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin.path), + ]), + ); + }); + test('runs flutter test on each plugin', () async { final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -63,7 +78,7 @@ void main() { 'example/test/an_example_test.dart' ]); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -86,13 +101,13 @@ void main() { .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ FakeProcessInfo( - MockProcess(exitCode: 1), ['test']), // plugin 1 test - FakeProcessInfo(MockProcess(), ['test']), // plugin 2 test + MockProcess(exitCode: 1), ['dart-test']), // plugin 1 test + FakeProcessInfo(MockProcess(), ['dart-test']), // plugin 2 test ]; Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -110,7 +125,7 @@ void main() { final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -128,7 +143,7 @@ void main() { extraFiles: ['test/empty_test.dart']); await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); + runner, ['dart-test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, @@ -153,7 +168,7 @@ void main() { 'example/test/an_example_test.dart' ]); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -178,7 +193,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -203,7 +218,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -226,7 +241,7 @@ void main() { }, ); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -239,6 +254,27 @@ void main() { ); }); + test('Does not run on Chrome for web endorsements', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: ['test/empty_test.dart'], + platformSupport: { + platformWeb: const PlatformDetails(PlatformSupport.federated), + }, + ); + + await runCapturingPrint(runner, ['dart-test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin.path), + ]), + ); + }); + test('enable-experiment flag', () async { final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); @@ -246,7 +282,7 @@ void main() { extraFiles: ['test/empty_test.dart']); await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); + runner, ['dart-test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 7bb0297de131..09862b3b3212 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -111,7 +111,7 @@ void main() { test('updates example readme when config is present', () async { final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); + extraFiles: [kReadmeExcerptConfigPath, 'example/README.md']); final Directory example = getExampleDir(package); final List output = @@ -153,6 +153,52 @@ void main() { ])); }); + test('includes all top-level .md files', () async { + const String otherMdFileName = 'another_file.md'; + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, + extraFiles: [kReadmeExcerptConfigPath, otherMdFileName]); + final Directory example = getExampleDir(package); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + '../README.md', + '../$otherMdFileName', + ], + example.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + test('skips when no config is present', () async { createFakePlugin('a_package', packagesDir); @@ -277,7 +323,7 @@ void main() { test('fails if example injection fails', () async { createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); + extraFiles: [kReadmeExcerptConfigPath, 'example/README.md']); processRunner.mockProcessesForExecutable['dart'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), @@ -307,7 +353,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: [kReadmeExcerptConfigPath]); - const String changedFilePath = 'packages/a_plugin/README.md'; + const String changedFilePath = 'README.md'; processRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo(MockProcess(stdout: changedFilePath)), ]; @@ -323,9 +369,10 @@ void main() { expect( output, containsAllInOrder([ - contains('README.md is out of sync with its source excerpts'), + contains( + 'One or more .md files are out of sync with their source excerpts'), contains('Snippets are out of sync in the following files: ' - 'packages/a_plugin/README.md'), + '$changedFilePath'), ])); });